简介:这份在线收银系统源码包面向Java Web初学者、计算机专业毕业设计与课程设计人群,基于SSM(Spring+SpringMVC+MyBatis)与MySQL构建,围绕小型零售场景提供从商品入库到销售统计的完整业务闭环。系统覆盖系统用户、员工、普通用户、商品类别、商品、入库、销售及销售统计等管理模块,可帮助读者理解权限划分、商品档案维护、库存流转与订单数据统计等典型业务,并借助配套说明文档梳理数据库设计与环境搭建思路。压缩包约23.97MB,文件明细与类型分布暂未提供;已有78人学习,适合用作毕业设计选题参考、课程设计原型或SSM入门练手项目。源码结构便于对照学习控制层、业务层与持久层的协作方式,读者可据此拆解模块实现、复用基础增删改查与统计逻辑,并在此基础上做二次开发或答辩演示。
1. 从收银台的三分钟说起:这套 SSM 在线收银系统要解决什么
收银台前站着两位顾客,收银员却要切到 Excel 里翻库存、拿计算器核对金额、再手写一张小票,这一单三分钟还没结束。把这段流程搬进浏览器,用 java + ssm 写一套在线收银系统,是毕业设计和课程设计中很常见、也容易讲清楚设计思路的题目:登录鉴权、商品检索、购物车、结算扣库存、小票与日报,每个环节都有明确的输入输出,答辩时不容易被问倒。这份源码给的是 Spring + SpringMVC + MyBatis 加 MySQL 的完整实现,附带说明文档与论文(LW)部分,适合要交毕设的学生照着结构梳理自己的方案,也适合需要一套可运行样板改造成门店收银的开发者。它解决的问题很朴素:让一台电脑、一个浏览器,把「查货—算钱—扣库存—留凭证」这条链路跑完整。
2. 需求与技术选型:为什么 SSM 三件套仍是收银系统的稳妥解
先把需求说透,再谈技术。很多同学一上来就堆框架,结果做到一半发现「权限」没地方挂、「库存」对不上账,回头重构的成本比重新写一遍还高。收银系统的需求其实非常收敛:它是围绕「一笔交易」展开的,所有功能的最终目的都是让这笔交易可记录、可核对、可追溯。把这层想明白,技术选型就不太会跑偏。
2.1 三类使用者的真实需求拆解
把使用者拆开看,需求会具体很多。第一类是收银员,他的核心诉求只有一个字:快。扫码枪扫一下条码,商品立刻出现在购物车里,回车即结账,整个过程不能有多余的点击。所以商品检索必须支持条码精确匹配,也必须支持名称模糊查询作为兜底——条码贴纸磨损扫不出来是常态。
第二类是店长或管理员,关心的是「账对不对得上」。他需要维护商品与分类、查看库存、按日期查销售流水、导出日报,还需要管理收银员账号,避免出现谁都能改价格的局面。第三类是部署系统的开发者本人,关心的是配置项在哪、数据库脚本怎么导、页面路径怎么配。
| 角色 | 核心功能 | 验收时的观察点 |
|---|---|---|
| 收银员 | 登录、条码检索、购物车增删、结账、打印小票 | 从扫码到出小票能否在 30 秒内完成 |
| 店长/管理员 | 商品与分类维护、库存查看、销售日报、账号管理 | 日报金额与订单明细求和是否一致 |
| 部署者 | 数据库脚本、配置文件、启动说明 | 换一台机器能否按文档跑起来 |
这份源码的模块划分基本对应上表:商品与分类管理、订单与订单明细、库存扣减与流水、用户与角色、日报统计。功能不花哨,但闭环是完整的,这也是它能作为毕设方案底稿的原因。
2.2 SSM 与 MySQL 的选型理由
答辩现场被问得最多的一句是「为什么不用 SpringBoot」。这个问题不需要回避,答案也不复杂:SSM 把 Spring 的容器管理、SpringMVC 的请求分发、MyBatis 的 SQL 映射三件事拆开摆在你面前,配置文件虽然多,但每一行都能对应到「它解决了什么问题」。
- Spring 负责 Bean 的创建与依赖注入,Service 层通过
@Autowired拿到 Mapper,不用自己 new,事务也能通过@Transactional声明式地挂上去。 - SpringMVC 的
DispatcherServlet分发链路清晰,处理器映射、视图解析器、拦截器各司其职,讲清楚一次请求怎么落到 Controller、怎么回到 JSP,是答辩的加分项。 - MyBatis 把 SQL 写在 XML 里,收银系统里那些「带条件的库存扣减」「按日聚合的日报」用原生 SQL 表达最直接,也方便在文档里逐条解释。
数据库选 MySQL,理由同样务实:金额字段用DECIMAL(10,2)存,不会出现double累加后差几分钱的情况;InnoDB引擎支持事务,结算时要「扣库存 + 写订单 + 写明细」三步一致,没有事务兜底就只能靠手工对账。前端一般走 JSP + Bootstrap + jQuery 的组合,表格渲染和弹窗够用,页面改动不需要额外构建工具,对交作业的场景来说反而省事。
配置上通常把数据源单独抽成db.properties,Spring 主配置里引用,改数据库地址时只动一个文件:
# db.properties:数据源四要素,换机器部署时只改这里 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/cashier?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=rootcharacterEncoding=utf8和serverTimezone这两个参数看着不起眼,实际是中文乱码与时间差 8 小时的高发点,后面避坑章节会展开。驱动类写成com.mysql.cj.jdbc.Driver对应 MySQL 8 系列,如果本地是 5.x,换成com.mysql.jdbc.Driver并把连接串里的serverTimezone去掉即可。
<!-- spring-dao.xml 片段:数据源 + SqlSessionFactory + Mapper 扫描 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <!-- 别名包:XML 里 resultType 可以直接写 Product 而不用全限定名 --> <property name="typeAliasesPackage" value="com.cashier.entity"/> <!-- 下划线转驼峰:create_time 自动映射到 createTime --> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property> </bean> <!-- 扫描 Mapper 接口所在包,接口不需要写实现类 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.cashier.mapper"/> </bean>mapUnderscoreToCamelCase这一项建议一开始就打开,否则create_time查出来永远是null,而你又很难从日志里一眼看出原因。typeAliasesPackage则是让resultType="Product"这类写法生效,省掉一长串包名。这两处配置一旦漏掉,表现都不是报错,而是「字段莫名其妙是空的」,排查起来比异常麻烦得多。
3. 总体架构设计:四个层次与六张核心表的职责划分
设计阶段最容易出问题的不是「功能没想到」,而是「职责没分清」。比如把库存扣减写在 Controller 里,把金额计算散落在 JSP 页面里,最后每个页面各算一遍,日报和明细必然对不上。一份能交付的方案,应该先把层次和表结构定下来,再往里填代码。
3.1 分层结构与一次结算请求的完整链路
系统按照经典四层划分:表现层(JSP 页面 + 静态资源)、控制层(Controller)、业务层(Service)、持久层(Mapper 接口 + XML)。每层的职责边界写清楚,后面加功能才不会互相污染。
- 表现层只负责展示与收集,不做金额计算。页面上的小计只是给人看的,真正的金额必须由 Service 算一遍再落库,否则前端改个数就能把价格改掉。
- 控制层做参数接收与校验、调用 Service、组织返回视图。Controller 里不写业务分支,
if一多就说明该下沉到 Service 了。 - 业务层承担事务边界,一笔结算就是一次事务,扣库存、写订单、写明细要么全成功要么全回滚。
- 持久层只写 SQL,复杂查询(日报聚合、模糊检索)直接写在 XML 里,比用代码拼条件可读得多。
一次完整的结算请求走下来是这样的:收银员点击结账 → 浏览器提交购物车 JSON 或表单到/order/checkout→ SpringMVC 的DispatcherServlet按@RequestMapping找到 Controller 方法 → 参数绑定后调用OrderService.checkout()→ 事务开启,循环逐条扣减库存并累计金额 → 写订单主表、批量写订单明细 → 事务提交 → 返回订单号 → 跳转小票页。任意一步抛异常,事务回滚,库存和订单都不会留下半截数据。
通常还会在spring-mvc.xml里注册一个登录拦截器,把「未登录直接访问后台页面」挡在外面:
<!-- spring-mvc.xml 片段:视图解析器 + 拦截器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:interceptors> <mvc:interceptor> <!-- 拦截后台全部路径,登录页与静态资源放行 --> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/auth/login"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.cashier.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>视图解析器的prefix指向/WEB-INF/jsp/,意味着 Controller 返回"product/list"时实际渲染的是/WEB-INF/jsp/product/list.jsp。好处是 JSP 放在WEB-INF下无法被浏览器直接访问,必须先过 Controller;代价是文件放错目录就一定 404,而且报错信息不会告诉你「路径拼错了」。
3.2 六张核心表与关键字段设计
表不在多,在于能不能支撑住业务。这套系统通常包含六张主表:用户表、分类表、商品表、订单表、订单明细表、库存流水表。下面这张表是我一般会先画出来的东西,字段名和类型基本可以照搬:
| 表名 | 作用 | 关键字段 | 设计注意点 |
|---|---|---|---|
sys_user | 收银员与管理员账号 | id、username、password、role、status | 密码存 MD5 或加盐摘要,不存明文 |
category | 商品分类 | id、name、sort_no | 排序字段方便前台按序展示 |
product | 商品主数据 | id、barcode、name、price、stock、category_id | barcode建唯一索引,price用DECIMAL(10,2) |
sale_order | 订单主表 | id、order_no、total_amount、operator_id、create_time、status | order_no唯一,status区分正常与已退 |
order_item | 订单明细 | id、order_id、product_id、price、num、subtotal | price存成交价快照,商品改价不影响历史单 |
stock_record | 库存流水 | id、product_id、change_num、type、order_id | 每次扣减留一条,方便核对差异 |
order_item里单独存一份price是血泪经验:如果明细只存product_id,将来商品调价,历史订单的金额会跟着变,日报立刻对不上。成交价必须在结算瞬间快照下来。同理,sale_order存total_amount而不是每次实时求和,一是性能,二是防止明细被误改后主表金额漂移。
stock_record这张表很多同学会省掉,觉得库存字段够用。但一旦出现「库存少了 3 件不知道被谁扣的」,没有流水就只能翻订单明细一条条对。加一张流水表成本很低,结算时插一条,类型标记为「销售出库」,退货时插一条「退货入库」,库存差异排查从半天缩短到五分钟。
3.3 工程目录结构与包命名
目录结构建议按「层 + 业务」两级划分,不要把所有 Controller 塞一个包里。下面这种结构是我见到过最不容易乱的一种:
cashier ├── src/main/java/com/cashier │ ├── controller # LoginController、ProductController、OrderController、ReportController │ ├── service # 接口 + impl 子包,事务声明在 impl 上 │ ├── mapper # MyBatis 接口,与 XML 一一对应 │ ├── entity # 与表对应的实体类 │ ├── dto # 购物车项、日报汇总等传输对象 │ ├── interceptor # 登录拦截器 │ └── util # MD5、订单号生成、金额工具 ├── src/main/resources │ ├── mapper # *.xml 映射文件 │ ├── spring # spring-dao / spring-service / spring-mvc │ └── db.properties └── src/main/webapp ├── WEB-INF/jsp # 页面,按模块分子目录 └── static # bootstrap、jquery、css、js有一点需要提醒:src/main/resources/mapper与src/main/java/com/cashier/mapper同名但不同位置,MyBatis 扫描 XML 时路径要写对,否则启动报「Invalid bound statement (not found)」,这个错在 SSM 项目里出现频率极高,原因永远是接口和 XML 的namespace或id对不上。
4. 关键模块落地:登录鉴权、条码检索、结算事务与日报对账
设计定完了,落到代码。这里挑四个最能体现设计功力的模块展开:登录会话、条码检索与分页、结算事务与库存扣减、日报对账。每一段代码后面都会说明「为什么这么写」和「改哪里能适配自己的需求」。
4.1 登录与会话拦截:把「谁在操作」钉死
登录看着简单,实际是权限体系的入口。核心是三件事:密码不明文存、登录态放 Session、后续请求统一拦截。订单表里的operator_id就来自 Session 里的用户,出问题时能追到人。
@Controller @RequestMapping("/auth") public class LoginController { @Autowired private UserService userService; // 登录:校验账号密码,成功则写入 Session 并跳转商品页 @RequestMapping(value = "/login", method = RequestMethod.POST) public String login(@RequestParam String username, @RequestParam String password, HttpSession session, Model model) { SysUser user = userService.findByUsername(username); // 账号不存在与密码错误给同一句提示,避免暴露账号是否存在 if (user == null || !user.getPassword().equals(Md5Util.encode(password))) { model.addAttribute("msg", "账号或密码不正确"); return "login"; } if (user.getStatus() != null && user.getStatus() == 0) { model.addAttribute("msg", "该账号已被停用"); return "login"; } // 拦截器靠这个 key 判断是否放行,改名要同步改拦截器 session.setAttribute("loginUser", user); return "redirect:/product/list"; } }redirect:前缀很重要,登录成功后必须重定向而不是直接返回视图,否则用户按 F5 会重复提交登录表单。返回"login"时由视图解析器拼成/WEB-INF/jsp/login.jsp,所以登录页必须放在那个目录下。
拦截器本身很短,但它决定了后台页面是否裸奔:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute("loginUser"); if (user == null) { // 未登录跳登录页,注意带上项目上下文路径,否则部署到非根路径会 404 response.sendRedirect(request.getContextPath() + "/login.jsp"); return false; } return true; } }request.getContextPath()这一句是必须的。项目通常部署为http://localhost:8080/cashier/,如果直接sendRedirect("/login.jsp"),浏览器会跳到根路径下的登录页,结果是 404 而不是登录页,现象很迷惑。
4.2 条码检索与分页:让扫码枪像「回车键」
扫码枪的本质是一个键盘,扫完自动补一个回车。所以商品检索框只要做到「默认聚焦 + 回车即查询」,就能无缝对接硬件,不需要额外的驱动或接口。
@RequestMapping("/list") public String list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(required = false) String keyword, Model model) { // PageHelper 只对紧随其后的第一条查询生效,中间不能插别的 SQL PageHelper.startPage(pageNum, 10); List<Product> list = productService.search(keyword); PageInfo<Product> pageInfo = new PageInfo<>(list); model.addAttribute("pageInfo", pageInfo); model.addAttribute("keyword", keyword); return "product/list"; }对应的 XML 用LIKE同时匹配条码和名称,条码命中时排在前面:
<select id="search" resultType="Product"> SELECT id, barcode, name, price, stock, category_id FROM product <where> <if test="keyword != null and keyword != ''"> AND (barcode = #{keyword} OR name LIKE CONCAT('%', #{keyword}, '%')) </if> </where> ORDER BY (barcode = #{keyword}) DESC, id DESC </select>ORDER BY (barcode = #{keyword}) DESC是一个小技巧:完全匹配条码的记录排最前,其余按 id 倒序。这样扫码枪扫出来的商品永远在第一行,收银员连查找按钮都不用点。<where>标签会自动处理AND前缀,避免关键字为空时拼出WHERE AND的语法错误。
页面上再加一段脚本做自动聚焦,收银台就可以只用一个输入框干活:
// 页面加载后聚焦检索框,扫码枪输入完自带回车,直接触发查询 $(function () { var $kw = $('#keyword'); $kw.focus(); $kw.on('keydown', function (e) { if (e.keyCode === 13) { // 回车即提交,不做二次确认 $('#searchForm').submit(); } }); // 列表页每次渲染完把焦点还给检索框,避免点完按钮焦点丢失 $('#productTable').on('click', 'tr', function () { $kw.focus(); }); });分页参数pageNum给了默认值 1,避免第一次访问时null导致PageHelper报错;pageSize固定 10 条,是考虑到收银屏幕一般不大,一屏能看完。如果要改成可配置,把它提到配置文件里更合适。
4.3 结算事务与库存扣减:把超卖挡在 SQL 里
结算是最不能出错的一段。它要做三件事:算钱、扣库存、写单据。三件事必须在一个事务里,而且要防住「两个人同时买最后一件」的经典场景。
@Service public class OrderServiceImpl implements OrderService { @Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; @Autowired private StockRecordMapper stockRecordMapper; @Override @Transactional(rollbackFor = Exception.class) public Long checkout(List<CartItem> items, Long operatorId) { if (items == null || items.isEmpty()) { throw new BizException("购物车为空,无法结算"); } BigDecimal total = BigDecimal.ZERO; for (CartItem item : items) { Product p = productMapper.selectById(item.getProductId()); if (p == null) { throw new BizException("商品不存在:" + item.getProductId()); } // 条件更新:库存不足时影响行数为 0,而不是把库存扣成负数 int rows = productMapper.reduceStock(item.getProductId(), item.getNum()); if (rows == 0) { throw new BizException("库存不足:" + p.getName()); } // 金额用 BigDecimal 累加,每一步都保持两位小数 total = total.add(p.getPrice().multiply(BigDecimal.valueOf(item.getNum()))); } SaleOrder order = new SaleOrder(); order.setOrderNo(OrderNoUtil.next()); order.setTotalAmount(total.setScale(2, RoundingMode.HALF_UP)); order.setOperatorId(operatorId); order.setStatus(1); order.setCreateTime(new Date()); orderMapper.insert(order); for (CartItem item : items) { orderMapper.insertItem(order.getId(), item.getProductId(), item.getPrice(), item.getNum()); // 每扣一次库存留一条流水,日后对账靠它 stockRecordMapper.insert(order.getId(), item.getProductId(), -item.getNum()); } return order.getId(); } }@Transactional(rollbackFor = Exception.class)里的rollbackFor不能省。Spring 默认只对RuntimeException回滚,业务里抛的自定义异常如果继承了Exception,事务会「假装成功」地提交掉一半数据。这个坑我在早期的项目里踩过,现象是订单生成了但库存没扣,最后靠人工对账才发现。
真正的防超卖发生在 SQL 里:
<update id="reduceStock"> UPDATE product SET stock = stock - #{num}, update_time = NOW() WHERE id = #{productId} AND stock >= #{num} </update>AND stock >= #{num}是整段的核心。数据库在执行这条 UPDATE 时会加行锁,两个并发请求只有第一个能把库存从 1 扣到 0,第二个影响行数为 0,Service 里据此抛异常回滚。这种做法不依赖应用层的判断,比「先查库存再扣减」安全得多——后者在两次操作之间留了一个时间窗,正是超卖的来源。注意 XML 里>=要写成>=,否则解析器会把它当标签开头。
4.4 日报对账与小票打印:让数据闭环
日报是给店长看的,核心就一条 SQL,按天聚合订单数和金额:
SELECT DATE(create_time) AS day, COUNT(*) AS order_cnt, SUM(total_amount) AS amount FROM sale_order WHERE status = 1 AND create_time >= #{begin} AND create_time < #{end} GROUP BY DATE(create_time) ORDER BY day DESC;这里用左闭右开的区间>= begin AND < end,而不是BETWEEN。create_time带时分秒时,BETWEEN '2024-05-01' AND '2024-05-01'只能查到零点整这一瞬间的数据,是典型的翻车点。前端传日期字符串,Service 里补上00:00:00,查询时用<加一天,数据才不会漏。
小票打印不需要引入打印控件,浏览器自带的打印就够了。页面上单独做一个只含小票内容的视图,配合打印样式去掉页眉页脚和按钮:
/* 打印时隐藏操作区,只保留小票主体 */ @media print { .no-print { display: none !important; } body { margin: 0; } .ticket { width: 76mm; font-size: 12px; } /* 常见热敏纸宽度 */ }打印按钮触发window.print(),用户在打印对话框里选择打印机即可。小票宽度按 76mm 设计,是市面上小票机的常见规格;如果门店用的是 A4 打印机,把宽度改成百分比就行。这种方案的好处是不依赖任何本地组件,换台电脑就能用。
5. 常见问题排查:五个最容易在这类项目里翻车的点
下面这五条几乎每个做 SSM 收银系统的同学都会遇到至少两条。放上来不是因为它们难,而是因为它们的报错信息往往和真实原因对不上号,靠猜会浪费大量时间。
5.1 库存被扣成负数,或者两单只扣了一次
现象:并发测试或者两个人同时结账时,库存出现-1;或者订单生成了,商品库存却没变。
原因:前者是「先select查库存、在 Java 里判断够不够、再update扣减」,两步之间没有锁,两个请求都通过了检查。后者多半是事务没生效——Service 类没被 Spring 扫描到,或者事务管理器配置的数据源和 Mapper 用的不是同一个,@Transactional挂了个寂寞。
解决:把判断放进 UPDATE 的WHERE条件里,用影响行数判断是否成功;同时在启动日志里确认 Service 是否被代理(日志里能看到OrderServiceImpl$$EnhancerBySpringCGLIB这类字样说明代理生效了)。事务管理器要显式配置DataSourceTransactionManager并注入同一个dataSource。
5.2 金额差几分钱,日报和明细对不上
现象:订单总额和明细求和差 0.01,或者商品打 9 折后金额出现19.900000000000002。
原因:用了double或float存金额。浮点数在二进制下无法精确表示 0.1,累加次数一多误差就显形。另一种情况是明细表和主表的取整方式不一致,一个HALF_UP、一个直接截断。
解决:数据库字段用DECIMAL(10,2),Java 用BigDecimal,并且在每一次乘除之后显式setScale(2, RoundingMode.HALF_UP),不要等到最后一步才取整。页面展示可以用fmt:formatNumber格式化,但参与计算的值必须是BigDecimal原值。
5.3 中文乱码,时好时坏
现象:页面上商品名显示成问号或方块,但数据库里用工具查又是正常的;或者提交表单后时间比实际少 8 小时。
原因:链路上有三处编码,数据库表的charset、连接串的characterEncoding、JSP 页面的pageEncoding。任何一处不一致都可能出问题。时间差 8 小时则是serverTimezone没配或配错。
解决:建库时统一utf8mb4,连接串带上useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,JSP 首行声明<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>,同时在web.xml里配一个CharacterEncodingFilter设置forceEncoding=true,把 GET 和 POST 都兜住。
5.4 启动报 Invalid bound statement (not found)
现象:项目能启动,一访问就报这个错,堆栈指向 Mapper 接口的某个方法。
原因:MyBatis 没找到对应的 SQL 语句。常见分支有四种:XML 的namespace和接口全限定名不一致;方法的id和接口方法名不一致;XML 放在src/main/java下但 pom 没配资源过滤,编译后根本没被复制到target;SqlSessionFactoryBean的mapperLocations路径写错。
解决:先把编译产物打开看一眼 XML 在不在target/classes下,不在就是资源过滤问题,在 pom 的build/resources里把**/*.xml加进去。路径没问题再核对namespace与方法名,建议直接从接口复制粘贴,避免手敲出错。
5.5 页面能进但数据全是 null
现象:列表能查出来几条记录,但所有字段都是null,页面表格一片空白。
原因:数据库字段是下划线命名(create_time),实体类是驼峰命名(createTime),而mapUnderscoreToCamelCase没打开。这个问题不报错,只在结果集映射时静默丢弃。
解决:在SqlSessionFactoryBean的configuration里打开mapUnderscoreToCamelCase,或者老老实实在 XML 里写<resultMap>逐字段映射。前者省事,后者更明确,团队里如果有多人协作,写resultMap反而不容易出分歧。
6. 验收与答辩:用一份十分钟演示脚本把系统讲清楚
验收不是把功能挨个点一遍,而是要让评审在十分钟内看到「这条业务链是通的、数据是对的、异常是兜得住的」。我的习惯是先写一份演示脚本,按脚本操作,中途不临时找菜单。
| 步骤 | 操作 | 要展示的点 |
|---|---|---|
| 1 | 未登录直接访问后台地址 | 拦截器生效,跳转到登录页 |
| 2 | 错误密码登录一次 | 提示统一,不暴露账号是否存在 |
| 3 | 登录后在检索框输入条码 | 扫码枪场景,精确匹配排第一 |
| 4 | 加购两件商品,其中一件超出库存 | 提示库存不足,购物车不落库 |
| 5 | 正常结账 | 订单号、金额、小票页一致 |
| 6 | 回到商品列表看库存 | 库存减少数与订单明细一致 |
| 7 | 查当日日报 | 金额与刚才订单求和相同 |
| 8 | 用另一个账号登录并停用 | 停用账号无法再登录 |
演示过程中主动说一句「这里我特意用条件更新防超卖」,比等着被问要好。答辩提问基本围绕四个方向:为什么选 SSM 而不是别的(答分层清晰、SQL 可控、便于讲解请求链路);事务边界在哪(答 Service 方法,粒度为一次结算);库存怎么防并发(答 UPDATE 带条件 + 影响行数判断);金额为什么用BigDecimal(答浮点误差)。这四个问题准备充分,剩下的多半是页面细节。
还有一个容易被忽略的加分项:准备一份「数据核对」截图。订单总额、明细求和、日报聚合三个数一致,比任何架构图都有说服力。评审最怕的就是「页面挺好看,数对不上」。
从那以后我每次带人做这类项目,都会强制走一遍「断网、空库、错误密码、超量下单」这四个异常场景,跑通了才允许开始写论文。正常路径谁都能走通,能不能扛住异常才是方案成不成立的分水岭。希望帮到你。
本文还有配套的精品资源,点击获取