这套咖啡门店进销存系统的毕设题是这段时间我帮学生带的项目里比较有意思的一个,题号38142,技术路线限定JAVA。看到题目第一反应是常规CRUD,但真正把咖啡门店的进销存业务拆完才发现,这玩意儿跟学校里那种纯商品进销存还是有不小的距离。咖啡门店除了有常规的采购、入库、销售、盘点外,还夹着"原料到成品"的加工换算、鲜牛奶的保质期批次管理、杯装饮品的损耗折算。这篇文章我就把这套系统的拆题思路、数据建模、核心功能实现、还有联调时踩过的坑完整梳理一遍,代码部分已经整理成可直接运行的工程,需要的同学可以对照本文按图索骥。
1. 项目背景与需求拆解
1.1 咖啡门店进销存和普通商品进销存差在哪
早几年很多学生做进销存系统会套超市模板——商品表、供应商表、采购单、销售单,完事。但咖啡门店的真实运营状态完全不是这么回事。
门店里要管的东西分成两大类:一类是原材料,咖啡豆、全脂牛奶、燕麦奶、糖浆、纸杯、杯盖、吸管、打包袋;另一类是卖给顾客的成品,拿铁、美式、卡布奇诺、冷萃,甚至还有少数非咖啡饮品。原材料入库以后不能直接按"件"卖,它要先经过咖啡机、制冰机、人手操作,加工成成品才能销售。这意味着系统不能只管理商品本身的库存,还必须管一条"原料消耗关系链",咖啡行业叫配方或BOM。
第二点差异是保质期。普通便利店的进销存对保质期敏感性不强,但咖啡门店的冷藏牛奶保质期短则6天,长则14天,一旦把批次先进先出做不好,过期报废的损耗就会很夸张。系统里的库存就不能只是"一个数量字段",必须把每一批入库的原料按批次记录入库日期和保质期,销售扣减时按先进先出原则消耗。这些需求在校验一般的"商品进销存"模板里根本没有。
还要考虑"半成品"或"门店自制"带来的库存变化口径问题。咖啡门店有部分物料是二次加工的,比如冷萃咖啡液提前批量制备,装瓶后也会有损耗报废。不是说采购买进来多少,销售端就能对上多少,中间的损耗和内部转化需要有单据记录,最好有独立的加工单或者领料出库单。
1.2 系统角色与核心业务流程梳理
和这类毕设系统常见的角色划分差不多,这套系统我拆成了四类角色,覆盖门店内外两条线。
| 角色 | 权限边界 | 典型操作 |
|---|---|---|
| 系统管理员 | 用户管理、基础数据维护、角色授权 | 创建员工账号、维护供应商档案、配置预警参数 |
| 采购专员 | 采购计划、采购订单创建与提交、到货确认 | 新建采购单、提交审核、确认到货、登记采购入库 |
| 店长 | 采购审核、加工单审核、库存盘点审核、报表查看 | 审批采购计划、批准盘点结果、查看经营日报 |
| 收银员/店员 | 前台开单、销售出库、简单库存查询 | 点单结算、查看库存和预警 |
角色划分定了以后,核心业务流其实可以收敛成三条主线:
第一条是"采购进店"链路:门店根据每日销售量预测和库存水位,向供应商发起采购申请 → 采购专员录入采购订单 → 店长审核 → 供应商送货 → 仓库/吧台负责人确认收货 → 系统按采购明细自动生成入库记录,并给每一批原料生成独立的批次和保质期信息。
第二条是"生产/加工消耗"链路:咖啡门店一般不会有庞大的生产车间,更多是"吧台边做边卖"模式。这部分我建议用"配方+销售扣料"来实现,也就是在商品和物料之间维护BOM表,销售一个拿铁,系统就自动按配方扣减对应咖啡豆、牛奶、纸杯等原料库存。如果想要更严谨,还可以加一个"每日备料/预加工单"来处理批量煮制咖啡液或批量备料,这样能够区分"理论消耗"和"实际消耗"。
第三条是"库存查询与盘点"链路:店长和店员能实时查看每一种原料的库存数、可用天数、临期批次,定期做全盘或抽盘,系统自动生成盘盈盘亏记录并调整库存。
这三条链路理顺了,报表模块才有数据可以汇总,日报就能算出原料成本率、理论毛利和损耗率,而不是只有光秃秃的销售流水。
2. 总体设计与技术选型
2.1 后端技术栈选了哪些组件,为什么这么选
毕设项目的一个特点是既要保证技术方案能落地,又不能让复杂度失控。这套系统最终锁定的后端组合是:Spring Boot 2.7.x + MyBatis-Plus + MySQL 5.7,权限部分用Spring Security整合JWT做无状态认证。解释一下每个选择的理由。
Spring Boot不必多说,当前企业级Java后端最主流、文档最全、学生最熟悉。MyBatis-Plus是加分项,它的BaseMapper直接提供单表CRUD,代码量少很多,同时又保留了XML自定义SQL能力,适合做进销存里那种多表关联、分组汇总的统计查询。MySQL稳定好用,配SQLyog或Navicat管理都很方便。
权限这块,常见的选择有Spring Security + JWT和Shiro + JWT。我实际给学生用的是Spring Security + JWT,只有一个核心原因:项目里不需要太复杂的资源级权限,只需要按角色控制接口访问,Spring Security的注解式鉴权最顺手。需要注意Spring Security 5.7版本之后WebSecurityConfigurerAdapter被弃用,很多教程里的写法会报错,所以工程里我直接用了SecurityFilterChain,后面写代码时会贴这段。
前端部分为了让论文里有界面截图和演示视频,单独用Vue3 + Element Plus起了一个管理端页面,通过Axios调后端REST接口。如果觉得Vue门槛偏高,也可以把前端换成Thymeleaf服务端渲染,后端一套代码直接出页面,工作量会更小。但考虑到进销存系统里表格、弹窗、表单交互很多,Vue + Element Plus做出来的视觉效果确实更好,我就保持了这种前后端分离结构。
一个容易被忽略的小点是JDK版本。如果选Spring Boot 2.7.x,用JDK 8最稳,不要顺手用JDK 17或21,MyBatis-Plus老版本在编译期有时会踩坑;如果非要用JDK 17,就得把Spring Boot升到3.0以上,但Spring Boot 3.x把javax改成jakarta,很多细节点会跟着变,毕设阶段真没必要给自己加戏。
2.2 项目分层结构与源码目录规划
工程项目按标准的单模块Maven结构组织,但package分层一定要清晰,不能让Service里蘸着Controller,也不能把SQL写在业务代码里。这套系统我采用的包结构是这样的:
com.coffee.stock ├── common // 统一返回结果、异常处理、常量定义 ├── config // Security、CORS、Jackson等配置 ├── controller // 控制层接口入口 ├── service // 业务逻辑层接口及实现 ├── mapper // MyBatis-Plus Mapper接口 ├── entity // 数据库实体类 ├── dto // 入参出参对象、视图模型 └── utils // JWT工具、日期工具等分层的核心原则是"接口不进controller做业务"、"controller只做参数接收和结果包装"。很多学生喜欢把判断逻辑全写在controller里,觉得这样代码少,但一旦后面加入采购审核流、配方换算这些逻辑,controller就会膨胀到没法看,而且Service层没法独立测试。
再单独说说exception处理。系统里要处理采购单不存在、库存不足、单据已被审核、重复提交这类业务异常,我统一定义了BizException,通过@RestControllerAdvice做一个全局异常处理器,把错误信息封装成统一格式返回前端。这样Service里不需要每个方法都try-catch来回传,代码干净很多。
2.3 数据库表结构设计的关键决策
数据库是整个系统的地基,这一块多花十分钟,后面能少改三天代码。进销存相关的核心表我拆成了十二张,按功能域可以分成四组。
基础资料域比较直接,包括sys_user(用户)、sys_role(角色)、product(商品/物料档案)、supplier(供应商);业务单据域是进销存的主干,包括purchase_order(采购单头)、purchase_order_item(采购单明细)、inbound_order(入库单)、stock_record(库存流水);库存核心域是bom_info(成品配方表)、product_batch_stock(批次库存表)、stock_check(盘点单);再加上system_config(系统配置)用来存预警参数。
重点说一下product表的建模。这张表不能只按业务直觉建成一张普通"商品表",因为咖啡门店的product既要存原料,也要存成品,两类数据字段大部分相同但业务含义差异很大。一张表里我用product_type做区分,0代表原料、1代表成品,价格、单位、库存预警值都放同一张表里就能统一维护。如果拆成RawMaterial和Product两张表,配方表做关联和统计时会变得复杂,没必要。
批次库存表是整个库存模块的精华,字段设计我强烈建议按下面的样子来:
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| product_id | bigint | 商品/物料ID |
| supplier_id | bigint | 供应商ID |
| batch_no | varchar | 批次号,方便追溯 |
| quantity | decimal | 当前批次剩余数量 |
| production_date | date | 生产日期/入库日期 |
| expire_date | date | 保质期截止日期 |
| create_time | datetime | 创建时间 |
把库存按批次拆开,意味着同一个物料同一时刻可能有多个批次并存,比如说牛奶7月1日和7月3日分别入库了两批,批次库存表里就有两条记录。销售扣减时按expire_date升序遍历,先扣临期批次,这样"先进先出"才精准。如果只是在一行库存记录上写quantity加加减减,保质期预警就无从谈起。
配方表bom_info我是这样设计的:一个成品对应多行配方明细,每行记录原料id和用量。举例,一杯拿铁对应的bom_info可以有三行:咖啡豆18克、全脂牛奶220毫升、PET杯+盖一套。销售端只需要知道卖出了一杯"拿铁",系统就按bom读取明细,把消耗记录写入库存流水。这个思路对咖啡门店特别适配,对写论文时讲"成本核算"也是一个很好的切入点。
3. 核心流程与功能实现
3.1 采购入库流程的状态机与实现思路
采购业务这条链上,状态管理是核心难点。我见过不少学生项目把采购单做成一次性创建的"死单",建成即入库,这样虽然代码简单,但完全不符合门店"先审后收"的真实流程。这套系统的采购单设计了四个状态:
待审核(0) → 已审核/待收货(1) → 已入库(2) ↘ 已驳回(3)具体来说,采购员填单时录入供应商、预计到货日期和每行物料的数量、预估单价,采购单生成后默认是待审核。提交给店长审核后有两种结果:驳回,退回给采购员修改;通过,采购单进入待收货状态。等货到了,采购员或库管在系统里做"确认到货并入库",系统才把采购单状态改成已入库,同时往两处写库存数据:一是往product_batch_stock表插一条带批次的库存记录,二是往stock_record表写一笔"入库"流水。
很多初学者会问:"直接改库存表数量,同时插一条流水不就行了,为什么还要多一个批次表?"原因是库存数量是状态,流水是行为,如果要查"某一天某一批牛奶还剩多少"或者做保质期预警,只靠流水远远不够。批次表相当于"当前存量结构",入库按批次插入,出库按批次扣减,流水则负责记录每一次变化的来龙去脉。库存快照+行为流水分开维护,报表和审计都能追溯。
我贴一段采购入库service里"生成批次库存"的核心代码,这是整个模块里最容易理解也最值得参考的部分:
@Transactional(rollbackFor = Exception.class) public void confirmInbound(Long orderId) { PurchaseOrder order = purchaseOrderMapper.selectById(orderId); if (order == null || !Integer.valueOf(1).equals(order.getStatus())) { throw new BizException("采购单不存在或当前状态不允许入库"); } LambdaQueryWrapper<PurchaseOrderItem> itemWrapper = Wrappers.lambdaQuery(); itemWrapper.eq(PurchaseOrderItem::getOrderId, orderId); List<PurchaseOrderItem> items = purchaseOrderItemMapper.selectList(itemWrapper); for (PurchaseOrderItem item : items) { ProductBatchStock batch = new ProductBatchStock(); batch.setProductId(item.getProductId()); batch.setSupplierId(order.getSupplierId()); batch.setBatchNo(generateBatchNo(order.getId(), item.getProductId())); batch.setQuantity(item.getQuantity()); batch.setProductionDate(LocalDate.now()); Product product = productMapper.selectById(item.getProductId()); // 库存预警天数在物料档案上配置,比如牛奶3天预警 batch.setExpireDate(LocalDate.now().plusDays(product.getShelfLifeDays())); productBatchStockMapper.insert(batch); StockRecord record = new StockRecord(); record.setProductId(item.getProductId()); record.setChangeType("INBOUND"); record.setChangeQuantity(item.getQuantity()); record.setRefOrderNo(order.getOrderNo()); stockRecordMapper.insert(record); } order.setStatus(2); // 已入库 purchaseOrderMapper.updateById(order); }几个细节再强调一下。createOrder后如果货一直不到就永远卡在待收货状态,系统里提供了一个"采购单跟踪"列表,按供应商汇总可查看"已订未到"金额,否则过期订单只能靠人去翻。入库时还要校验商品档案里是否启用了批次管理,不是所有物料都严格按保质期走,像吸管、打包盒这类仓储期很长的物料,可以走简单库存逻辑,批次表只记录一批即可。
3.2 配方出库与销售扣料如何联动
咖啡门店的前台卖出一杯饮品,和后台库存之间不是直接一对一关系。一杯美式卖28元,系统如果直接把"美式"这个商品的库存减1,那咖啡豆库存一辈子都不会减少。所以这里必须引入配方扣料机制。
表格化描述一下拿铁和美式两个配方,能更直观看清建模逻辑:
| 成品 | 配方原料 | 单位用量 | 单位 |
|---|---|---|---|
| 拿铁(大杯) | 意式咖啡豆 | 18 | 克 |
| 拿铁(大杯) | 全脂牛奶 | 220 | 毫升 |
| 拿铁(大杯) | 大杯+盖 | 1 | 套 |
| 美式(大杯) | 意式咖啡豆 | 20 | 克 |
| 美式(大杯) | 大杯+盖 | 1 | 套 |
销售结算操作触发的服务,伪代码逻辑是这样的:
@Transactional(rollbackFor = Exception.class) public void sellFinish(SaleOrder saleOrder) { BigDecimal totalAmount = BigDecimal.ZERO; for (SaleOrderItem saleItem : saleOrder.getItems()) { Product product = productMapper.selectById(saleItem.getProductId()); if (Integer.valueOf(1).equals(product.getProductType())) { // 成品,需要按配方扣减原料库存 List<BomInfo> bomList = bomInfoMapper.selectList(Wrappers.lambdaQuery() .eq(BomInfo::getFinishedProductId, product.getId())); for (BomInfo bom : bomList) { deductStock(bom.getMaterialId(), bom.getQuantity().multiply(saleItem.getQuantity())); } } else { // 如果物料被直接销售,也需要扣库存 deductStock(product.getId(), saleItem.getQuantity()); } totalAmount = totalAmount.add(product.getSalePrice().multiply(saleItem.getQuantity())); } // 落销售单主表 saleOrder.setTotalAmount(totalAmount); saleOrderMapper.insert(saleOrder); }如果门店有"批量备料"环节,比如早上一口气煮2升冷萃液,这时候需要在系统里做一个"生产入库单",把咖啡液按自制半成品入库,再从半成品往下做一层配方。这种设计有两个好处,一是核算准确,冷萃液生产过程中产生的咖啡渣损耗可以体现在领料量与实际入库量的差额上;二是收银员在高峰时段不需要逐杯扣料,系统压力集中在备料环节。
不过对大多数毕设而言,做到"销售单据自动按配方扣减原料"这一层已经超过了及格标准。有了这层逻辑,后台报表才能算出每杯饮品的原料成本,比如一杯拿铁原料成本大约是多少,当月总销量对应理论原料消耗又是多少,再对比实际盘点结果,损耗异常就能一眼看出来。
3.3 保质期预警模块的设计
进销存系统如果只是管数量,保质期预警就没必要单独做模块了。但咖啡门店损耗重灾区就是"牛奶过期没发现、到盘点才发现一整批报废"。所以在批次库存表之上,要单开一个预警模块。
我是用一个Spring定时任务来做这件事的,每天凌晨两点扫描一次product_batch_stock这张表,找出expireDate与当前日期的差值小于等于预警天数(物料档案可以单独配置,比如牛奶配置3天、面包配置2天、糖浆配置15天)且剩余数量大于零的批次,在预警结果表里生成记录,并在登录后的首页以醒目列表展示给店长。
单纯有定时任务还不够,临期处理动作也很重要。预警列表的每条记录要能点击"处理",处理动作分三种:正常使用(继续扣减优先消耗该批次)、转赠/内部食用、报损报废。选择报废后系统会自动生成一条报损出库流水,扣减该批次库存,这样月底报损的原因是清晰可查的,不会产生一笔"进得来、出不去"的账。
3.4 盘点与损益调整怎么做到不破坏库存流水审计
盘点逻辑看着简单,实际上是进销存系统踩坑重灾区。原因在于盘点过程中会出现"账实差异",系统要调库存,但库存调整如果直接去改product_batch_stock的现有数量,那之前的所有流水记录跟当前库存必然对不上,整套系统就丧失可信度了。
正确的做法是让盘点也走"单据+流水":先按实物盘点的结果录入初盘数量,系统自动算出每项物料的"账存数量-实盘数量"差额,正数代表盘盈、负数代表盘亏。盘点单经店长审核通过后,系统生成一笔类型为STOCK_CHECK的库存流水,并同时调整对应批次的库存剩余数量。这就能保证库存变化永远有单据来源,不会出现"凭空消失"的记录。
还需要考虑拆单审核场景。一张全店盘点单可能涉及两百多个物料,如果某几个物料盘亏较大需要店长单独确认,整个单一次性审核显然不方便。但考虑到毕设的演示场景,做成整单审核完全没有问题,也不会被评委抓住把柄。重点是表达清楚了"盘盈盘亏是要经过审核的"这个业务支撑。实际企业ERP会做拆单审批,那是产品复杂度的问题,与这个项目规模不匹配。
4. 实战中容易踩的坑与排查方法
4.1 库存扣减并发问题,别用查再减
做进销存系统,并发是绕不开的话题。但大多应届生写的代码都是这样的套路:
ProductBatchStock stock = mapper.selectById(productId); if (stock.getQuantity() >= target) { stock.setQuantity(stock.getQuantity() - target); mapper.updateById(stock); }这段代码单线程测没问题,但一旦收银台同时来两笔订单,两个请求同时读出库存为10,同时判断够扣,又同时把库存更新成8,最后一杯饮品就被凭空"卖"了两次。这就叫超卖/超扣。
解决方式最常用的是两条路,一条是更新带上库存条件,一条是用乐观锁版本号。
更新带库存条件是这么写的:
int rows = productBatchStockMapper.deductStock(batchId, quantity); // SQL: update product_batch_stock // set quantity = quantity - #{quantity} // where id = #{batchId} and quantity >= #{quantity} if (rows == 0) { throw new BizException("库存不足,扣减失败"); }数据库行锁会保证同一时刻只有一个事务能成功更新该行,不会出现先读出旧值再覆盖新值的问题。用乐观锁理论上也可以,但需要额外维护version字段,代码多几行,不如直接用条件更新简单粗暴。注意,这里一定要在事务里执行,并且如果一次销售扣多个物料,任何一个物料扣减失败都需要整体回滚,避免出现"收了钱却只扣了部分库存"的脏数据。
如果觉得批次级扣减还不够,可以考虑在product上限时约束,调整到物料维度再按批次先进先出遍历,不过那属于更精细的库存引擎设计,毕设做到单批次扣减+防超扣已经足够,答辩时能把并发扣减原理讲清楚就已经是亮点。
4.2 BigDecimal不要用double算金额
这是老生常谈,但每次改代码还是会看到有人用double来存金额。门店一杯拿铁28元,如果营销活动打9折,double计算会出现25.200000000000003这种经典结果。哪怕最后四舍五入显示没问题,审计一栏也会显得不专业。
这系统里我统一要求:数据库金额字段用decimal(10,2),Java实体用BigDecimal,计算销售总价、采购总价、毛利都走BigDecimal。MyBatis-Plus映射decimal不会丢精度,但做除法时注意要显式传入MathContext或setScale,否则除不尽时会有ArithmeticException。
还有一点,销售单价、采购单价、成本价三个字段虽然在product表里都叫price,但含义不同。为了论文里好描述,建议在实体里明确区分salePrice、purchasePrice和costPrice,而不是全都叫price。今后做利润计算、成本分析报表时就不用再猜字段含义了。
4.3 JWT认证和接口鉴权的常见报错
毕设项目里做接口权限的适配,报错重灾区通常是Spring Security的过滤放行顺序写错了。早期的WebSecurityConfigurerAdapter不推荐之后,新写法要让所有接口默认走认证,同时把登录接口和静态资源放行:
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests() .antMatchers("/api/auth/login", "/doc.html", "/webjars/**").permitAll() .antMatchers("/api/purchase/audit/**").hasRole("MANAGER") .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }注意hasRole和hasAuthority的差异是Spring Security的经典扣分题。hasRole("MANAGER")要求用户角色名是ROLE_MANAGER,但很多人数据库里存了manager,接口一调就403。解决办法是统一在用户登录成功后给角色设置ROLE_前缀,或者在配置里用hasAuthority("ROLE_MANAGER"),总之前后要一致。这个问题我在联调阶段基本每次都会被绊一下,建议项目跑起来就先用一个带权限的接口验证login、无权限、越权三个场景。
5. 后期扩展与答辩准备
5.1 这套系统还能往哪些方向做增强
进销存系统是做不完的,毕设能稳定运行、论文逻辑自洽就已经达到目标。但我个人建议,如果时间和精力允许,可以挑下面一到两个方向做增强,会让项目明显区别于隔壁同学的"普通管理系统"。
第一个方向是数据可视化大屏。把每天的销售额、订单量、原料消耗量、库存周转天数放到一个首页看板上,用ECharts展示折线图和饼图。门店经营者不需要打开一堆列表自己去算今天赚了多少,打开系统第一眼就能看到所有核心指标。这个方向对前端要求不高,但演示时视觉冲击力极强。
第二个方向是库龄分析和智能补货建议。目前系统能知道每个批次入库多久了、有多少临期库存,那稍微扩展一下,就能在采购计划页面上提示哪些物料需要在未来几天补货、建议补货量是多少。计算公式是:近7天平均日销量×采购提前期天数-当前可用库存。这已经有点商业系统智能化的味道,放在论文的创新点里也说得过去。
第三个方向是移动端适配。用H5或微信小程序给店员做一个移动盘点入口和采购审核入口,店长人不在店里也能完成单据审批,贴近真实运营。如果做小程序,需要考虑后端接口要能被微信访问,本地联调时可以用内网穿透工具,但如果担心复杂,做H5配合浏览器直接访问会更省事。
5.2 答辩评委常追着问的几类问题
写论文和准备答辩的过程里,有几个技术问题基本属于"必问题",提前把回答思路准备好,现场就不会卡壳。
第一个问题一定是"库存扣减如何防止超卖"。答案要讲清楚两层结构:数据库层面靠条件更新解决并发覆盖,应用层靠事务保证多物料扣减原子性。如果能说清悲观锁与乐观锁的取舍、为什么选择条件更新的理由,基本就已经能够证明是真正理解了。
第二个问题会围绕"为什么要设计配方表"。可以回答说咖啡门店的库存主体是原料而非成品,销量不能直接对应库存量,需要通过BOM配方将销售单转化为原料消耗,同时还可以用预计消耗和实际消耗对比来发现原料浪费。最好举一个"一杯拿铁需要18g豆子+220ml牛奶"的具体例子来佐证。
第三个问题会问"系统的表和表之间怎么关联"。这时不能只会说外键,而要说明主单据与明细单据是1对N,物料库存按批次独立存储,销售单、采购单、库存流水是围绕单据形成闭环,并强调所有库存变动都有单据来源,财务审计上才能解释每一笔差异。
第四个问题可能会把业务和实现结合:"如果今天牛奶库存为负,说明什么?"回应思路要给出具体的排查路径:先确认是否存在未审核的盘点单,再查临期报废有没有下达报损出库,再核对销售扣料是不是BOM配置单位不对导致超扣。这些问题看起来在问业务,实际上是在考察数据一致性和错误追踪意识。
5.3 一份能直接上手的自测清单
系统写完不等于功能全对,建议大家按下面的顺序做一轮自测,基本上可以把隐藏的80%逻辑问题暴露出来。
| 编号 | 自测场景 | 预期结果 |
|---|---|---|
| 1 | 创建一个没有任何明细的采购单并提交 | 系统应拦截,不能生成空单 |
| 2 | 采购单驳回后再次编辑提交 | 状态能回到待审核,且原单号不变 |
| 3 | 对已入库的采购单再次入库 | 系统拒绝,提示采购单已完成 |
| 4 | 一次售出多杯不同饮品 | 原料库存按BOM明细逐项扣减 |
| 5 | 库存不足时执行销售 | 整笔订单回滚,不产生部分扣减 |
| 6 | 修改物料保质期预警天数后重新扫描 | 预警结果按新参数刷新 |
| 7 | 库存盘点提交后审核 | 库存同步变化,流水记录类型为盘点 |
| 8 | 三个角色分别登录访问越权接口 | 返回403或无权限提示 |
| 9 | 销售总价用BigDecimal累加 | 分页合计和订单金额完全一致 |
| 10 | 模拟同一商品并发销售 | 库存不会出现负数或超扣 |
这些自测项除了要过功能,更重要的是观察程序有没有报异常或者日志里有没有莫名其妙被吞掉的错误。一个成熟项目,前端弹错误提示也好、后端打日志也好,错误信息必须能读懂,不能只显示"系统异常"四个字,否则排查问题会变得非常痛苦。
在项目交付前,我自己有一个多年的习惯:从git仓库clone一份新代码,按README文档从零把数据库和生产配置跑起来,再完整跑一遍自测清单。很多毕设项目在原作者电脑上能跑、换台电脑就起不来,绝大多数问题就出在环境依赖文档写得不完整、数据库脚本执行顺序没人验证、配置文件里的绝对路径没有改成相对路径。这套系统的数据库脚本和初始化数据已经全部集成到工程目录的doc/sql路径下,执行顺序按文件名编号走就行,双击跑完就能看到带演示账号的初始数据。最后再提醒一句,如果时间不太够,优先把主流程的健壮性打磨好,其次才是页面美化,毕竟系统最终是给店长和收银员用的,一次入库录入中途报错、再登录发现单据状态错乱,这种事一发生就会让整个系统的可信度垮掉。