做毕设选这类题目的同学,通常会被标题里的“批发零售”、“社区团购”、“库存订单一体化”、“SCM”这些词吓到,觉得是不是要搞一个像京东供应链一样复杂的系统。其实剥开包装看本质,这就是一个典型的中小商户进销存 + 客户订单管理项目,SpringBoot 做后端接口,微信小程序做业务员或门店老板的手机端,真正核心的只有三件事:管商品、管库存、管订单。至于“SCM”和“社区团购”,更多是选题包装,答辩老师想看的是你有没有把供应链里最关键的“进销存”逻辑跑通,而不是真的搭建一套 ERP。
这篇文章我会从需求收敛、技术选型、数据库设计、接口实现到问题排查,完整拆解这个毕设/实战项目应该怎么做。内容适合正在做 SpringBoot + 微信小程序毕设的同学,也适合想拿这个题目练手、补齐企业级开发经验的开发者,哪怕你是第一次接触小程序后端,按这个思路走完也能交出一份能演示、能答辩、逻辑完整的作品。
1. 先砍需求:为什么我建议你不要做“全套社区团购”
标题里出现了三个高频词汇:批发零售、社区团购、SCM供应链。很多同学拿到题目就开始焦虑,觉得系统里既要有团长管理、又要拼团活动,还要有供应链协同和物流跟踪,最后代码写到一半就崩了。这种痛我太懂了,因为毕设不是商业项目,核心目标是论证你具备独立完成一个完整业务系统的能力,而不是证明你能把一个创业公司的全部业务塞进一个仓库里。
1.1 用一句话定义你的系统边界
如果让我给这个项目定一个落地范围,我会这样说:面向批发零售场景,以微信小程序为操作入口,实现商品管理、多仓库存管理、客户下单、订单履约和基础数据报表的后台管理系统。注意,这里我没有提“社区团购”里的拼团、团长佣金、自提点这些东西,因为它们是典型的“大需求”,会无限放大工作量。一个人一个月做完所有模块不现实,硬做出来的质量也很难过答辩。
那“SCM”和“社区团购”是不是应该彻底丢弃?不是。你需要的是“映射”而不是“照搬”。供应商采购入库对应 SCM 里的采购协同,库存变动记录对应供应链的可追溯性,客户等级价格对应批发业务的分级销售策略,这些才是这个题目的灵魂。至于社区团购,如果你的导师明确要求必须体现,那可以做一个轻量的“自提点 + 订单汇总”模块,把当日订单按自提点维度汇总,仅此而已。
1.2 功能模块的优先级排序(按 MVP 思路)
我把功能分成 P0、P1、P2 三级,做毕设时优先保证 P0,再根据时间决定是否做 P1:
| 优先级 | 模块 | 核心功能点 | 价值说明 |
|---|---|---|---|
| P0 | 商品管理 | 商品分类、商品 SPU/SKU、上下架、批发价/零售价 | 没有商品,后面所有模块都是空的 |
| P0 | 库存管理 | 多仓库、入库、出库、库存查询、库存预警 | 批发零售项目最核心的供应链能力 |
| P0 | 订单管理 | 小程序端下单、后台订单列表、订单状态流转 | 连接客户与库存的关键链路 |
| P0 | 客户管理 | 客户档案、等级价、收货地址 | 批发业务中客户就是上帝 |
| P1 | 采购入库 | 供应商档案、采购单、审核入库 | 对应标题中的 SCM 采购协同 |
| P1 | 数据统计 | 近 7 天销售趋势、热销商品、库存周转 | 答辩展示时的可视化亮点 |
| P2 | 社区团购 | 自提点管理、按自提点汇总订单 | 如果导师不强制,可以后置 |
这样一来,你的项目从表面看是一个完整的“批发零售商品管理系统”,内里又是围绕“进销存 + 订单履约”的企业级核心链路,和标题的关键词形成了一一映射。答辩时,老师问你“怎么体现 SCM 概念”,你直接说“库存来源于采购,销售消耗库存,通过库存流水把供应商、商品、客户三个角色串在一个链条上”,这句话一出口,系统的高度就出来了。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型:为什么是 SpringBoot 而不是 SpringCloud
毕设技术栈选型有一条铁律:选你熟悉的技术,而不是选最流行的技术。但这不代表可以随便选,你得能说服老师为什么这么选。这里我给出基于该项目的技术组合,并解释每个选择背后的考量。
2.1 后端框架与版本选择的坑
SpringBoot 目前主流版本分两派:2.7.x 和 3.x。很多同学一拍脑袋就选了最新的 Spring Boot 3.2,结果连 JDK 版本都匹配不上,报错报到头秃。这里我强烈建议:毕设项目使用 Spring Boot 2.7.x + JDK 1.8 组合。
为什么?原因有三:
第一,微信小程序的后端接口开发需要大量第三方依赖,比如 MyBatis-Plus、Hutool、JWT 工具包,这些依赖在 Spring Boot 2.x 时代经历了充分的兼容性验证,你搜教程时不会遇到太多“版本过新导致配置不兼容”的问题。
第二,绝大多数学校的毕业设计指导文档和机房环境还停留在 JDK 1.8,你写代码在本机,但是答辩演示时的运行环境谁能保证?用 JDK 1.8 最稳妥。
第三,微信小程序的后端接口本质是 RESTful API,压测环境就是演示现场的几十个请求,完全不需要 Spring Cloud 那套微服务注册发现体系。用单体应用能把代码结构做得更清晰,也更容易让答辩老师看懂。
我推荐的依赖版本组合如下,可以直接抄作业:
xml复制<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/>
</parent>
xml复制<!-- 核心依赖 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.22</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.4.0</version>
</dependency>
从 2.7.18 开始 Spring Boot 已经进入维护期尾声,但从稳定性角度它依然是企业存量项目中最常见的版本。如果你选 Spring Boot 3.x,就绕不开 JDK 17 和 Jakarta EE 命名空间——这些不致命,但会耗掉你大量本该用来做业务的调试时间。
2.2 微信小程序端:原生还是 uni-app
这个问题我直接给结论:如果你没有同时上架 H5 和 App 的需求,老老实实用微信原生小程序开发。用 uni-app 可以一套代码多端复用,听起来很香,但代价是:你踩的坑会加倍。原生小程序里 wx.request 出问题,网上随手一搜就是解决方案;uniapp 编译后出现问题,你要先定位是框架问题还是业务问题,排查链路过长。
另外,毕设答辩时老师很可能现场打开小程序开发者工具看你的代码。原生小程序的 wxml、wxss、js 结构对老师来说一目了然,而你用 uniapp 写的那一堆 vue 文件,渲染层逻辑都要经过编译解释,反而增加了沟通成本。
小程序的目录结构我这样规划,你可以参考:
text复制/miniprogram
/pages
/login 登录页
/home 首页(商品浏览 + 轮播图)
/category 商品分类
/goods/detail 商品详情
/cart 购物车
/order/confirm 下单确认页
/order/list 订单列表
/order/detail 订单详情
/user 个人中心(客户查看自己信息)
/utils
request.js 封装 wx.request
auth.js Token 存储与校验
config.js 后端接口地址配置
/components
/price 价格组件(处理批发价/零售价展示)
/stock-tag 库存状态角标
app.js
app.json
app.wxss
后端代码结构我建议按模块分包,而不是按技术层面分包。很多同学喜欢建 controller/service/mapper 三个顶层包,结果几十个文件堆在一起,找东西全靠滚动,项目一大就完全失控。分包思路如下:
text复制com.example.retail
/common 通用返回体、异常处理、工具类
/config MyBatis-Plus 配置、拦截器注册、CORS 配置
/security JWT 拦截器、登录用户上下文
/modules
/auth 登录模块(小程序 code 登录)
/user 系统用户(后台管理员)
/customer 客户模块
/goods 商品模块(分类/SKU/价格)
/inventory 库存模块(仓库/库存流水/预警)
/purchase 采购模块(供应商/采购单)
/order 订单模块(购物车/下单/订单状态流转)
/report 统计模块(销售统计接口)
这种分包方式的好处是,每个业务模块内部自成体系,答辩时老师让你介绍“订单模块怎么设计的”,你直接打开 order 包说“controller 负责接收参数,service 处理业务规则,mapper 访问数据库,dto 和 vo 区分请求和响应”,逻辑非常清晰。
3. 数据库设计:库存订单一体化的核心是“衡”
在业务系统里,数据库是地基,地基建歪了后面全是雷。做批发零售商品管理,最忌讳的是想不清楚“库存”和“订单”之间到底是什么关系。先想明白一个道理:订单不是单纯记录了一次购买行为,订单还代表了库存的预占或者扣减。在这个体量的系统里,我推荐采用“下单直接扣减可用库存,取消订单回补库存”的策略,这是最直观也最好理解的。
一个完整的数据库设计至少需要这几张核心表:用户表(后台管理员)、客户表、商品 SPU 表、SKU 表、仓库表、库存表、库存流水表、订单表、订单明细表。下面我挑几张最关键的展开说。
3.1 商品表与 SKU 表:批发和零售价格分开存
批发零售项目里最易错的设计是把价格写成单个字段“price”,这完全没法支撑业务。一个商品可能同时面向门店和散客销售——同一样东西,你按箱卖给零售商是一个价,按最小单位卖给散客是另一个价。所以我的建议是设计两张表:goods(商品基本信息)和 goods_sku(规格 + 多价格)。
goods 表重点字段如下:
sql复制CREATE TABLE `goods` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`category_id` bigint(20) DEFAULT NULL COMMENT '分类ID',
`name` varchar(200) NOT NULL COMMENT '商品名称',
`unit` varchar(20) DEFAULT NULL COMMENT '基本销售单位:箱/件/斤',
`image_url` varchar(500) DEFAULT NULL COMMENT '主图',
`status` tinyint(4) DEFAULT '1' COMMENT '1上架 0下架',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`deleted` tinyint(4) DEFAULT '0' COMMENT '逻辑删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
goods_sku 表则是商品多规格和多价格的核心:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| goods_id | bigint | 关联商品 ID |
| spec_name | varchar | 规格描述,如“500g/袋” |
| barcode | varchar | 条形码,批发零售场景很实用 |
| wholesale_price | decimal(10,2) | 批发价 |
| retail_price | decimal(10,2) | 零售价 |
| member_price | decimal(10,2) | 会员/等级价 |
| stock_warn_threshold | int | 库存预警阈值 |
这个设计的核心价值是让开发者彻底告别“一个商品只有一个价格”的思维定式。批发零售的商品天然具有多渠道、多客户等级属性,SKU 层做价格矩阵,后续接“客户等级折扣”或“满减促销”时才不会被迫改表结构。
3.2 库存表四件套:仓库、SKU、可用数量、锁定数量
库存表我只提供一个简单核心的版本:inventory 表记录每个仓库下每个 SKU 的可用库存和锁定库存,stock_flow 表记录每一次库存变动。表结构如下:
sql复制CREATE TABLE `inventory` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`sku_id` bigint(20) NOT NULL,
`warehouse_id` bigint(20) NOT NULL,
`available_stock` int(11) NOT NULL DEFAULT '0' COMMENT '可用库存',
`locked_stock` int(11) NOT NULL DEFAULT '0' COMMENT '锁定库存(下单未支付)',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sku_warehouse` (`sku_id`, `warehouse_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里的 available_stock 和 locked_stock 分别对应“可以卖的数量”和“已经被订单占用但还没出库的数量”。拿水果批发举例,你进货 1000 箱苹果放在 1 号仓,小程序展示可用库存是 1000。这时候有一个客户下单了 200 箱但还没付款,如果不锁定,另一个客户又把 1000 箱全下走了,库存就超卖了。有了锁定概念后,第一单产生 200 锁定,第二单最大可下单量变成 800,逻辑才闭合。
库存流水表是供应链追溯的关键,所有库存变化必须落流水:
| 字段 | 说明 |
|---|---|
| id | 主键 |
| sku_id | 商品 SKU ID |
| warehouse_id | 仓库 ID |
| change_type | 变动类型:1 采购入库、2 销售出库、3 订单取消回补、4 盘点调整 |
| change_quantity | 变动数量(正数入、负数出) |
| before_stock | 变动前可用库存 |
| after_stock | 变动后可用库存 |
| biz_order_no | 关联业务单号(采购单号/订单号) |
| remark | 备注 |
| create_by | 操作人 |
| create_time | 操作时间 |
很多同学做进销存只维护一张库存表,改完之后根本没记录。老师问“这个库存数怎么来的”,一句话答不上来。有了库存流水,你可以沿着时间轴把一件商品从采购入库到销售出库的完整生命周期讲清楚,这是毕设答辩的加分项。
3.3 订单表:拒绝一张表挂天下
订单主表和订单明细表必须分开。主表记录订单整体状态、金额、客户信息和收货信息,明细表记录每一件商品的下单数量、单价和 SKU 信息。真实场景里,客户一次下单多种货品是最常见的业务形态,明细表不可省。
订单主表核心字段:
- order_no:订单号,建议用时间戳 + 随机数生成,比如 2025010112000012345
- customer_id:客户 ID
- total_amount:订单总额
- discount_amount:优惠金额
- pay_amount:应付金额
- status:订单状态(待付款/待发货/已发货/已完成/已取消)
- address_snapshot:收货地址快照(防止客户修改地址后影响历史订单)
- remark:客户备注
- cancel_reason:取消原因
- supplier_id:如果有多个供应商,可以关联,但作为毕设可先忽略
订单明细表核心字段:
- order_id:关联订单主表
- sku_id:关联商品 SKU
- goods_name:下单时的商品名称快照
- spec_name:规格快照
- goods_image:商品图快照
- price:实付单价
- quantity:数量
- total_price:小计金额
为什么明细表里要冗余一份 goods_name、spec_name、price?因为这些字段在商品表中是会变化的。比如苹果今天卖完了,明天又进了一批,价格从 50 涨到 55。如果订单明细不做快照,你查三个月前的订单,显示的价格就是今天的最新价格,这在统计历史销售数据时会造成严重失真。设计表时“用空间换正确性”,是业务系统里的基本原则。
4. 后端核心逻辑实现:订单状态机与库存扣减
数据库设计完毕,接下来是代码。后端核心要解决的三个问题是:微信登录鉴权、下单过程中的库存扣减、订单状态的流转一致性。这三个问题每个都是新手重灾区,我逐个拆解。
4.1 小程序登录与 JWT 鉴权:别把 openid 当密码
微信小程序登录的完整链路是:小程序端调用 wx.login 获取临时 code,把 code 发给后端,后端用 code + appid + appsecret 请求微信接口服务,换取 openid 和 session_key,然后后端视情况把 openid 作为唯一标识去数据库查/建用户记录。
后端接口示例:
java复制@PostMapping("/wx/login")
public Result<LoginVO> wxLogin(@RequestBody WxLoginDTO dto) {
// 1. 使用 code 换取 openid
WxMaJscode2SessionResult session =
WxMaService.getUserService().getSessionInfo(dto.getCode());
String openid = session.getOpenid();
// 2. 根据 openid 查客户表
Customer customer = customerService.getByOpenid(openid);
if (customer == null) {
customer = customerService.register(openid, dto.getNickname(), dto.getAvatar());
}
// 3. 生成 JWT token
String token = JWT.create()
.withClaim("customerId", customer.getId())
.withClaim("openid", openid)
.withExpiresAt(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000))
.sign(Algorithm.HMAC256("your-secret-key"));
return Result.ok(new LoginVO(token, customer));
}
Openid 是用户的唯一标识,但这不意味着你可以直接把 openid 明文保存在小程序端当作身份凭证用。小程序端不像传统 Web 端有完整的会话管理,你需要在后端签发一个 JWT token 返回给前端,前端后续请求时在 Header 里带上 Authorization: Bearer <token>,后端拦截器解析 token,从 token 里取出用户 ID,再通过 ThreadLocal 传递给业务代码读取当前登录用户。
4.2 下单扣库存:一条 SQL 教会你做乐观锁
在我刚入行的时候带我的师傅说过一句话,我至今记在心里:“做库存扣减,永远不要先查询库存数够不够,再执行 update,因为中间这 1 毫秒就可能被别人插一脚。”并发场景下直接查了再改会超卖,解决办法有两个:悲观锁(SELECT FOR UPDATE)和乐观锁(版本号 / 条件更新)。
对毕设体量而言,我推荐乐观锁。核心 SQL 如下:
sql复制-- 扣减库存,条件里带上 available_stock >= 扣减数量
UPDATE inventory
SET available_stock = available_stock - #{quantity},
locked_stock = locked_stock + #{quantity},
version = version + 1
WHERE sku_id = #{skuId}
AND warehouse_id = #{warehouseId}
AND available_stock >= #{quantity};
这条 SQL 的妙处在于:数据库的行锁机制保证同一时间的多个扣减请求会串行执行,available_stock >= #{quantity} 这个条件确保只有库存足够时才更新成功。只要 update 返回的影响行数为 1,说明扣减成功;影响行数为 0,直接抛出“库存不足”异常,并触发上层事务回滚。这一套操作就是教科书上的乐观锁条件更新实现,代码量少、正确性高、也好向老师解释。
接下来梳理一下下单的完整链路,必须放在同一个事务里:
第一步,校验商品上下架状态和购买数量合法性。
第二步,根据 skuId 批量查库存,与请求中的购买数量做比对,任一项不足则直接中断下单。这里要注意“批量查库存”与“循环 update 扣减”可能存在不一致——所以最终扣减时还要再用行锁条件更新保护。
第三步,循环扣减每个 SKU 的库存,同时生成库存流水记录。
第四步,计算订单金额。这里要注意批发和零售场景价格要分类讨论:如果有“客户等级价”,优先使用等级价。价格计算建议独立成服务,不要在 Controller 里写。
第五步,插入订单主表和订单明细表,订单状态为待付款。
第六步,如果选择“在线支付”,则创建微信支付预支付单并把支付参数返回小程序端。如果选择“货到付款/线下转账”,订单状态直接变为待发货。
这个顺序不能乱。很多人的错误是先在订单表插入记录再扣库存,一旦库存不足,订单已经生成了,你要再做状态变更或删除,事务会变得非常绕。
4.3 订单状态流转:一个字典值说不清楚业务
订单状态字段在数据库里只有一个数字,但在业务逻辑上它是一个完整的状态机。每一笔订单在某个状态只能执行特定的操作,否则整个链路会乱套。我建议把状态机直接用常量类定义好,杜绝在代码里用魔法数字:
java复制public class OrderStatus {
public static final int PENDING_PAYMENT = 10; // 待付款
public static final int PENDING_SHIPMENT = 20; // 待发货
public static final int SHIPPED = 30; // 已发货
public static final int COMPLETED = 40; // 已完成
public static final int CANCELLED = 50; // 已取消
}
不同状态允许的操作,我总结成一张速查表,写接口时对照实现就不会乱:
| 当前状态 | 允许的操作 | 操作后状态 |
|---|---|---|
| 待付款 | 取消订单(回补库存) | 已取消 |
| 待付款 | 模拟支付 / 微信支付回调 | 待发货 |
| 待发货 | 后台发货(填写物流单号) | 已发货 |
| 已发货 | 客户确认收货 | 已完成 |
| 已发货 | 售后退款(作为扩展) | 已关闭 |
这里有一个特别容易踩的坑——取消订单必须回补库存,而且必须回到可用库存。很多人取消订单只改订单状态,结果库存越卖越少,最后不得不手工盘点才能修正,这就是没有设计库存流水导致的。正确的操作是在取消订单的事务里做一次与下单相反的库存变更:available_stock 加回去、locked_stock 减掉,并插入一条 change_type 为“订单取消回补”的库存流水。
5. 前端小程序的核心实现与对接细节
有了后端接口,小程序端要做的事就是把业务场景搬进手机里。这里我挑三个核心页面讲清楚实现逻辑,分别是商品列表页、下单确认页和订单列表页,这三个页面做通了,整个小程序的骨架就立住了。
5.1 request 请求封装:别在 30 个页面里写 wx.request
如果你在每个页面里直接写 wx.request,第一个遇到的问题就是代码冗余,第二个问题就是无法统一处理 Token 失效。建议在 utils/request.js 里做一层封装。
javascript复制const request = (url, method = 'GET', data = {}) => {
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token');
wx.request({
url: `${config.baseUrl}${url}`,
method,
data,
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
success: (res) => {
// 后端统一返回格式 { code: 200, message: 'success', data: {...} }
if (res.data.code === 200) {
resolve(res.data.data);
} else if (res.data.code === 401) {
// token 过期,跳转登录页
wx.removeStorageSync('token');
wx.navigateTo({ url: '/pages/login/login' });
reject(res.data);
} else {
wx.showToast({ title: res.data.message, icon: 'none' });
reject(res.data);
}
},
fail: (err) => {
wx.showToast({ title: '网络请求失败', icon: 'none' });
reject(err);
}
});
});
};
封装的好处是业务代码里只需要写 request('/api/order/list', 'GET', { page: 1 }) 即可,看起来就像你在调本地函数,背后帮你自动完成 BaseURL 拼接、Token 注入和错误统一提示。这样写出来的业务代码干净清爽,答辩演示时临时改接口地址也只需动一个 config 文件。
5.2 商品详情与购物车联动:利用全局数据还是本地缓存
小程序不像 Web 端那样天然有全局 Store。购物车功能有两种实现方案:后端保存购物车与本地缓存保存购物车。对这个项目,我建议以本地缓存为主,后端暂不建表。因为购物车属于强实时、高频率操作的数据,本地缓存可以实现页面秒开,提交订单时再把购物车数据一次性传给后端校验扣库存。这种设计也能减轻后端压力,对毕设完全够用。
本地缓存购物车的数据结构建议这样存:
javascript复制// cartList: [{ skuId, goodsName, specName, price, quantity, stock, checked }]
wx.setStorageSync('cartList', cartList);
商品详情页点“加入购物车”时,先基于 skuId 判断是否存在相同项,存在则数量加一,否则 push 新对象。购物车角标数量直接用 cartList.length 算出来,配合 TabBar 的 setTabBarBadge 实现红点展示。
5.3 商品列表带动价格切换:批发价和零售价怎么展示
一个 SKU 既有批发价又有零售价,小程序端怎么展示呢?这里有一个实用思路——用 tab 切换或下拉选择器来实现。商品列表页默认展示零售价,点击价格旁边的“批发价”切换按钮,触发重新渲染。
价格展示组件我建议在 components/price 里用一个自定义组件统一封装:
javascript复制Component({
properties: {
wholesalePrice: Number,
retailPrice: Number,
showMode: {
type: String,
value: 'retail' // retail | wholesale
}
},
observers: {
'wholesalePrice, retailPrice, showMode': function() {
const price = this.properties.showMode === 'retail'
? this.properties.retailPrice
: this.properties.wholesalePrice;
this.setData({ displayPrice: price });
}
}
});
小程序自定义组件里的 observers 监听器非常实用,相当于 Vue 里的 watch。属性变化时自动更新展示价格,这样商品列表和详情页只要把 showMode 传进去,价格展示逻辑就统一了。要注意的是,如果服务端设计的是“按下单价显示客户等级专属价”,需要在下单接口传 currentUser 的 level 参数,由后端从 price 矩阵里找到对应价格返回,不能让前端做价格计算,否则用户可以改请求数据手动改价。
6. 常见问题与排查技巧实录
写毕设这一个月里,百分之八十的时间其实不是在写业务逻辑,而是在解决环境问题、联调问题和数据一致性问题。我把自己实操过程中遇到的高频坑做了一个汇总,按频率排序如下,每一个都是真实经历。
6.1 SpringBoot 版本太高导致的连环坑
我的习惯是装最新版依赖,结果 Spring Boot 3.2 刚上,MyBatis-Plus 和 springdoc 全都出现兼容性报错,勾起了我一整周的阴影。Spring Boot 3.x 是建立在 Jakarta EE 9+ 之上的,很多老的第三方库还在用 javax.* 命名空间,直接启动报 ClassNotFoundException: javax.servlet.Filter。如果你也遇到这种问题,最快的解决方式是不要挣扎,直接把手头项目的 Spring Boot 版本降回 2.7.x,并把代码里的 javax.servlet 全部改成 jakarta.servlet(Spring Boot 3 项目里才会这样做,2.x 不需要)。我还试过把 MyBatis-Plus 换成最新版去适配 Boot 3,看起来是能跑了,但是代码生成器和 BaseMapper 里的一些方法行为和预期不一样,反而是给自己造坑。对毕设来说,项目的稳定性和可演示性永远大于版本的新鲜度。
6.2 微信小程序真机预览请求失败:localhost 只能存在于模拟器
小程序开发工具里接口能通,一到真机预览就白屏或超时,这个问题是必踩的。原因在微信公众平台小程序后台有域名校验机制:开发模式下可以在开发者工具详情里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”,但真机预览依然有诸多限制。
解决方案有三步:第一步,把后端的接口地址从 localhost 改成本机局域网 IP,比如 http://192.168.1.101:8080,手机和电脑连同一个 Wi-Fi,真机预览即可访问。第二步,最终要演示或者上线体验版,就必须把这个地址换成一个已备案并配置 HTTPS 的域名,后端用 Nginx 反向代理到 8080 端口,同时在微信公众平台后台把域名加入 request 合法域名列表。第三步,要开发环境快速联调,不要反复打包上传,可以用“真机调试”功能配合“不校验域名”开关。
这里还要注意一个常见错误,就是后端接口虽然通了,但请求头跨域导致前端读取不到响应。小程序端由微信客户端发起请求,没有浏览器同源策略限制,但开发者工具里却有跨域校验,所以后端仍然建议配置 CORS:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
6.3 微信小程序调试器一直卡在 paused in debugger
这是一个看起来吓人但实际上很简单的坑。小程序开发者工具默认开了 Sources 调试面板,真机调试或模拟器跑起来后,如果代码里有 debugger 语句或在 Sources 面板里打了断点,就会暂停在某个位置。有些时候你没主动打断点,它也停在 app.js 第一行——这通常是因为开了“自动附加”调试模式。
解决办法:关闭调试面板,或者按 F8(Resume Script Execution)继续执行。如果你发现每次启动都会卡在同一个位置,检查是不是在 app.js 或者某个公共模块里写了 debugger。我在毕设期间为了调试一个问题在 request.js 里留了一行 debugger,后来忘记删,每次跑都像死机一样卡住,不是程序有 bug,是调试语句没有清理干净。
6.4 库存为负数:检查是不是漏了回补操作
系统跑着跑着,某件商品的库存变成了 -5,这种问题通常有三种成因。第一种是下单接口没有加事务控制,扣减库存的 SQL 成功了,但后续插入订单明细时抛异常,库存已经扣了但订单没生成,下一次查询就发现对不上账。第二种是订单取消接口没有回补库存,用户下单锁了 10 件库存,支付超时被系统取消,但锁定量没有释放。第三种是采购入库时 UPDATE 语句没有带 @Transactional 注解,入库单审核通过后,库存表更新了,但库存流水没有同时写入。
先说明排查思路:拿库存为负的 sku_id 去 stock_flow 表按时间倒序查流水,看最后一次变动记录的是哪个业务单号,是订单号还是采购单号,再到对应单据的状态去查,基本一眼就能定位问题。修复方式是给库存变更相关的方法都加上 @Transactional(rollbackFor = Exception.class),并且统一封装一个 InventoryChangeService,所有业务模块只允许通过这个服务操作库存,不允许单独写 UPDATE 语句绕过流水,这样从制度上保证“有变动必有流水,有流水必能追溯”。
6.5 小程序抓包与排查:chrome 的开发者工具是救星
小程序端的接口问题,很多人不知道怎么排查。如果在真机上调用接口报错,可以在开发者工具里打开“调试器”的 Network 面板,查看请求和响应详情。但有一个问题,开发者工具默认的 simulate 环境与真机有差异,此时我最常用的方法是打开 Chrome 的无痕模式,手动输入后端的 Swagger 页面或接口地址,先把后端接口单独调通,再回小程序端联调,缩小问题范围。如果你用 SpringDoc 或 Knife4j 集成了接口文档,这个调试流程会非常高效,强烈建议后端集成 Knife4j,页面好看还能直接在网页上测试接口。
7. 写在最后:根据我的经验给你三条建议
整个系统写完之后,复盘下来有三个建议想送给正在做类似毕设的同学,都是我自己踩过坑之后觉得最有用的话。
第一,尽早让一份数据贯穿整个系统。不要为了演示而伪造数据,而是从采购入库开始,到客户下单,再到发货完成,用同一批商品完整跑通一条链路。运行过程中库存数据始终能对得上账,订单状态流转线清晰可见,这种“能讲故事”的系统比单纯功能齐全但数据经不起推敲的系统强太多。
第二,项目演示前专门走一遍异常场景。比如库存不足时下单、重复提交订单、取消订单,让老师看到你的系统不会崩、不会出现负库存,这会给答辩加分不少。很多同学只准备幸福路径,一被问到边界情况就卡壳,其实这些场景在你的系统实现里可能只差几行代码的判断。
第三,线上运行如果有条件,就别在本地演示。用一台云服务器部署后端,小程序连正式域名,整个流程会顺畅很多。本地演示容易翻车的环节包括:笔记本网络不稳定、电脑休眠导致服务断掉、微信开发者工具和手机之间网络隔离。部署到云端后,这些问题基本不用再操心,答辩时你只需要打开小程序给老师展示。
这个项目叫“批发零售商品管理系统”也好,叫“SCM 轻量运营系统”也罢,框架搭起来的时候它是一个技术项目,真正跑通业务闭环之后,它就是你对一个行业业务逻辑理解能力的证明。希望你写代码顺利,答辩一次过关。
