一年多前我接了一个用微信小程序做购物商城的项目后端选型就是经典的 SSMSpring SpringMVC MyBatis整套源码做完之后交付得挺顺利。最近好几个朋友问我这类小程序商城 SSM 后端的项目该怎么完整落地今天干脆把整个思路和技术细节从头到尾捋一遍。这类项目在计算机专业里特别常见毕业生选题、课程设计、私活外包都绕不开。很多人拿到需求就急着写代码结果做到一半发现接口对不上、登录态搞不定、订单状态一塌糊涂最后赶工交付一身 bug。这篇文章我会从数据库设计、接口规划、小程序端对接、订单流转、部署踩坑几个核心环节逐一拆开讲适合刚接触小程序开发、想做商城类项目的同学也适合一个人从零单干接私活的开发者参考。1. 项目定位与技术选型为什么小程序商城要用 SSM 这套经典组合1.1 项目核心需求拆解先把这个项目到底要做什么说清楚。一个购物商城小程序面向的用户是普通消费者面向的管理员是运营人员核心业务链路是用户登录 → 浏览商品 → 查看详情 → 加入购物车 → 提交订单 → 支付 → 查看订单状态。细拆下来前端小程序需要这些页面首页轮播图分类入口推荐商品、商品列表页、商品搜索页、商品详情页、购物车页、订单确认页、订单列表页、个人中心页、地址管理页。后台管理端需要商品管理增删改查、分类管理、轮播图管理、订单管理发货、查看详情、用户管理。有人会说这功能看起来不复杂为什么要用 SSM 这种老牌框架组合这就要说到项目场景了。如果你拿到的是一个课程设计项目要求就是 SSM如果你接的是小商家的私活更重要的前提是跑得稳、改得动、后续人能接手。SSM 最大优势是结构清晰Spring 管对象、SpringMVC 管路由、MyBatis 管数据库操作每一层职责单一出了问题很好定位。而且作为国内高校和培训机构覆盖面最广的组合绝大多数初级开发都看得懂、改得动这对项目交付后的维护非常有价值。1.2 技术栈对比SSM、Spring Boot、Node.js 各适合什么场景这里先不急着写代码把选型逻辑说透。我实际对比过几个方案。技术方案优势劣势适合场景SSMSpringSpringMVCMyBatis分层明确、资料多、毕业生熟悉、部署资源占用低配置繁琐、需手动装配课程设计、毕业设计、低成本私活Spring Boot MyBatis-Plus开发效率高、自动配置省心、社区活跃相对依赖更重、学习曲线稍陡中小企业真实落地项目Node.jsExpress/Koa前后端语言统一、异步效率高类型约束弱、团队技能匹配要求高轻量级原型、实时交互类应用我这个项目选择 SSM还有一个很实际的原因目标服务器配置不高一台 2 核 4G 的小型云服务器就够了。SSM 打出来的 war 包丢进 Tomcat 就能跑省掉很多折腾成本。1.3 整体项目结构规划项目分为两个子工程后端一个标准的 SSM 分层结构的 web 项目包含 controller、service、mapperdao、entity、config 包结构前端微信小程序原生框架编写的客户端按页面 pages 组织外加 utils 封装的网络请求模块有人喜欢用现成的若依、人人开源这类脚手架直接改但如果你跟我一样是抱着把技术抠明白的想法做的项目建议从空目录开始创建自己把 Spring 容器、MyBatis 配置、mvc 拦截器一步步搭一遍。这样虽然前期慢一点但后面排错会非常快。2. 数据库设计与后端接口规划先把地基打牢2.1 数据表设计与字段规范购物商城项目的数据表不多但这几张表的关系如果没设计好后面会出现一堆逻辑漏洞。我的核心表设计如下用户表userid、openid微信唯一标识、nickname、avatar_url、phone、create_time商品分类表categoryid、name、sort、status商品表productid、category_id、name、subtitle、main_image、detail、price、stock、sales、status、create_time轮播图表bannerid、image_url、product_id、sort、status购物车表cartid、user_id、product_id、quantity、checked、create_time订单表ordersid、order_no、user_id、receiver_name、receiver_phone、receiver_address、total_amount、status、create_time、pay_time、ship_time、finish_time订单明细表order_itemid、order_id、product_id、product_name、product_image、current_price、quantity收货地址表addressid、user_id、receiver_name、receiver_phone、receiver_address、is_default几个容易忽略的字段我单独说明一下。商品表里 sales 字段用来记录销量这是首页热门推荐排序的重要依据status 字段对商品做上下架控制不能只靠物理删除。订单表的 order_no 一定要用唯一的订单号而不是用自增 id 暴露给前端否则用户可以通过 id 越权查看别人的订单这是一个很常见的安全疏漏。订单状态字段我建议用 int 类型存储0 待付款、1 待发货、2 待收货、3 已完成、4 已取消比存字符串更省空间状态流转也好写。2.2 数据库初始化与 MyBatis 配置要点建表语句写起来比较常规但在 MyBatis 配置上我踩过不少坑重点说两个。第一个是驼峰命名映射。数据库表字段一般是下划线命名如 create_timeJava 实体类属性是驼峰命名如 createTime一定记得在 mybatis-config.xml 里开启configuration settings setting namemapUnderscoreToCamelCase valuetrue/ /settings /configuration忘记这一句的话你会发现查询出来的 createTime 一直是 null排查半天也想不到是映射问题。第二个是分页。SSM 项目里我推荐手动写 LIMIT 语句配合 PageHelper 插件但如果用 PageHelper需要在 pom.xml 引入依赖同时注意版本的兼容性旧版本在 Spring 5 下会报错。2.3 后端接口规划RESTful 风格下的路径设计接口设计直接决定前端能否顺畅对接。我先列一下这个项目实际用到的接口清单后续前端开发围绕这套接口展开。模块请求方式路径说明用户POST/user/login微信登录code 换 token商品GET/product/list商品列表支持分页和分类筛选商品GET/product/detail/{id}商品详情商品GET/product/search商品搜索按名称模糊匹配首页GET/home/data返回轮播图、分类、推荐商品聚合数据购物车GET/cart/list当前用户购物车列表购物车POST/cart/add加入购物车购物车PUT/cart/update修改数量、勾选状态购物车DELETE/cart/delete/{id}删除购物车项订单POST/order/create创建订单订单GET/order/list订单列表按状态筛选订单GET/order/detail/{orderNo}订单详情订单PUT/order/cancel取消订单地址GET/address/list收货地址列表地址POST/address/save保存地址管理端POST/admin/product/save新增/修改商品管理端POST/admin/order/ship订单发货接口设计有两个原则一是所有业务接口都要求登录态前端通过请求头或参数携带 token二是首页聚合数据用一个接口拉取减少小程序的请求次数这对提升首屏加载速度很有帮助。3. 微信小程序端与 SSM 后端的会话对接3.1 微信登录流程code 换 openid 再换 token小程序端没有 cookie 和 session 的概念这是很多从 Web 开发转过来的朋友最不适应的一点。传统的 JSESSIONID 机制在这里失效原因是小程序的请求不一定走同一个域名下的 cookie 管理。正确做法是走一套 code 换 token 的逻辑小程序端调用wx.login()获取临时登录凭证 code小程序把 code 发送给后端/user/login接口后端拿着 code 请求微信接口服务获取 openid 和 session_key后端判断 openid 是否已存在不存在则自动注册新用户后端把 openid 作为用户标识生成一个自定义 token比如 UUID并保存 token 与用户 id 的映射关系后端把 token 返回给小程序小程序存入本地 storage后续所有业务请求小程序在请求头里带上 token后端通过拦截器解析 token 获取用户信息这里我重点说一下第 5 步。token 的存储方式有两种内存 Map 或者 Redis。对于课程设计、低并发场景直接用一个 ConcurrentHashMap 存 token 和用户的对应关系完全够用还省去装 Redis 的成本。但要注意服务器重启后 token 会全部失效用户需要重新登录这在毕业设计答辩演示时容易出意外——建议提前预留记住我的逻辑或者直接把 token 有效期设长一些。示例代码片段后端 ControllerController RequestMapping(/user) public class UserController { Autowired private UserService userService; RequestMapping(value /login, method RequestMethod.POST) ResponseBody public Result login(RequestBody MapString, String params) { String code params.get(code); if (StringUtils.isEmpty(code)) { return Result.error(code 不能为空); } // 调用微信接口服务换取 openid String openid wechatService.getOpenId(code); // 根据 openid 查询或创建用户 User user userService.loginByOpenId(openid); // 生成 token 并缓存 String token UUID.randomUUID().toString().replace(-, ); tokenCache.put(token, user.getId()); return Result.success(token); } }3.2 小程序端请求封装与登录态失效处理小程序的wx.request是低层 API直接在业务代码里到处写会很痛苦。我的做法是在 utils 目录下封装一个request.js模块统一处理 baseUrl、请求头携带 token、响应状态码校验以及登录态失效时的自动跳转。const BASE_URL https://www.example-domain.com/api; function request(url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method, data: data, header: { Content-Type: application/json, token: wx.getStorageSync(token) || }, success(res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.data.code 401) { // 登录态失效重新登录 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(res.data.msg); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data.msg); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };这里踩过的坑是后端返回码和 HTTP 状态码的语义要统一。我项目中统一用的是 HTTP 200 业务码 code0 成功、1 失败、401 未登录好处是小程序端可以集中处理异常不用每个接口单独写错误分支。3.3 跨域与域名配置问题小程序端请求后端接口涉及两个老生常谈的问题跨域和合法域名。开发阶段可以在微信开发者工具里勾选不校验合法域名这样能用本地 IP 调试但真机预览和上线发布阶段必须使用 HTTPS 域名。这里我特别提醒后端必须正确配置 CORS 跨域响应头否则小程序在某些环境下会拦截请求。SSM 项目里最简单的方法是在 web.xml 或自定义 Filter 里配置public class CorsFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse resp (HttpServletResponse) response; resp.setHeader(Access-Control-Allow-Origin, *); resp.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, OPTIONS); resp.setHeader(Access-Control-Allow-Headers, Content-Type, token); resp.setHeader(Access-Control-Allow-Credentials, true); chain.doFilter(request, response); } }注意如果用了Access-Control-Allow-Credentials: true那Access-Control-Allow-Origin不能简单写*否则浏览器会拒绝。小程序环境相对宽松但真机测试时保险起见我建议把 Origin 精确到你的小程序业务域名。4. 购物车与订单模块从加入购物车到支付完成的业务闭环4.1 购物车设计数据库存储还是本地存储购物车功能有两种方案一种是存在本地 storage另一种是存后端数据库。本地存储方案简单、不用写后端接口但存在明显缺陷——用户换了手机登录购物车数据就丢了而且购物车里的商品价格信息是前端缓存的可能和数据库里的最新价格不一致。这个项目我选择数据库存储方案购物车表保存 userId、productId、quantity、checked 字段。购物车的接口逻辑比较直接添加购物车时先查询同一用户同一商品是否已存在存在则累加数量不存在则插入新记录更新购物车时要同步修改商品勾选状态和数量删除购物车时支持单个删除和批量删除。有一点和 Web 购物车很不一样小程序商城通常没有独立的结算页选择购物车中多个商品的复杂交互一般就是勾选若干商品点去结算跳转订单确认页。所以购物车接口最好提供一个GET /cart/settleList?ids1,2,3这样的接口一次性查询勾选商品的完整信息实时价格、库存确保结算页展示的价格是最新的。4.2 订单创建事务与库存扣减的正确姿势订单创建是整个项目的核心逻辑涉及多个表的写入操作必须用Transactional保证原子性。这里我拿当时在代码里写的核心流程图式描述说明一下订单创建包含四个动作根据用户勾选的购物车记录组装订单明细数据商品快照名称、图片、单价、数量计算订单总金额生成唯一订单号扣减商品库存删除购物车中已结算的商品记录你需要特别注意的是第 2 步扣减库存的并发安全问题。如果直接用UPDATE product SET stock stock - #{quantity} WHERE id #{productId}在高并发下会出现超卖。正确的做法是加上库存条件UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}如果更新影响行数为 0说明库存不足直接在代码里抛异常回滚事务。这个写法不需要加锁性能也好是我在这个项目里最想强调的一个设计点。顺序上先扣库存、再生成订单、再删购物车。有人习惯先删购物车再扣库存结果库存不够导致事务回滚购物车又被删了用户一脸懵。这就是顺序设计不合理导致的体验问题。4.3 支付模块不接微信支付如何做成可演示的闭环很多课程设计和私活项目不会真的接入微信支付因为商户号申请门槛、资质审核、资金结算都比较麻烦。那怎么把订单闭环走通我用的是模拟支付方案。订单创建后状态为待支付前端展示一个立即支付按钮。点击支付时后端接口模拟支付成功的动作把订单状态从 0 改为 1并记录支付时间。这样整个流程在演示时非常顺畅从下单到支付到发货到收货每一步都有据可查。如果将来真要接入微信支付在小程序端的步骤是调用wx.requestPayment前先由后端调用微信统一下单 API 获取支付参数小程序端拉起支付面板。这部分代码可以后补不影响整个项目结构的完整性。4.4 订单状态机一张表理清全部流转订单状态管理是个容易写乱的地方我的经验是先画清楚状态流转再动手写代码。这里用表格列出我项目里的状态定义状态值前端展示触发动作操作角色0待付款创建订单用户1待发货支付成功用户/系统2待收货管理员发货管理员3已完成用户确认收货用户4已取消用户取消/超时关闭用户/系统写代码的时候建议在 OrderService 里定义一个状态变更方法统一校验当前状态是否允许迁移到目标状态。比如已取消的订单不能再次变成待发货已完成的订单不能重复确认收货。很多初级开发者图省事直接在 Controller 里执行UPDATE orders SET status 1 WHERE id xx这样会导致状态跳转异常管理端看到的订单状态乱七八糟。4.5 首页聚合数据的组装策略首页要展示轮播图、分类导航、热销商品、最新商品四块内容。如果小程序端分别请求 4 个接口串行加载会白屏很久。我在后端做了一个聚合接口GET /home/data一次性返回完整数据。这个接口的实现逻辑是public HomeDataVO getHomeData() { HomeDataVO vo new HomeDataVO(); vo.setBannerList(bannerMapper.selectList()); vo.setCategoryList(categoryMapper.selectList()); vo.setHotProducts(productMapper.selectBySales()); vo.setNewProducts(productMapper.selectByCreateTime()); return vo; }一次请求搞定首页全部渲染小程序端在onLoad时只调用一次接口体验提升非常明显。5. 部署上线与真实环境踩坑记录5.1 从本地到云服务器的完整部署步骤项目开发完成后部署到 Linux 服务器我用的是最经典省事的组合Nginx Tomcat MySQL。流程如下本地把 SSM 项目打 war 包mvn clean package上传 war 包到服务器 Tomcat 的 webapps 目录启动 Tomcat验证后端接口是否能正常访问安装 MySQL导入数据库脚本创建同名数据库和专用账号安装 Nginx配置 HTTPS 证书反向代理到 Tomcat 的 8080 端口小程序后台配置 request 合法域名一个容易踩的坑是Tomcat 默认监听 8080 端口而小程序要求请求域名必须是 HTTPS 且不能带端口。解决办法是 Nginx 监听 443 端口将/api路径代理到http://localhost:8080小程序请求的 baseUrl 写成https://你的域名/api这样既满足 HTTPS 又隐藏了后端端口。Nginx 关键配置片段server { listen 443 ssl; server_name www.example-domain.com; ssl_certificate /etc/nginx/cert/domain.pem; ssl_certificate_key /etc/nginx/cert/domain.key; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意proxy_pass末尾的斜杠如果写http://127.0.0.1:8080/请求https://域名/api/product/list会转发成http://127.0.0.1:8080/product/list如果不写斜杠转发后路径会变成/api/product/list导致后端 404。这个细节我调试了一个晚上才找到原因。5.2 图片上传与访问路径压缩后图片的存储策略购物商城必然涉及商品图片、轮播图、用户头像。我的做法是图片上传接口/admin/upload把文件保存到服务器指定目录比如/data/images/数据库里只存相对路径/images/xxx.jpg前端访问时通过 Nginx 将该路径映射到物理目录。Nginx 配置静态资源映射location /images/ { alias /data/images/; }这个方案的好处是将来迁移服务器时只需要同步图片目录即可数据库路径不用改动后端重启也不会丢失图片数据。上传代码里我额外做了一层日期目录管理每天一个子目录例如/images/2025/06/12/xxx.jpg。这样图片文件不会全部堆在同一个目录里也便于定期清理过期图片。5.3 实测踩过的三个坑先聊聊时区问题。后端返回 JSON 数据时日期时间字段默认是 UTC 时间小程序端解析出来比北京时间早 8 个小时。解决办法是在 SpringMVC 配置中指定时区或者在实体类对应的 Jackson 配置里设置spring.jackson.time-zoneGMT8 spring.jackson.date-formatyyyy-MM-dd HH:mm:ss第二个坑是 MySQL 连接编码。数据库连接串必须带字符集参数否则商品名称带中文时可能出现乱码jdbc:mysql://localhost:3306/shop_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai第三个坑是小程序端的临时路径 vs 本地路径。使用wx.chooseImage选择图片后返回的 file 路径是临时路径以http://tmp/开头用户关闭小程序再次打开就失效了。要实现修改头像后再次进入仍然显示新头像的效果必须把图片先上传到后端然后从数据库返回的永久路径来展示。这个逻辑看似简单但我在项目里见到不少同学直接把临时路径存数据库的写法。5.4 为什么我把 MyBatis 的 XML 文件路径单独拎出来配置用 SSM 写 MyBatis 时一个高频报错是Invalid bound statement (not found)。问题根源通常是 XML 文件没有和 Mapper 接口放在同一个包下或者没有在 applicationContext.xml 里声明 mapper 的 XML 路径。我的统一做法是bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.project.mapper/ /bean同时在 mybatis-config.xml 里配置 mapper-locationsmybatis.mapper-locationsclasspath:mapper/*.xml把 XML 文件放在 src/main/resources/mapper 目录下而不是和 Java 接口放一起这样打 war 包时不会遗漏。这个配置错误通常在本地 IDE 运行时不会暴露因为 IDE 的 target 目录里有编译后的 XML部署到 Linux 服务器后才会突然报错。所以我一贯的建议是本地测试通过后最好在打包部署到云服务器的环境里再复测一遍接口不要只在本地跑通了就觉得完了。6. 管理端功能的快速实现路径6.1 后台管理的技术选择JSP 还是纯接口购物商城除了小程序前端通常还需要一个管理后台。在 SSM 架构下后台管理有两条路用 JSP 做页面或者只提供 RESTful 接口外加一个简单的 HTML 管理页。我这次选的是接口 精简 HTML 页面的方式理由有两个一是和后端 API 共用同一套 Service 和 Mapper不用重复开发逻辑二是课程设计演示时浏览器打开管理页操作直观比用 Postman 调接口专业得多。管理端核心功能围绕商品管理、订单管理、分类管理展开。商品管理包括列表查询、新增、编辑、上下架图片上传是必做功能。订单管理的核心操作是发货点击发货后订单状态从待发货变为待收货。这些操作的实现逻辑和用户端接口高度相似无非是增加了管理员角色的鉴权。6.2 权限校验的简易方案管理员的登录鉴权不能和用户端混用。我的做法是单独建一张 admin 表字段只有 id、username、password、create_time。密码用 MD5 加盐存储登录成功后同样下发 token。不过由于管理端和小程序端是两个客户端我在拦截器中通过 URL 前缀区分/admin/**走管理员 token 校验其他接口走用户 token 校验。拦截器实现的核心思路Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requestURI request.getRequestURI(); String token request.getHeader(token); if (requestURI.startsWith(/admin/)) { // 校验管理员 token if (adminTokenCache.get(token) null) { response.setStatus(401); return false; } } else { // 校验用户 token if (userTokenCache.get(token) null) { response.setStatus(401); return false; } } return true; }这个方案谈不上多高级但对项目规模来说够用而且逻辑清晰。7. 我做完这个项目后的几点实际体会代码全部写完、部署上线、小程序真机跑通之后回头再看这个购物商城项目有几点体会想分享出来。第一项目千万别憋着一次性写完再开始联调。我见过太多人把后端所有接口写完才开始写小程序结果前端对接时发现字段命名不一致、返回值结构对不上又要回头大改。正确做法是做一个接口联调一个页面比如先把登录接口和小程序登录页打通再做商品列表积木式地推进每块都是验证过的最后整体稳定得多。第二token 失效策略一定要提前想好。演示现场最怕的就是登录状态过期被踢出登录页这种尴尬情况。我建议用户 token 有效期设置得宽松一点比如 7 天如果做了登录状态过期自动重新登录的逻辑也一定要在小程序端静默处理好不要让用户手动重新登录。第三图片资源的备份是很多人忽略的坑。如果你的项目要提交源码或者换服务器演示图片上传目录不会跟着代码走必须单独备份 /data/images 目录。我在交付项目时都会把图片目录打成压缩包放进源码说明文档里硬核一点说看图能跑、不看图全靠脑补是完全不一样的项目交付体验。最近我准备把同一套购物商城的逻辑迁移到一个 Spring Boot 3 的独立项目里核心流程不变主要是把配置简化、换成 MyBatis-Plus 的增强 mapper。如果你手头正好也在做类似的小程序商城项目或者对文中某个模块的实现细节有疑问欢迎随时来交流。这个项目的完整思路和关键代码后续我会补充到个人博客里希望能帮到正在走这条路的你。