erp-java/docs/逻辑错误修复报告.md

11 KiB
Raw Blame History

逻辑错误修复报告

任务编号: 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.1approve() 方法 — 本地事务先提交,远程通知后失败

// 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.2cancel() 方法 — 同样问题

// 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.3PurchaseReturnServiceImpl — 确认退货时调用仓库

// PurchaseReturnServiceImpl.java:137
@Transactional
public void confirmReturn(...) {
    // 本地更新退货单状态
    purchaseReturnMapper.updateById(ret);

    // 调用仓库创建出库单
    WarehouseFeignClient.OutboundResponse resp = warehouseClient.createOutbound(...);
    // ❌ 如果这里失败,退货单已更新但出库单未创建
}

问题 2.4PurchaseInboundServiceImpl — 入库时调用仓库

// PurchaseInboundServiceImpl.java:168
WarehouseFeignClient.InboundResponse resp = warehouseClient.createInbound(warehouseRequest, operatorId);

问题 2.5FeignClient无Fallback降级

FinanceFeignClientWarehouseFeignClient 均未配置 fallbackfallbackFactory,服务不可用时直接抛出异常。

修复方案

核心原则Feign远程调用必须放在事务之外或者在事务提交之后执行

方案A推荐使用 TransactionSynchronizationManager 在事务提交后执行

@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

@FeignClient(name = "finance-service",
             url = "${finance-service.url:}",
             fallbackFactory = FinanceFeignClientFallbackFactory.class)
public interface FinanceFeignClient { ... }

三、数据库死锁风险

未发现明显死锁风险

扫描范围: 所有 @Transactional 方法中的多表操作

检查项 结果
跨表显式 SELECT ... FOR UPDATE 未发现(无悲观锁查询)
多表按不同顺序访问 未发现跨服务多表锁场景
@Async + @Transactional 混用 未发现

⚠️ 轻量级风险(库存并发)

问题文件: inventory-service/.../StockCheckServiceImpl.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 注解:

// 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.1PurchaseStatisticsServiceImpl.getSupplierStats()

文件: purchase-service/.../PurchaseStatisticsServiceImpl.java:127

// 从Map按supplierId分组理论上不可能为空
List<PurchaseOrder> supplierOrders = entry.getValue();
String supplierName = supplierOrders.get(0).getSupplierName(); // ⚠️ supplierOrders理论上非空
                                                                       // 但get(0)依赖列表非空
                                                                       // 且getSupplierName()可能返回null

实际情况: entry.getValue() 来自 Collectors.groupingBy()key存在则value必为非空ListgetSupplierName() 可能在数据库中为NULL。

修复: 防御性处理

String supplierName = supplierOrders.stream()
    .map(PurchaseOrder::getSupplierName)
    .filter(Objects::nonNull)
    .findFirst()
    .orElse("未知供应商");

问题 5.2OrderSettlementServiceImpl 返回 .get(0)

文件: order-service/.../OrderSettlementServiceImpl.java:530

return stats.getShopStatistics().get(0);  // ⚠️ 列表可能为空

修复:

List<ShopSettlementVO> 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