Day2把库存判断和评审流程的骨架搭起来之后,今天的任务是对应试卷5.4、5.5条——订单变更和急插单。原本以为主要工作量在业务逻辑设计上,结果这次踩的坑一半是逻辑设计,另一半是纯手滑,记录一下。
今天的目标
在SalesOrderService基础上补两块:
订单变更:变更后订单自动打回CHANGE_PENDING状态,触发相关部门重新评审
急插单:订单标记为urgent后插队进入评审/生产流程
同时把Day2遗留的两个"占位没实现"补上:listAll/listByStatus/getById已经在Day2补完了,但reviewOrder到今天还是个返回null的空壳。
先揪出两个藏在Day2代码里的老bug
写Day3之前先回头看了一遍createOrder,翻出两个之前没注意到的问题。
问题一:多产品行订单的状态判断被最后一行覆盖
java
for (OrderItem reqItem : orderRequest.getItems()) {
if (reqItem.getQuantity() <= cycle.getStockQuantity()) {
order.setStatus(OrderStatus.WAITING_FOR_SHIPMENT);
} else {
order.setStatus(OrderStatus.PENDING_REVIEW);
}
}
order.setStatus()写在了循环体里。如果订单有多个产品行,最终状态只取决于最后一行的库存情况,跟前面几行是否缺货完全无关——第一行缺货、第二行有货,最终会被错误地标记成"可发货",其实还有一行没货没处理。
问题二:产品不存在时的死代码+空指针
java
ProductionCycle cycle = cycleRepository.findByProductCode(reqItem.getProductCode()).orElse(new ProductionCycle());
if (cycle == null) {
throw new RuntimeException(...);
}
orElse(new ProductionCycle())永远不会返回null,下面那个if (cycle == null)压根执行不到。真实情况下产品代码不存在时,会拿到一个空的ProductionCycle对象(stockQuantity是null),下一行拆箱直接抛NullPointerException,而不是我原本想要的友好提示。
两处都改成了循环结束后统一判定状态、orElseThrow直接抛业务异常,问题不大,但如果不是今天回头看,这俩bug可能要留到后面某天调试时才会暴露,排查成本更高。
设计上的决策点:评审不通过怎么办
写变更/急插单触发重新评审的逻辑时,卡在一个问题上:现在的OrderStatus枚举只有评审通过的路径(ENGINEERING_REVIEWED → PLANNING_REVIEWED → CONFIRMED),没有"评审不通过"对应的状态。
最后决定:不通过就打回复用PENDING_REVIEW,重新走一遍评审,而不是新增一个REVIEW_REJECTED状态。理由是这样能复用同一套评审入口逻辑,不用为拒绝分支单独再维护一条状态流转路径,对个人技能测试这种范围的项目来说更划算。
又一个"占位没实现",今天补上了
java
public SalesOrder reviewOrder(Long orderId, String department) {
return null;
}
这个方法从Day2起就是个空壳,今天扩展了签名(加上reviewer、opinion、approved),并接上了打回PENDING_REVIEW的逻辑。不过实现完之后意识到一个新的设计缺口:目前是"最后评审的部门决定状态",如果计划部评审完直接判定CONFIRMED,工程部还没评审也会被跳过。如果实际业务要求两个部门都必须评审通过,现在这版逻辑还不够严谨,需要在SalesOrder上加类似engineeringApproved/planningApproved的独立标记分别记录——这个问题先记在这,还没最终定下来怎么改。
一次纯手滑引发的连环事故
设计和bug都不算意外,真正意外的是今天在往OrderChangeRecordRepository.java里粘贴构造函数代码时,贴错了文件——本该粘进SalesOrderService.java的一段构造函数代码,直接糊进了这个本该只有几行的Repository接口里,导致package声明和类体混在一起,编译器直接报了7、8个错误。
更麻烦的是第一次尝试"修复"的时候,没有先全选清空文件内容,只是把新代码粘贴插入到了原内容中间——结果变成了interface嵌套interface、package语句出现了两次、末尾还多了个孤立的},比原来的错误更离谱。最后是老老实实Ctrl+A全选、Delete清空、再重新粘贴一次干净版本才解决。
教训:跨文件复制粘贴时,一定要先确认当前光标停在哪个文件的哪个位置;如果发现贴错了,第一反应应该是全选清空重来,而不是在错误内容基础上"打补丁"式地再贴一次——那样只会让问题更难看懂。
环境这块,还是老熟人
MySQL连接问题从Day2拖到了今天还没根治,报错还是熟悉的Communications link failure / Connection refused: getsockopt。排查发现问题出在服务名上——用sc query state= all | findstr /i mysql查出真实服务名是MySQL267(不是常见的MySQL80),而且第一次net start还因为命令行没有用管理员权限跑,报了"系统错误5,拒绝访问"。换成管理员权限的命令行窗口后才顺利执行net start MySQL267。
这个坑本质上还是Day1、Day2就总结过的经验:新环境第一件事先确认基础设施是不是真的就位,服务名、权限这些看着琐碎的细节,一旦漏查,报错信息会指向完全不相关的方向(比如JDBC连接失败,实际根因却是服务名打错+权限不够)。
今天的成果
修复了Day2遗留的两个bug:多产品行状态覆盖、产品不存在时的空指针
changeOrder、urgentInsert两个方法完成设计并写好代码,评审不通过统一打回PENDING_REVIEW
reviewOrder从空壳补成了可用实现,但双部门评审的严谨性还留了一个待改进点
定位到MySQL服务真实名称MySQL267并用管理员权限成功启动
经验总结
回头看老代码跟往前写新代码同样重要:今天两个bug都是在写Day3新逻辑之前,回头审视Day2代码时发现的,如果不做这一步,可能会一直带着病继续往上叠代码。
状态机没覆盖的分支要显式决策,而不是含糊过去:评审不通过要不要单独状态,这种问题拖到写代码的时候才发现最容易踩坑,最好是设计阶段就想清楚。
复制粘贴要认清"贴在哪":多文件同时开着改代码时,贴错文件、贴错位置的成本比想象中高,出错了要敢于推倒重来,而不是在错误基础上继续打补丁。
环境问题的报错信息往往会指向错误方向:JDBC连接失败第一反应是查连接串,但今天的根因其实是服务名和权限,报错信息本身不会直接告诉你这些。
明日预告
Day4计划做发货与交付跟踪接口,对应流程图里"发货"到"交付"的环节,同时要联动今天写的变更/急插单逻辑——处于CHANGE_PENDING状态的订单不能直接进入发货环节,需要先走完重新评审。
浙公网安备 33010602011771号