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

312 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 逻辑错误修复报告
**任务编号:** 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.5FeignClient无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<PurchaseOrder> 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<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 |