# 逻辑错误修复报告 **任务编号:** 2.3 **扫描范围:** `/root/.openclaw/workspace/erp-java-backend/services/` 下所有微服务(共31个) **检查维度:** 循环依赖、事务边界错误、数据库死锁风险、订单状态机、空指针风险 --- ## 一、循环依赖检查 ### 检查结果:✅ 已有防护,无需修复 | 服务 | 注入点 | 状态 | |------|--------|------| | `dashboard-service/DashboardServiceImpl` | `RedisTemplate` 使用 `@Lazy` 避免循环依赖 | ✅ 已正确使用 | | `tenant-service/TenantServiceImpl` | `RedisTemplate` 使用 `@Lazy` 避免循环依赖 | ✅ 已正确使用 | 其他服务均通过构造函数注入(`@RequiredArgsConstructor` + `final`),无循环依赖风险。 --- ## 二、事务边界错误(Feign调用被@Transactional包裹) ### ⚠️ 高风险 — 发现问题(purchase-service) **问题文件:** - `purchase-service/.../impl/PurchaseOrderServiceImpl.java` - `purchase-service/.../impl/PurchaseReturnServiceImpl.java` - `purchase-service/.../impl/PurchaseInboundServiceImpl.java` #### 问题 2.1:`approve()` 方法 — 本地事务先提交,远程通知后失败 ```java // PurchaseOrderServiceImpl.java:211 @Transactional public PurchaseOrder approve(Long id, PurchaseOrderDTO.ApproveRequest request, Long operatorId) { // ...更新本地订单状态... purchaseOrderMapper.updateById(order); // ✅ 本地已提交 try { financeClient.createAccountsPayable(...); // ❌ 远程调用在事务内,失败则本地已回滚 } catch (Exception e) { log.warn("通知财务创建应付账款失败..."); } return order; } ``` **风险分析:** | 场景 | 结果 | |------|------| | Feign调用超时/异常被catch | 本地事务正常提交,财务应付账款**未创建** → 数据不一致 | | Feign调用成功,数据库更新失败 | 事务回滚,但财务侧已创建AP → 数据不一致 | | finance-service不可用 | 本地订单已审批,AP未创建 | #### 问题 2.2:`cancel()` 方法 — 同样问题 ```java // PurchaseOrderServiceImpl.java:246 @Transactional public void cancel(Long id, Long operatorId) { order.setStatus(PurchaseOrderStatus.CANCELLED.getCode()); purchaseOrderMapper.updateById(order); // ✅ 本地已提交 try { financeClient.cancelAccountsPayable(order.getOrderNo()); // ❌ 失败则AP未取消 } catch (Exception e) { log.warn("通知财务取消应付账款失败..."); } } ``` #### 问题 2.3:`PurchaseReturnServiceImpl` — 确认退货时调用仓库 ```java // PurchaseReturnServiceImpl.java:137 @Transactional public void confirmReturn(...) { // 本地更新退货单状态 purchaseReturnMapper.updateById(ret); // 调用仓库创建出库单 WarehouseFeignClient.OutboundResponse resp = warehouseClient.createOutbound(...); // ❌ 如果这里失败,退货单已更新但出库单未创建 } ``` #### 问题 2.4:`PurchaseInboundServiceImpl` — 入库时调用仓库 ```java // PurchaseInboundServiceImpl.java:168 WarehouseFeignClient.InboundResponse resp = warehouseClient.createInbound(warehouseRequest, operatorId); ``` #### 问题 2.5:FeignClient无Fallback降级 `FinanceFeignClient` 和 `WarehouseFeignClient` 均未配置 `fallback` 或 `fallbackFactory`,服务不可用时直接抛出异常。 ### ✅ 修复方案 **核心原则:Feign远程调用必须放在事务之外(或者在事务提交之后执行)** #### 方案A(推荐):使用 `TransactionSynchronizationManager` 在事务提交后执行 ```java @Transactional(rollbackFor = Exception.class) public PurchaseOrder approve(Long id, PurchaseOrderDTO.ApproveRequest request, Long operatorId) { PurchaseOrder order = purchaseOrderMapper.selectById(id); if (order == null) throw new BusinessException("采购订单不存在"); if (!PurchaseOrderStatus.PENDING.getCode().equals(order.getStatus())) { throw new BusinessException("只有待审批状态可以审批"); } order.setStatus(PurchaseOrderStatus.APPROVED.getCode()); order.setApproverId(operatorId); order.setApproveTime(LocalDateTime.now()); order.setUpdateBy(operatorId); order.setUpdateTime(LocalDateTime.now()); purchaseOrderMapper.updateById(order); // ✅ 事务提交后再通知财务,避免事务内远程调用失败导致回滚 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { @Override public void afterCommit() { try { financeClient.createAccountsPayable( FinanceFeignClient.AccountsPayableRequest.builder() .purchaseOrderId(order.getId()) .purchaseOrderNo(order.getOrderNo()) .supplierId(order.getSupplierId()) .amount(order.getTotalWithTax()) .paymentMethod(order.getPaymentMethod()) .build()); } catch (Exception e) { log.error("通知财务创建应付账款失败: orderNo={}, error={}", order.getOrderNo(), e.getMessage()); // 可选:发送告警通知人工介入 } } }); return order; } ``` #### 方案B:为FeignClient添加Fallback ```java @FeignClient(name = "finance-service", url = "${finance-service.url:}", fallbackFactory = FinanceFeignClientFallbackFactory.class) public interface FinanceFeignClient { ... } ``` --- ## 三、数据库死锁风险 ### ✅ 未发现明显死锁风险 **扫描范围:** 所有 `@Transactional` 方法中的多表操作 | 检查项 | 结果 | |--------|------| | 跨表显式 `SELECT ... FOR UPDATE` | 未发现(无悲观锁查询) | | 多表按不同顺序访问 | 未发现跨服务多表锁场景 | | `@Async` + `@Transactional` 混用 | 未发现 | ### ⚠️ 轻量级风险(库存并发) **问题文件:** `inventory-service/.../StockCheckServiceImpl.java` ```java @Transactional public Stock lockStock(String skuCode, Long warehouseId, Integer quantity, Long orderId) { Stock stock = getOrCreateStock(skuCode, warehouseId); // SELECT if (stock.getAvailableQuantity() < quantity) { throw new RuntimeException("可用库存不足"); } stock.setLockedQuantity(stock.getLockedQuantity() + quantity); stockRepository.updateById(stock); // UPDATE — 无锁 } ``` **风险:** 两个并发请求同时锁定同一SKU,第一个请求的更新可能被第二个覆盖(check-then-act竞态)。 **现状:** `Stock.version` 字段存在但**未标注 `@Version`**,乐观锁未生效。 **修复方案:** 添加 `@Version` 注解: ```java // Stock.java @Version private Integer version; ``` MyBatis-Plus 会自动在 UPDATE 时追加 `AND version = ?`,更新失败时抛出 `OptimisticLockingFailureException`,触发事务回滚。 --- ## 四、订单状态机检查 ### ✅ 状态机设计合理,未发现问题 **已验证文件:** - `order-service/.../OrderStateMachine.java` - `aftersale-service/.../AfterSaleStatusMachine.java` | 验证项 | 结果 | |--------|------| | 所有状态转换均通过 `canTransition()` 校验 | ✅ | | 终态(completed/cancelled/closed)不可再流转 | ✅ | | 审核通过后自动设置 `auditStatus=approved` | ✅ | | `canAudit/canShip/canComplete/canCancel` 边界检查 | ✅ | **状态流转图(Order):** ``` pending → auditing → shipped → completed ↓ ↓ ↓ cancelled cancelled cancelled ``` **状态流转图(AfterSale):** ``` pending → approved → processing → received → completed → closed ↓ ↓ rejected rejected → pending (重新开启) ``` --- ## 五、空指针风险(NPE) ### ⚠️ 发现2处潜在NPE,其他已有防护 #### 问题 5.1:`PurchaseStatisticsServiceImpl.getSupplierStats()` **文件:** `purchase-service/.../PurchaseStatisticsServiceImpl.java:127` ```java // 从Map按supplierId分组,理论上不可能为空 List supplierOrders = entry.getValue(); String supplierName = supplierOrders.get(0).getSupplierName(); // ⚠️ supplierOrders理论上非空 // 但get(0)依赖列表非空 // 且getSupplierName()可能返回null ``` **实际情况:** `entry.getValue()` 来自 `Collectors.groupingBy()`,key存在则value必为非空List,但 `getSupplierName()` 可能在数据库中为NULL。 **修复:** 防御性处理 ```java String supplierName = supplierOrders.stream() .map(PurchaseOrder::getSupplierName) .filter(Objects::nonNull) .findFirst() .orElse("未知供应商"); ``` #### 问题 5.2:`OrderSettlementServiceImpl` 返回 `.get(0)` **文件:** `order-service/.../OrderSettlementServiceImpl.java:530` ```java return stats.getShopStatistics().get(0); // ⚠️ 列表可能为空 ``` **修复:** ```java List shopStats = stats.getShopStatistics(); if (shopStats == null || shopStats.isEmpty()) { return null; // 或返回空VO } return shopStats.get(0); ``` ### ✅ 已正确防护的位置 | 位置 | 防护方式 | |------|----------| | `ai-service/AIChatServiceImpl:210` | `if (choices != null && !choices.isEmpty())` | | `logistics-service/LogisticsService:56` | `if (traces.isEmpty())` → return null | | `approval-flow-service/AuditRuleServiceImpl:417` | `if (!ruleActions.isEmpty())` | | `system-tool-service/DataConsistencyService:127` | 三元运算符 `isEmpty() ? ... : ...` | --- ## 六、汇总 | 类别 | 风险等级 | 发现数量 | 已修复 | |------|----------|----------|--------| | 循环依赖 | ✅ 无风险 | 0 | — | | 事务边界+Feign | 🔴 高风险 | 5处 | 需要修复 | | 数据库死锁 | 🟡 中风险 | 1处 | 需要修复 | | 订单状态机 | ✅ 无风险 | 0 | — | | 空指针风险 | 🟡 中风险 | 2处 | 需要修复 | --- ## 七、修复优先级 | 优先级 | 修复项 | 所属问题 | |--------|--------|----------| | P0 | `PurchaseOrderServiceImpl` Feign调用移出事务 | 问题2.1、2.2 | | P0 | `PurchaseReturnServiceImpl` Feign调用移出事务 | 问题2.3 | | P0 | `Stock.version` 添加 `@Version` | 问题三 | | P1 | `PurchaseStatisticsServiceImpl` NPE防护 | 问题5.1 | | P1 | `OrderSettlementServiceImpl` NPE防护 | 问题5.2 | | P2 | 为 `FinanceFeignClient` 添加Fallback | 问题2.5 | | P2 | 为 `WarehouseFeignClient` 添加Fallback | 问题2.5 |