做商城类系统我前后折腾过好几个版本。最开始图省事一个单体JSP项目硬扛所有模块结果用户管理、商品库存、订单状态机全挤在一起改一个BUG牵一发动全身。后来换成SpringSpringMVCMyBatis这套组合也就是大家常说的SSM业务层清爽了不少。但光靠SSM也不是万能——数据分析、可视化、推荐算法这些场景Python生态明显更顺手硬用Java重写一遍纯属给自己挖坑。所以这次做的购物商场系统我直接采用了SSM为主、Flask为辅的双引擎架构把两边最擅长的事情交给对应的技术栈。这套系统面向的其实是两类人一类是正在做毕业设计或课程设计的学生需要一套结构完整、能跑通全流程的商城项目源码作为参考骨架另一类是想把传统线下商场搬到线上的中小型运营团队需要一个包含商品、订单、会员、库存、数据看板的轻量级管理平台。整篇文章我会从架构设计讲起把核心模块的实现思路、部署过程中的坑、联调时的细节全部摊开来讲如果你正准备动手搭一套类似的系统这篇内容可以直接当施工图纸用。1. 项目架构设计与技术选型思路1.1 为什么选择SSMFlask双引擎组合先说说选型这件事。很多人在做商城系统时有个误区觉得一个后端框架打天下就够了。但实际上商城系统背后藏着两类截然不同的需求一类是强事务、高一致性的交易链路用户下单、库存扣减、订单状态流转每一步都不能出错另一类是弱一致性的分析链路比如销售趋势统计、商品热度排行、用户画像标签这些数据晚几分钟同步完全不影响业务。SSM里的Spring负责管理Bean和事务SpringMVC处理HTTP请求的路由分发MyBatis把SQL和Java对象做映射这三兄弟组合在一起处理交易类业务的成熟度是经过大量生产环境验证的。尤其MyBatis手写SQL灵活度极高商城系统中那些复杂的多表关联查询、动态条件拼装用MyBatis写起来比JPA那种自动生成SQL的方式要可控得多慢查询也好优化。那为什么还要引入Flask直接原因是我在系统里做了一个数据可视化大屏模块需要从订单表、商品表里拉数据做聚合分析然后生成图表。用Java做图表不是不行但Python的Pandas做数据清洗聚合、Plotly生成交互式图表开发效率是Java的几倍。与其在Java里费劲折腾不如拆一个独立的Flask服务出来专门负责数据分析与可视化接口。Flask轻量、启动快用SQLAlchemy连MySQL读点只读数据根本不占多少资源而且作为独立进程部署就算挂了也不影响主交易链路。这套组合本质上就是微服务思想的一个最小实践按业务边界拆服务每个服务用最合适的语言和框架实现。前面提到的两类用户都能从这个架构里得到自己想要的——学生可以学到异构系统之间如何协作运营团队则拿到了一套可扩展的基础骨架。1.2 核心模块划分与系统边界整个系统我按业务域划分成了五个核心模块用户中心、商品中心、订单中心、库存中心和数据分析中心。前四个落在SSM主服务里数据分析中心作为独立Flask服务运行。用户中心负责注册登录、会员等级、收货地址管理权限上区分了普通用户和管理员两种角色。商品中心管理商品分类、品牌、规格参数、上下架状态以及商品主图和详情图的存储路径。订单中心是整个系统的核心包含购物车、下单、支付回调、订单状态流转、退款退货一整套流程。库存中心与商品中心紧密耦合每次下单要锁定库存支付成功扣减库存取消订单则回补库存。数据分析中心比较特别它通过只读账号直接连接主数据库的业务表定时用SQL拉取前一天的订单、商品、用户数据到独立的分析库然后在Flask里用Pandas做聚合统计对外暴露REST接口前端的大屏页面按分钟级频率轮询这些接口刷新图表。服务之间的通信方式也很简单SSM和Flask之间通过HTTP接口调用不存在消息中间件。我把Flask的接口地址配置在SSM的配置文件里业务层需要图表数据时直接发起HTTP请求。如果后续要做成生产级可以加一个RabbitMQ或Redis Stream做异步解耦但当前阶段这套同步调用的方案足够简单可靠出了问题也好排查。1.3 数据库设计与核心表结构数据库我用的是MySQL 5.7字符集统一utf8mb4——这里提醒一下千万别用utf8否则用户昵称里出现生僻字或emoji表情时会直接报错或乱码。核心表一共设计了十几张这里挑几张最关键的说说。用户表user:主键id、username、passwordBCrypt加密存储、phone、email、avatar、member_level、status。member_level用于区分普通用户和VIPstatus控制账号是否禁用删除用户永远用逻辑删除也就是置一个deleted_flag不要物理删行后面做数据分析时要保留历史数据。商品表product:主键id、category_id、brand_id、name、sub_title、main_image、detail_html、price、stock、sales、status。价格字段我用的是DECIMAL(10,2)不是float浮点数算钱会有精度丢失问题这点算是个基本功。status有0下架、1上架、2删除三种状态。订单表orders:主键id、order_no唯一业务编号、user_id、total_amount、pay_amount、status、address_snapshot、create_time、pay_time。order_no我直接用时间戳加随机数生成格式类似20250115143022001保证高并发下不重复。address_snapshot是把用户下单时的收货地址快照成JSON字符串存进去这样即使用户之后改了默认地址订单里的地址也不会变——这是电商系统的一个经典设计。订单明细表order_item:记录订单里的每个商品快照含product_id、product_name、product_image、price、quantity、total_price。这里同样存快照因为商品名称和价格后续可能调整订单详情必须保持下单那一刻的信息。库存表stock:product_id、total_stock、locked_stock、available_stock。下单时锁库存就是把商品从available_stock挪到locked_stock支付成功再从locked_stock里扣掉取消订单就把locked_stock还回available_stock。这套字段设计能有效避免超卖。表名核心字段用途userusername, password, member_level用户账号与会员等级productprice, stock, status商品信息与销售状态ordersorder_no, status, address_snapshot订单主信息与状态流转order_itemproduct_id, price, quantity订单商品快照stocktotal_stock, locked_stock库存锁定与扣减数据分析中心这边有自己的分析库通过定时任务把主库的业务数据同步过来主要表就是daily_sales每日销售额汇总和product_rank商品销售排行。主库和分析库分开的好处是跑聚合SQL时不会拖垮线上交易库的性能。2. 核心功能模块与实现细节2.1 用户认证、权限拦截与会话管理用户模块的第一个重点是登录认证。密码加密我用的BCryptSpring Security里自带BCryptPasswordEncoder但我不想为了一个加密方法引入整套Security框架所以直接引了spring-security-crypto这个独立依赖轻量又干净。BCrypt的好处是每次加密会自动加盐即使两个用户密码相同加密后的密文也不一样就算数据库泄露彩虹表攻击也拿不到原始密码。登录成功后我用Redis来存会话。用户ID作为key用户信息的JSON作为value过期时间设成30分钟每次请求时拦截器从请求头里取token然后去Redis查会话。相比传统的HttpSessionRedis会话的优势是支持多实例部署时的会话共享以后系统横向扩展也不用改代码。token的生成规则是UUID加用户ID拼接用MD5哈希一下既保证不可预测又不过度设计。权限拦截这块我在SpringMVC里写了一个LoginInterceptor注册的时候配置了拦截路径规则所有/admin/**路径必须是管理员角色才能访问所有/user/**路径必须登录静态资源和公共接口放行。拦截器里除了校验登录状态还会把当前用户信息塞进ThreadLocal业务层随时可以拿到当前操作人这个设计在生成订单操作日志时非常有用。这里分享一个我踩过的坑Interceptor里校验完权限后千万别忘了在preHandle方法返回true否则你会在一个BUG上浪费两小时。还有就是设置排除路径时要仔细核对比如登录接口的路径如果是/user/login但拦截规则写成了/user/**全部拦截用户永远登录不上因为登录请求根本到不了Controller。2.2 商品发布、上下架与多图管理商品管理是后台管理员用得最频繁的模块。商品表里我用一个detail_html字段存商品详情页的富文本内容后台集成了UEditor编辑器图片上传后返回URL前端展示时直接用HTML渲染。这里有个注意点富文本编辑器上传图片默认是base64编码直接塞进HTML里的一张几MB的图片会让整个详情页膨胀到十几MB页面加载慢得没法用。解决办法是配置UEditor的上传接口为独立图片接口上传后返回图片URL编辑器里存储的是URL地址。商品主图和轮播图我用了FastDFS分布式文件系统来存nginx做HTTP访问代理。上传接口接收MultipartFile后调用FastDFS客户端上传返回的路径格式是group1/M00/00/01/xxx.jpg数据库里存的是这个相对路径前端拼上nginx的访问域名就能完整访问。如果你不想搭FastDFS直接用本地磁盘存储加nginx映射也可以小规模项目完全够用但千万注意服务器磁盘扩容的问题图片存多了迟早会满。商品上下架我用的状态机思路status字段在三态之间流转上架操作必须从下架状态才能转成上架不允许从删除状态直接上架。删除是逻辑删管理员在回收站里还能恢复。每次上下架操作都会往操作日志表里写一条记录谁在什么时间把哪个商品改成了什么状态责任可追溯这个习惯在做后台系统时一定要养成。编辑商品时还有一个细节库存字段的编辑入口要单独放在库存管理模块里商品编辑页不能直接改库存避免运营人员改价格时手滑把库存改了。库存的变更只能通过入库单、出库单这类单据操作来驱动每次变更都有流水记录这对后续排查库存差异问题至关重要。2.3 购物车、订单状态机与库存扣减策略购物车我直接用Redis的Hash结构来存key是cart:userIdfield是productIdvalue是数量。用Redis存购物车的好处是读写都快而且不用建表用户添加、修改、删除购物车都是O(1)级别的操作。用户在商品详情页加入购物车前端调用加入购物车接口后端把数据写进Redis并设置过期时间比如7天避免长期不登录产生垃圾数据。下单流程是这套系统里最需要谨慎的部分。前端提交订单时后端要做这几件事校验商品是否上架、校验库存是否充足、计算总金额、生成订单号、锁定库存、清空购物车、返回订单ID。库存锁定我用的是带条件的UPDATE语句UPDATE stock SET locked_stock locked_stock #{quantity} WHERE product_id #{productId} AND available_stock #{quantity}。这条SQL利用数据库行锁保证并发安全如果影响行数为0说明库存不足直接抛异常回滚事务就不会出现超卖的问题。订单状态我定义了一个枚举类包含待付款、已付款待发货、已发货、已完成、已取消、退款中、已退款这几个状态。每个状态之间的流转写在一个OrderStatusService里用switch判断当前状态和目标状态是否合法比如待付款状态下只能流转到已付款或已取消不能跳到已完成。这种集中式的状态管理比散落在各个Service里的if判断要清晰得多后续加状态也好维护。支付这块我接的是支付宝沙箱环境SDK封装好后调用比较简单。核心逻辑在支付回调接口里支付宝异步通知到达后先验签确认是支付宝发的请求然后根据trade_status判断支付结果把订单从待付款流转到已付款同时扣减锁定库存给用户加积分。这里有一个特别重要的坑支付回调接口一定要做幂等处理因为支付宝的网络通知可能会重复发送多次如果每次收到回调都执行一遍扣库存加积分的逻辑数据就乱了。我的做法是先在Redis里用SETNX设置一个order:pay:orderNo的key设置成功才执行处理逻辑处理完删掉key重复回调直接返回success。2.4 Flask数据分析服务与大屏可视化Flask服务这块我单独建了一个项目目录结构大概是这样的app.py是入口文件models.py放SQLAlchemy的模型api.py放路由接口utils.py放公共工具函数。数据库连接我用的是PyMySQL加SQLAlchemy连接串里配置的是只读账号账号权限只给了SELECT即使接口被注入攻击最坏情况也就是读数据写不了库。数据分析的定时同步我用的APScheduler每天凌晨两点执行一次同步任务把前一天的数据从主库聚合后写入分析库。任务逻辑用SQL查询订单表按天聚合出销售额、订单量、客单价写入daily_sales表再从order_item表按商品维度聚合并排序取前20名写入product_rank表。用纯SQL做聚合比把数据拉到内存里用Pandas算要高效得多能下推到数据库的运算就别在应用层做。可视化接口方面我给大屏提供了三个核心接口。第一个是销售趋势接口返回最近30天的每日销售额前端用ECharts折线图展示第二个是品类占比接口按分类统计销售额占比前端用饼图展示第三个是实时订单接口查询最近一小时的订单列表前端用滚动列表展示。Flask侧要注意CORS跨域问题前端页面如果部署在另一个端口请求Flask接口会触发跨域拦截解决办法是用Flask-CORS扩展统一配置allow_origins。我在实际运行中发现一个细节Flask的默认开发服务器Werkzeug是单线程的并发一高就会阻塞。如果大屏页面的访问量比较大建议用gunicorn启动Flask配置4个worker和2个线程性能提升非常明显。启动命令是gunicorn -w 4 -b 0.0.0.0:5000 app:app生产环境一定要换成这个别用app.run()。3. 实操部署与环境配置全记录3.1 从零搭建开发环境与依赖清单开发环境这里列一份完整的清单照着配就行。JDK用1.8虽然现在Java 17甚至21都很成熟但SSM这套老架构配合JDK8是最稳的组合网上资料也最多遇到问题好搜。Maven用3.6.3MySQL用5.7Redis用6.xPython用3.8以上。IDE方面Java侧我用IDEAFlask侧用PyCharm两边各开一个窗口联调时切换起来效率高。Java侧Maven依赖里有几个核心坐标需要划重点spring-webmvc 5.3.x、mybatis-spring 2.0.x、mybatis 3.5.x、druid 1.2.x、fastjson 1.2.83、lombok 1.18.x、spring-security-crypto 5.3.x。版本号一定要锁死这些别用latestSSM这套组合的版本兼容性比较敏感随便升一个小版本有可能引爆一堆Bean注入的异常。Flask侧用到的主要依赖是Flask 2.2.x、Flask-SQLAlchemy 3.0.x、Flask-CORS、APScheduler、PyMySQL。Python的依赖用pip install -r requirements.txt一键装完requirements文件用pip freeze requirements.txt生成确保团队成员装出来的环境一致。数据库初始化这块我准备了一个init.sql脚本包含建库、建表、插入初始管理员账号和测试商品数据。管理员账号的密码是BCrypt加密后的密文直接用SQL插入而不是通过代码初始化这样部署时不用额外跑一个初始化接口。测试数据我造了20个商品、5个分类、3个用户方便前端开发时有数据可看。3.2 SSM三件套的配置要点与坑位提醒SSM配置是新手最容易卡住的地方我梳理一下我项目里的标准配置结构。web.xml里配置DispatcherServlet和Spring的ContextLoaderListenerspring-mvc.xml里开启注解驱动、配置静态资源映射、注册视图解析器spring-mybatis.xml里配置数据源、SqlSessionFactory和Mapper扫描。数据源我用的阿里Druid连接池配置里几个核心参数initialSize设5minIdle设5maxActive设20maxWait设60000。maxWait是获取连接的最大等待时间单位毫秒超时会抛异常避免请求无限挂起。Druid的监控页面也顺手打开访问项目的/druid路径就能看到SQL执行情况、连接池使用率排查慢SQL时特别有用。MyBatis这块我重点说两个配置。一个是驼峰映射在mybatis-config.xml里设置mapUnderscoreToCamelCase为true这样数据库的create_time字段能自动映射到Java对象的createTime属性不用写一堆resultMap。另一个是Mapper接口扫描在spring-mybatis.xml里配mybatis:scan base-packagecom.shop.mapper/启动时会把所有Mapper接口注册成Bean注意包路径不要配错否则启动直接报NoSuchBeanDefinitionException。事务管理我是用注解驱动的在spring-mybatis.xml里配一个DataSourceTransactionManager然后在Service实现类或方法上标注Transactional(rollbackFor Exception.class)。有一个非常容易踩的坑Transactional只对public方法生效而且默认只在RuntimeException时回滚如果你在方法里抛的是自定义的checked exception不回滚的。解决方法是rollbackFor显式指定Exception.class或者自定义异常继承RuntimeException。3.3 Flask服务打包、启动与Java侧联调Flask服务部署我做了两种方案本地开发直接python app.py服务器上用gunicorn。项目里我写了一个deploy.sh脚本内容是先进入项目目录用git pull拉最新代码然后pip install -r requirements.txt刷新依赖最后用gunicorn重新启动服务。脚本里用pkill -f gunicorn先杀掉旧进程再启动新的虽然粗暴但够用也可以换成gunicorn的优雅重启方式不过对于内部服务来说几秒中断完全能接受。Java和Flask联调时跨语言的数据格式统一走JSON。Java侧用RestTemplate发HTTP请求调用Flask接口反序列化用fastjson。我在调用Flask的地方做了一个降级处理如果Flask接口响应超时或返回500Java侧直接返回一个空的数据结构而不是报异常让整个页面挂了。大屏数据晚几分钟刷新没关系但主站页面因为分析服务挂了就白屏那是不能接受的。跨域问题在联调时几乎必现解决办法我前面提到了用Flask-CORS。但这里还有一个小坑如果Java侧是通过服务端RestTemplate调用Flask而不是浏览器直连其实不存在跨域问题CORS只管浏览器端的跨域请求。所以如果你的页面数据链路是浏览器→Java→Flask那根本不用配CORS。只有页面直接请求Flask接口时才需要配。搞清楚请求链路再动手别做无用功。联调场景请求链路是否需要CORS大屏图表数据浏览器 → Java → Flask不需要页面直连Flask浏览器 → Flask需要配置内部服务间调用Java → Flask不需要4. 常见问题与排查技巧实录4.1 SSM层高频异常与排查路径开发SSM项目遇到最多的就是Bean相关异常。ClassNotFoundException和NoClassDefFoundError基本都是依赖缺失或版本冲突解决办法是mvn dependency:tree看依赖树排查有没有jar包被错误排除或版本被覆盖。BeanCreationException通常是配置没扫到检查一下spring的component-scan包路径和Mapper扫描包路径是否一致。还有一个很隐蔽的问题MyBatis的Mapper接口和XML文件的namespace不匹配运行时会报BindingException。我的排查习惯是启动时看日志里有没有ParsingError同时检查XML文件是否被Maven打包进了classes目录——有时候XML放在src/main/java目录下但pom里没配resources过滤规则编译后XML没被复制出去Mapper就找不到对应的Statement。在pom.xml的build节点里加一段 配置把src/main/java下的.xml和.properties文件一并打包这个问题就能从根上解决。数据库连接超时也是高发问题。MySQL默认的wait_timeout是8小时如果系统部署后连续几天没人访问连接池里那些空闲连接会被数据库杀掉然后某个用户突然访问系统Druid还没来得及检测就把死连接分发出去直接抛CommunicationsException。解决办法是在Druid配置里设置testWhileIdle为true并配置timeBetweenEvictionRunsMillis为60000让连接池定时检测并剔除死连接同时给MySQL的wait_timeout设置成和连接池的maxEvictableIdleTimeMillis匹配。4.2 Flask与Java联调经典坑位Flask这侧的坑我印象最深的是编码问题。MySQL的utf8mb4字符集在Flask侧读取时如果连接串里没指定charsetutf8mb4中文会乱码。连接串要写成mysqlpymysql://user:passhost:port/db?charsetutf8mb4注意是问号拼接参数别写错位置。Java侧JDBC连接串也要加useUnicodetruecharacterEncodingutf8两边字符集一致才能保证数据互通。另一个坑是时间格式的序列化差异。Java的fastjson序列化日期默认输出时间戳Python的json.dumps处理datetime对象会直接报错。我的统一做法是Java侧在日期字段上加JSONField(formatyyyy-MM-dd HH:mm:ss)注解Flask侧写一个自定义的json encoder把datetime格式化成字符串。两边都输出标准字符串格式前端解析就省心多了。联调时接口返回格式也要提前约定好。我在系统里统一了响应结构{code: 200, message: success, data: ...}Java和Flask都按这个格式返回。code为200表示成功401表示未登录500表示服务端异常。前端封装了一个统一拦截器根据code全局处理错误提示不用在每个页面单独写错误处理逻辑。4.3 性能优化与并发安全经验总结压测的时候我发现下单接口在高并发下响应变慢主要瓶颈在库存扣减这条SQL上的行锁竞争。优化思路是减少锁的持有时间在事务里把库存扣减放在最前面执行然后立刻提交后面的订单创建、购物车清空不在事务里做失败了通过补偿机制处理。另外把热点商品的库存预加载到Redis里先用Redis的原子操作DECR扣减再异步同步到数据库这也是电商系统常用的秒杀优化方案不过我当前系统并发量还没到那个程度数据库锁的方案暂时够用。还有一个容易被忽略的性能点商品列表页的分页查询。如果直接用LIMIT offset, size随着数据量增长offset越大查询越慢因为MySQL要扫描并丢弃前面的记录。我的优化方式是采用分页游标方案前端传入上一页最后一条记录的IDSQL改成WHERE id #{lastId} ORDER BY id LIMIT #{size}这种方式的查询速度不受数据总量影响在大数据量场景下体验明显更好。最后说说日志。我在系统里用了SLF4J加Logback生产环境的日志级别设为INFO关键业务节点下单、支付回调、库存变动单独打业务日志日志里包含订单号和用户ID排查问题时直接grep订单号就能把整条链路串起来。线上环境千万不要用System.out.println打印日志不仅影响性能而且日志没有分级和文件滚动排查问题时很难定位。5. 项目扩展方向与实战心得系统做完之后回看有几个方面如果时间允许我一定会继续完善。第一个是把单体的SSM服务按用户、商品、订单拆成三个独立服务然后用Spring Cloud Alibaba这套微服务框架串联起来每个服务独立部署独立扩容这也是这套架构天然的演进方向。第二个是引入消息队列把订单创建后的发短信、加积分、数据分析这些非核心操作做成异步消息减少下单接口的响应时间。不过说句实在话对大多数使用场景——毕业设计展示、中小型商城起步——当前这套SSMFlask双引擎架构已经完全够用了。它的价值不在于技术有多前沿而在于架构边界清楚交易业务在SSM里分析业务在Flask里出了问题能快速定位加了功能知道往哪个模块放。这种清晰度对于一个项目的长期维护来说比任何花哨的技术栈都重要。我在实际项目中积累的一条最深的经验是做这种多功能系统先别急着写代码把数据表设计好、把模块边界画清楚后面能少踩百分之八十的坑。表结构设计不合理导致的返工远比你想象中更耗时。另外如果你是学生答辩时一定要能讲清楚每个模块为什么这么设计尤其是库存锁定、订单状态机、支付幂等这三个点这是整份项目中技术含量最高的地方也是评委最感兴趣的部分。最后再分享一个小技巧开发过程中养成每完成一个功能就顺手提交一次代码的习惯commit信息写清楚改了什么东西。别等到功能全做完才提交万一中途改崩了没有版本回退点会很痛苦。这套系统的完整源码、设计文档和调试记录我都整理好了有需要可以参考着去搭关键是动手把架构思路落到代码里踩过一遍坑之后这些经验才是真正长在自己身上的。