简介:这套在线收银系统源码面向Java毕业设计、课程设计学习者以及需要小型零售业务案例的开发者,基于SSM框架、MyBatis与MySQL实现,可帮助读者理解后台权限、商品库存到销售订单的完整业务链路。系统涵盖系统用户管理、员工管理、用户管理、商品类别管理、商品管理、入库管理、销售管理和销售统计等模块,适合在超市、便利店、专卖店等场景中做功能拆解与项目复现。压缩包为zip格式,约23.97MB,当前文件总数显示为0且类型明细暂未提供;已有78人学习,可结合说明文档梳理架构设计、数据库设计、模块划分、接口与部署流程,并借源码练习SSM分层、MyBatis数据访问和Tomcat部署,积累排错思路与答辩素材。
1. 一份 Java 毕业设计 SSM 在线收银系统源码,真正的门槛不在代码
很多人搜「java ssm 毕业设计 课程设计 源码」,下到一份在线收银系统源码,双击解压、找 main 方法,结果发现是个 war 工程,压根没有启动类,第一步就卡住了。这类 SSM 项目(Spring + SpringMVC + MyBatis + MySQL)配套说明文档和论文,功能通常覆盖收银台扫码结算、商品与库存、会员储值积分、订单退换、日结报表和后台权限,业务闭环是完整的,难点在于你得先弄清它怎么被 Tomcat 拉起来、事务挂在哪一层、库存怎么防超卖。适合两类人:赶课程设计要一个能跑能改的底座,以及想拿真实 CRUD 业务练 SSM 分层和事务控制的新手。下面按「看懂结构 → 跑起来 → 改得动」的顺序拆。
2. 技术选型与分层:SSM 三件套在收银场景里各管哪一段
2.1 为什么这类项目还在用 Spring + SpringMVC + MyBatis
SSM 在收银这类「表单多、查询多、事务边界清楚」的业务里其实很趁手,没必要上微服务。Spring 管 Bean 生命周期和声明式事务,SpringMVC 管 URL 到方法的映射和参数绑定,MyBatis 管 SQL——收银台的流水查询、报表统计这类需要手写 SQL 的场景,MyBatis 比全自动 ORM 更可控,报表 SQL 想怎么优化就怎么优化。
这类工程的依赖版本通常偏保守,pom.xml 里大致是这么一套:
| 依赖 | 常见版本 | 在项目里的作用 |
|---|---|---|
| spring-context / spring-webmvc | 4.3.x | IoC 容器、DispatcherServlet |
| spring-jdbc / spring-tx | 4.3.x | 数据源、@Transactional事务 |
| mybatis + mybatis-spring | 3.4.x / 1.3.x | SQL 映射、Mapper 扫描注入 |
| mysql-connector-java | 5.1.x | JDBC 驱动,注意和 MySQL 版本匹配 |
| druid | 1.1.x | 连接池,自带监控页 |
| jstl / servlet-api | 1.2 / 3.1 | JSP 渲染,provided 范围 |
| fastjson 或 jackson | 1.2.x / 2.9.x | Controller 返回 JSON |
提示:连接池和 JSON 库这类「锦上添花」的依赖,不同人手里的源码可能换过,以你那份 pom.xml 为准,不要照抄网上的版本号硬改,版本冲突是启动失败的头号原因。
没有 Spring Boot 的自动装配,所有东西都得在 XML 里显式声明,这正是新手最容易懵的地方——但反过来看,配置全摊在明面上,对理解「一个请求到底经过了哪些组件」反而是好事。
2.2 四层职责边界:Controller 不写业务,Service 不碰 request
我见过不少课程设计源码把库存扣减直接写在 Controller 里,表面能跑,一加事务就露馅。健康的分层应该是这样的:
- Controller:只做参数接收、校验、调用 Service、封装返回。不出现
Connection、不出现业务判断。 - Service:业务规则与事务边界。一个结算动作涉及订单、明细、库存、会员四张表,必须整体成功或整体失败。
- Mapper:只写 SQL,不写 if-else 业务分支。
- JSP / 静态页:只做展示,金额格式化、按钮权限这类逻辑尽量后移。
Controller 的典型写法长这样,注意它有多「薄」:
@Controller @RequestMapping("/cashier") public class CashierController { @Autowired private SaleOrderService saleOrderService; /** * 结算接口:前端收银台点击「收款」后调用 * 返回统一 JSON:{code:0, msg:"ok", data:{orderNo:"..."}} */ @RequestMapping(value = "/checkout", method = RequestMethod.POST) @ResponseBody public Map<String, Object> checkout(@RequestBody CheckoutDTO dto, HttpSession session) { Map<String, Object> result = new HashMap<String, Object>(); try { // 收银员身份从 session 取,不信任前端传的 operatorId SysUser user = (SysUser) session.getAttribute("loginUser"); dto.setOperatorId(user.getId()); SaleOrder order = saleOrderService.checkout(dto); result.put("code", 0); result.put("data", order); } catch (BizException e) { // 业务异常:库存不足、余额不够,属于可预期分支 result.put("code", 1); result.put("msg", e.getMessage()); } return result; } }这段代码的关键点有三个:@RequestBody说明前端提交的是 JSON 而不是表单,HttpSession里取操作员是防止越权,BizException是自定义业务异常,它和系统异常要分开处理——业务异常提示给收银员看,系统异常记日志、给用户一句「系统繁忙」。
Service 层接口先行,实现类上挂事务注解,这是 SSM 项目的标准套路:
public interface SaleOrderService { /** 结算:生成订单、扣库存、扣储值、加积分,全流程一个事务 */ SaleOrder checkout(CheckoutDTO dto); /** 整单退货:反向回滚库存与会员余额 */ void refund(String orderNo, String reason, Long operatorId); }接口的意义不只是「规范」,Spring 默认的 JDK 动态代理要求有接口才能织入事务;如果你改用 CGLIB,可以不要接口,但这类老项目一般还是保留接口的。
2.3 金额、库存、事务:收银系统里最不能省的三件事
这三件事任何一件做错,答辩现场演示就会翻车。
第一,金额一律用BigDecimal。用double算0.1 + 0.2得到0.30000000000000004,小票上就会出现「应收 0.30,实收 0.30000000000000004」这种玄学。所有金额字段数据库用decimal(10,2),Java 用BigDecimal,并且统一在 Service 里做setScale(2, RoundingMode.HALF_UP)。
第二,库存扣减不能「先查再改」。两个收银台同时卖最后一件商品,都查到stock = 1,都判断「够」,然后都执行stock = 1 - 1,结果卖了两件、库存还剩 0。正确姿势是一条带条件的 UPDATE,靠数据库行锁保证原子性,看影响行数判断是否成功:
<!-- 影响行数为 0 说明库存不足,Service 层据此抛异常回滚 --> <update id="reduceStock"> update goods set stock = stock - #{qty}, update_time = now() where id = #{goodsId} and stock >= #{qty} </update>第三,事务注解必须落在 public 方法上。@Transactional靠代理实现,同类内部方法互调、private 方法、Controller 上的注解都不会生效。还有一个隐蔽的坑:表引擎如果是 MyISAM,事务压根不起作用,建表时必须显式写ENGINE=InnoDB。
3. 目录结构与数据模型:解压后先看这五个位置
3.1 工程目录一览
SSM 的 war 工程目录结构基本是固定的,照着下面这张表找文件,比漫无目的翻要快得多:
| 路径 | 内容 | 你需要关注什么 |
|---|---|---|
src/main/java/com/xxx/controller | 控制器 | 有哪些 URL,前端怎么调 |
src/main/java/com/xxx/service | 业务接口与实现 | 事务边界在哪,业务规则是什么 |
src/main/java/com/xxx/mapper | DAO 接口 | 方法名对应 XML 里的 SQL id |
src/main/resources/mapper/*.xml | MyBatis 映射文件 | 复杂 SQL 都在这,报表逻辑重点看 |
src/main/resources/*.properties | 数据库、日志配置 | 跑起来前必改的两处 |
src/main/webapp/WEB-INF/web.xml | 部署描述符 | DispatcherServlet 的映射路径 |
src/main/webapp/WEB-INF/views/*.jsp | 页面 | 前后端字段名要对得上 |
db/*.sql或sql/xxx.sql | 建库脚本 | 导入前先看字符集和引擎 |
配套的说明文档一般会写清楚「环境要求 + 部署步骤 + 功能截图」,论文部分则是需求分析、ER 图、时序图和测试用例。别小看这两份文档:ER 图能帮你十分钟理清表关系,测试用例可以当冒烟清单用,改完代码照着点一遍就知道有没有改坏。
3.2 订单主表与明细表:为什么必须拆开
收银系统最核心的两张表是订单主表和订单明细表。有人图省事,把商品信息直接塞进订单表的一个varchar字段里,退货、统计、按商品查销量全部做不了。
CREATE TABLE `sale_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务单号,前端展示用', `member_id` bigint(20) DEFAULT NULL COMMENT '会员ID,散客为NULL', `total_amount` decimal(10,2) NOT NULL COMMENT '应收金额', `discount_amount` decimal(10,2) DEFAULT '0.00' COMMENT '优惠金额', `pay_amount` decimal(10,2) NOT NULL COMMENT '实收金额', `pay_type` tinyint(4) NOT NULL COMMENT '1现金 2微信 3支付宝 4会员储值', `operator_id` bigint(20) NOT NULL COMMENT '收银员', `status` tinyint(4) DEFAULT '1' COMMENT '1正常 2已退货', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售订单主表'; CREATE TABLE `sale_order_item` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `order_id` bigint(20) NOT NULL, `goods_id` bigint(20) NOT NULL, `goods_name` varchar(100) NOT NULL COMMENT '下单时快照,商品改名不影响历史单', `sale_price` decimal(10,2) NOT NULL COMMENT '成交单价', `qty` int(11) NOT NULL, `subtotal` decimal(10,2) NOT NULL, PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售订单明细表';两个设计点值得说:goods_name和sale_price是快照字段,商品后天改名或调价,历史订单必须保持原样,这是收银系统的硬要求;order_no和主键id分开,order_no是给人看的业务单号(形如20240612153012+ 收银员编号 + 三位随机),主键是给数据库用的,退货、查单都走order_no,加唯一索引防止重复提交生成两张单。
会员表通常还会有balance(储值余额)、points(积分)、level_id(等级),等级决定折扣率。库存流水表stock_record记录每一次增减,退货和盘点对账全靠它。
3.3 三处必改的配置:jdbc.properties、spring-mybatis.xml、web.xml
拿到源码后,改动量最小、但决定能不能跑起来的就这几个文件。
jdbc.properties改数据库连接:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/cashier_db?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=你的密码spring-mybatis.xml里确认三件事:数据源引用的属性名和上面一致、Mapper 扫描包路径和你的包名一致、事务管理器指向同一个数据源。
<!-- Mapper 接口扫描:路径写错会导致所有 Mapper 注入失败 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.xxx.mapper"/> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> </bean> <!-- 注解式事务:不配这个,@Transactional 就是一句注释 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>web.xml重点看DispatcherServlet的<url-pattern>。如果是/,静态资源(js、css、图片)会被它拦截,需要在 spring-mvc.xml 里加<mvc:resources>或<mvc:default-servlet-handler/>;如果是*.do这种后缀匹配,那所有请求 URL 都得带.do,前端 Ajax 地址要跟着改。这个细节决定了你改前端时会不会一路 404。
4. 核心链路拆解:一次结算从扫码到出小票的完整链路
4.1 收银台前端:扫码枪输入与购物车结构
扫码枪在浏览器眼里就是一个「打字特别快、末尾带回车」的键盘,所以不需要什么特殊驱动,一个隐藏的 input 监听回车就够了。收银台页面一般不用框架,jQuery 直接干:
// 扫码枪输入:监听回车,按条码查商品并加入购物车 $('#scanInput').on('keydown', function (e) { if (e.keyCode !== 13) return; // 只处理回车 var barcode = $.trim($(this).val()); if (!barcode) return; $.get(ctx + '/goods/queryByBarcode', {barcode: barcode}, function (res) { if (res.code !== 0) { alert('未找到商品:' + barcode); return; } addToCart(res.data); // 同一商品累加数量,不新增行 $(this).val(''); // 清空输入框,等下一枪 }); }); // 购物车用数组维护,页面渲染和提交都基于它 var cart = []; function addToCart(goods) { for (var i = 0; i < cart.length; i++) { if (cart[i].goodsId === goods.id) { cart[i].qty += 1; cart[i].subtotal = (cart[i].salePrice * cart[i].qty).toFixed(2); renderCart(); return; } } cart.push({goodsId: goods.id, goodsName: goods.goodsName, salePrice: goods.salePrice, qty: 1, subtotal: goods.salePrice}); renderCart(); }这里有两个工程习惯值得学:同一商品重复扫码是累加数量而不是追加行,收银员扫十瓶水不会得到十行;提交结算时只传goodsId和qty,不传价格,价格以服务端数据库为准,前端算出来的合计只用于显示和找零,绝不能当成账。
4.2 Service 结算事务:扣库存、扣储值、发积分
整个项目最值钱的一段代码在这里。一次结算要在同一个事务里完成:校验商品状态、按数据库价格重算金额、生成订单主表、批量插入明细、逐条扣库存、扣会员储值、加积分、记库存流水。
@Service public class SaleOrderServiceImpl implements SaleOrderService { @Autowired private GoodsMapper goodsMapper; @Autowired private SaleOrderMapper saleOrderMapper; @Autowired private MemberMapper memberMapper; @Override @Transactional(rollbackFor = Exception.class) // 所有异常都回滚,别只写 Exception 的子类 public SaleOrder checkout(CheckoutDTO dto) { if (dto.getItems() == null || dto.getItems().isEmpty()) { throw new BizException("购物车为空"); } // 1. 以数据库价格重算应收,不信任前端 BigDecimal total = BigDecimal.ZERO; List<SaleOrderItem> items = new ArrayList<SaleOrderItem>(); for (CartItem ci : dto.getItems()) { Goods goods = goodsMapper.selectByPrimaryKey(ci.getGoodsId()); if (goods == null || goods.getStatus() == 0) { throw new BizException("商品已下架:" + ci.getGoodsId()); } BigDecimal sub = goods.getSalePrice() .multiply(new BigDecimal(ci.getQty())) .setScale(2, RoundingMode.HALF_UP); total = total.add(sub); SaleOrderItem item = new SaleOrderItem(); item.setGoodsId(goods.getId()); item.setGoodsName(goods.getGoodsName()); item.setSalePrice(goods.getSalePrice()); item.setQty(ci.getQty()); item.setSubtotal(sub); items.add(item); } // 2. 逐条扣库存,影响行数为 0 直接抛异常,整个事务回滚 for (SaleOrderItem item : items) { int rows = goodsMapper.reduceStock(item.getGoodsId(), item.getQty()); if (rows == 0) { throw new BizException("库存不足:" + item.getGoodsName()); } } // 3. 写主表,拿到自增主键后再回填到明细 SaleOrder order = new SaleOrder(); order.setOrderNo(generateOrderNo(dto.getOperatorId())); order.setMemberId(dto.getMemberId()); order.setTotalAmount(total); order.setPayAmount(total); // 简化版:会员折扣在这里减 order.setPayType(dto.getPayType()); order.setOperatorId(dto.getOperatorId()); order.setStatus(1); saleOrderMapper.insertSelective(order); for (SaleOrderItem item : items) { item.setOrderId(order.getId()); } saleOrderMapper.batchInsertItems(items); // 一条 SQL 插完,别循环单条插 // 4. 会员储值支付:条件更新保证余额不会被扣成负数 if (dto.getPayType() == 4) { if (dto.getMemberId() == null) throw new BizException("储值支付必须选择会员"); int rows = memberMapper.deductBalance(dto.getMemberId(), total); if (rows == 0) throw new BizException("会员余额不足"); } // 5. 积分:每消费 1 元记 1 分,散客不记 if (dto.getMemberId() != null) { memberMapper.addPoints(dto.getMemberId(), total.intValue()); } return order; } }逐段说明:第 1 段把所有金额计算收口到服务端,并顺手做了商品下架校验;第 2 段是并发安全的核心,reduceStock返回 0 就意味着条件没满足,抛异常触发回滚,前面已经扣过的库存全部还原;第 3 段用insertSelective拿到自增主键,才能给明细表设置order_id;第 4、5 段把会员相关的扣款和积分也纳入同一事务,避免出现「钱扣了单没生成」。
参数上还有两处容易忽略:rollbackFor = Exception.class是因为 Spring 默认只对RuntimeException回滚,而 MyBatis 包装出来的可能是受检异常;setScale(2, RoundingMode.HALF_UP)必须显式写,否则BigDecimal除不尽时直接抛ArithmeticException。
4.3 Mapper 层:批量插入与带条件的库存更新
明细插入不要放在循环里一条条执行,几十条明细就是几十次数据库往返,慢得肉眼可见。MyBatis 的foreach一条 SQL 搞定:
<insert id="batchInsertItems" parameterType="java.util.List"> insert into sale_order_item (order_id, goods_id, goods_name, sale_price, qty, subtotal) values <foreach collection="list" item="it" separator=","> (#{it.orderId}, #{it.goodsId}, #{it.goodsName}, #{it.salePrice}, #{it.qty}, #{it.subtotal}) </foreach> </insert> <!-- 会员余额扣减,同样用条件更新兜底 --> <update id="deductBalance"> update member set balance = balance - #{amount}, update_time = now() where id = #{memberId} and balance >= #{amount} </update>separator=","保证拼接出来是合法 SQL;collection="list"对应接口上的List参数,如果接口用了@Param("items"),这里就得改成items,两边对不上会直接抛BindingException。>=是 XML 转义写法,SQL 里的>=在 XML 中必须转义,这是新手最常踩的一个坑。
还有一点:批量插入后用useGeneratedKeys回填自增主键,在 MySQL 的foreach批量语句里行为并不一致,有的驱动版本能回填、有的只回填最后一条。稳妥做法是先插主表拿主键,再给明细手动赋值orderId,不要指望批量插入帮你回填。
4.4 启动与冒烟验证:结出第一笔单
部署就四步:建库导 SQL、改配置、打 war 包、丢进 Tomcat。命令层面是这样:
# 1. 建库并导入(脚本里通常已经包含 CREATE DATABASE,注意字符集) mysql -uroot -p --default-character-set=utf8mb4 < db/cashier_db.sql # 2. 打包,跳过测试加快速度 mvn clean package -DskipTests # 3. 把 war 拷到 Tomcat,启动 cp target/*.war $TOMCAT_HOME/webapps/ $TOMCAT_HOME/bin/startup.sh启动后先别急着点收银台,按这个顺序冒烟:登录后台 → 商品列表有数据 → 新建一个会员并充值 100 元 → 收银台扫码加两件商品 → 选会员储值支付 → 看订单列表是否出现新单、库存是否减了 2、会员余额是否变成差额、积分是否增加。这五步走通,说明事务、Mapper、页面字段映射全都是对的,剩下的功能基本不会有问题。
提示:Tomcat 启动日志里搜
Context [] startup failed和Caused by,这两个关键词能定位 90% 的启动失败;运行期错误去logs/下按时间戳找,别盯控制台刷屏。
5. 常见问题排查:五个把我卡了半天的坑
5.1 环境与编码类:中文乱码、页面 404
现象一:导入 SQL 后,商品名称全是问号,页面显示???。原因通常是三处字符集不统一:数据库建库没指定字符集、表是latin1、JDBC URL 里没写characterEncoding。只改一处没用,得三层一起对齐。 解决:建库语句写DEFAULT CHARSET=utf8mb4,JDBC URL 加useUnicode=true&characterEncoding=utf8,web.xml 里保留CharacterEncodingFilter并设forceEncoding=true。导库时命令行也要带--default-character-set=utf8mb4,否则数据在导入那一刻就已经坏了,后面怎么改配置都救不回来。
现象二:项目能启动,访问首页 404,控制台没有任何报错。原因一般有两个:war 包名和访问路径对不上(cashier.war部署后上下文是/cashier,你直接访问localhost:8080/当然 404);或者DispatcherServlet的url-pattern是/,但spring-mvc.xml没配静态资源放行,所有 js/css 请求都被当成 Controller 去找,报 404。 解决:先确认访问地址带上下文路径;再看web.xml的映射方式,是/就在 spring-mvc.xml 补<mvc:resources mapping="/static/**" location="/static/"/>。
5.2 数据一致性类:库存超卖、事务不回滚
现象三:两个人同时结算最后一件库存为 1 的商品,两单都成功了,库存变成 -1。原因就是 2.3 节说的「先查后改」,select和update之间有时间窗,两个线程都通过了「库存够」的判断。 解决:改成where stock >= #{qty}的条件更新,Service 里判断影响行数,为 0 抛BizException回滚。这个改法不需要加锁、不需要分布式,单库场景足够用。改完可以用两个浏览器窗口同时点结算验证,能稳定复现「一单成功一单提示库存不足」才算过。
现象四:库存扣了、订单没生成,或者抛了异常但数据没回滚。最常见的原因是表引擎是 MyISAM,事务对它完全无效;其次是@Transactional加错位置——加在 Controller 上、加在 private 方法上、或者同类内部this.xxx()调用绕过了代理;还有一种隐蔽情况:spring-mybatis.xml里component-scan只扫了 controller 包,Service 压根没被 Spring 管理,注解自然不生效。 解决:show table status确认引擎是 InnoDB;注解一律加在 Service 实现类的 public 方法上;扫描包路径覆盖 service 包。验证方式是故意在结算最后一步抛异常,看前面的库存扣减有没有回滚。
5.3 SQL 与映射类:XML 转义与查询结果异常
现象五:Mapper XML 里写了stock >= #{qty},启动就报SAXParseException,说标签没闭合。XML 解析器把>当标签开始符了。>本身不强制转义,但和前面的<混在一起极易出错,统一用>=、<=最省心,或者整段包进<![CDATA[ ... ]]>。改完记得重新编译,IDEA 有时候不会自动把 resources 下的 XML 复制到 target 目录,跑起来还是旧 SQL,排查半天怀疑人生。
另外一类是查询报Expected one result (or null) to be returned by selectOne(), but found: 2。订单列表这种一对多关联,如果用resultType直接映射,主表字段会重复出现多行;正确做法是用resultMap+<collection>做嵌套映射,并且resultMap的id必须配在唯一主键上,否则 MyBatis 合并对象时会出错。
6. 二次开发:三个改动量小、答辩加分明显的方向
拿到这套源码,别满足于「能跑」,挑一两个方向改出差异,才叫真的掌握。
方向一:加一张「挂单/取单」表。收银现场经常出现顾客临时去拿东西、这一单先挂着的情况。改动很小:新建sale_order_hold表存购物车 JSON,收银台加「挂单」「取单」两个按钮,Service 里做序列化和反序列化。这个功能业务价值直观,演示效果好。
方向二:把日结报表改成按小时统计。现有报表多半是按天group by date(create_time),改成group by date_format(create_time,'%Y-%m-%d %H')就能看到一天内的销售高峰时段,配合 ECharts 画个折线图,页面立刻上一个档次。SQL 改动量就一行,但能体现你会用数据库函数做聚合。
方向三:给收银接口加幂等。收银员手抖点两次「收款」,会生成两张单。常见做法是把前端生成的requestId(UUID)带到后端,在订单表加唯一索引uk_request_id,重复提交时捕获DuplicateKeyException直接返回已存在的订单。这段代码不到二十行,但它是真实生产环境里的必备设计,写进论文的「系统优化」章节很合适。
| 改造点 | 涉及文件 | 工作量 | 演示价值 |
|---|---|---|---|
| 挂单/取单 | 新增表 + CashierController + 收银台 JSP | 半天 | 高 |
| 小时级报表 | SaleOrderMapper.xml + 报表页 | 两小时 | 中 |
| 接口幂等 | 订单表加索引 + Service 捕获异常 | 一小时 | 高 |
改完之后一定要回归:库存、订单、会员余额三者对得上,退货能把库存加回去。我一般改完任何涉及金额的代码,都会手动走一遍「下单 → 部分退货 → 再查余额」这条链路,不跑一遍心里不踏实。这套源码的配套文档和论文本身就是很好的需求清单,照着里面的功能点逐条验证,比盲目点页面靠谱得多。
从那以后我每次拿到这类 SSM 课程设计源码,都强制先做三件事:确认表引擎是 InnoDB、确认事务注解在 Service 上、确认金额字段是 decimal,这三步做完再谈功能。希望帮到你。
本文还有配套的精品资源,点击获取