Bladeren bron

feat: 业务逻辑深度强化+枚举消除魔法值+状态机校验

核心改进:
- InventoryService: 锁定/解锁校验、盘点、仓间调拨
- ShippingOrderService: 发货单状态机(PENDING_PICK→PENDING_SHIP→SHIPPED→DELIVERED)
- AfterSaleService: 退款状态机校验、真正创建补发订单
- FinancePaymentService/FinanceRefundService: 防重复处理校验
- ReplenishmentPlanService: 正确保存采购单明细项
- PricingRuleService: 规则有效期校验
- PurchaseArrivalService: 到货确认防重复校验

枚举消除魔法值:
- 新增 ReturnPackageStatus、HandleResult、TicketStatus、InvoiceStatus、ChannelSyncStatus
- 扩展 AuditStatus(PROCESSED)、GenericStatus(ENABLED/DISABLED)
- 所有Service层状态字符串改为枚举引用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Jax Docker 2 maanden geleden
bovenliggende
commit
9ff1ef8540
100 gewijzigde bestanden met toevoegingen van 2467 en 376 verwijderingen
  1. 12 8
      backend/src/main/java/com/oms/controller/AfterSaleController.java
  2. 18 0
      backend/src/main/java/com/oms/controller/InventoryController.java
  3. 16 8
      backend/src/main/java/com/oms/controller/OrdersController.java
  4. 30 0
      backend/src/main/java/com/oms/controller/PurchaseOrderController.java
  5. 2 2
      backend/src/main/java/com/oms/controller/ReturnPackageController.java
  6. 7 2
      backend/src/main/java/com/oms/controller/SupplierSettlementController.java
  7. 8 0
      backend/src/main/java/com/oms/dto/ApproveAfterSaleRequest.java
  8. 9 0
      backend/src/main/java/com/oms/dto/AssignHandlerRequest.java
  9. 8 0
      backend/src/main/java/com/oms/dto/AssignWarehouseRequest.java
  10. 8 0
      backend/src/main/java/com/oms/dto/CancelOrderRequest.java
  11. 11 0
      backend/src/main/java/com/oms/dto/ConfirmPaymentRequest.java
  12. 6 5
      backend/src/main/java/com/oms/dto/OrderListDTO.java
  13. 6 5
      backend/src/main/java/com/oms/dto/OrdersDTO.java
  14. 8 0
      backend/src/main/java/com/oms/dto/RejectAfterSaleRequest.java
  15. 10 0
      backend/src/main/java/com/oms/dto/ResendOrderRequest.java
  16. 9 0
      backend/src/main/java/com/oms/dto/ReturnTrackingRequest.java
  17. 2 1
      backend/src/main/java/com/oms/dto/ShippingOrderDTO.java
  18. 4 2
      backend/src/main/java/com/oms/entity/AfterSale.java
  19. 4 2
      backend/src/main/java/com/oms/entity/ChannelMapping.java
  20. 2 1
      backend/src/main/java/com/oms/entity/FinancePayment.java
  21. 2 1
      backend/src/main/java/com/oms/entity/FinanceRefund.java
  22. 2 1
      backend/src/main/java/com/oms/entity/Inventory.java
  23. 2 1
      backend/src/main/java/com/oms/entity/Invoice.java
  24. 2 1
      backend/src/main/java/com/oms/entity/Iqc.java
  25. 2 1
      backend/src/main/java/com/oms/entity/LogisticsProvider.java
  26. 6 5
      backend/src/main/java/com/oms/entity/Orders.java
  27. 4 2
      backend/src/main/java/com/oms/entity/Product.java
  28. 2 1
      backend/src/main/java/com/oms/entity/ProductCategory.java
  29. 2 1
      backend/src/main/java/com/oms/entity/ProductSku.java
  30. 2 1
      backend/src/main/java/com/oms/entity/PurchaseArrivalItem.java
  31. 4 2
      backend/src/main/java/com/oms/entity/PurchaseOrder.java
  32. 4 2
      backend/src/main/java/com/oms/entity/PurchaseRequest.java
  33. 2 1
      backend/src/main/java/com/oms/entity/ReplenishmentPlan.java
  34. 5 0
      backend/src/main/java/com/oms/entity/Report.java
  35. 2 1
      backend/src/main/java/com/oms/entity/ReturnPackage.java
  36. 4 2
      backend/src/main/java/com/oms/entity/ShippingOrder.java
  37. 2 1
      backend/src/main/java/com/oms/entity/ShippingTemplate.java
  38. 2 1
      backend/src/main/java/com/oms/entity/Supplier.java
  39. 2 1
      backend/src/main/java/com/oms/entity/SupplierSettlement.java
  40. 2 1
      backend/src/main/java/com/oms/entity/SupplyCapability.java
  41. 2 1
      backend/src/main/java/com/oms/entity/SysApiKey.java
  42. 2 1
      backend/src/main/java/com/oms/entity/SysDepartment.java
  43. 2 1
      backend/src/main/java/com/oms/entity/SysEmployee.java
  44. 2 1
      backend/src/main/java/com/oms/entity/SysRole.java
  45. 2 1
      backend/src/main/java/com/oms/entity/SysUser.java
  46. 2 1
      backend/src/main/java/com/oms/entity/Warehouse.java
  47. 25 0
      backend/src/main/java/com/oms/enums/AlertStatus.java
  48. 22 0
      backend/src/main/java/com/oms/enums/ArrivalStatus.java
  49. 23 0
      backend/src/main/java/com/oms/enums/AuditStatus.java
  50. 23 0
      backend/src/main/java/com/oms/enums/ChannelSyncStatus.java
  51. 22 0
      backend/src/main/java/com/oms/enums/ChatStatus.java
  52. 23 0
      backend/src/main/java/com/oms/enums/CouponStatus.java
  53. 29 0
      backend/src/main/java/com/oms/enums/GenericStatus.java
  54. 22 0
      backend/src/main/java/com/oms/enums/HandleResult.java
  55. 21 0
      backend/src/main/java/com/oms/enums/InventoryWarningStatus.java
  56. 22 0
      backend/src/main/java/com/oms/enums/InvoiceStatus.java
  57. 22 0
      backend/src/main/java/com/oms/enums/MappingStatus.java
  58. 24 8
      backend/src/main/java/com/oms/enums/OrderStatus.java
  59. 20 4
      backend/src/main/java/com/oms/enums/PaymentStatus.java
  60. 23 0
      backend/src/main/java/com/oms/enums/Priority.java
  61. 22 0
      backend/src/main/java/com/oms/enums/ProductStatus.java
  62. 29 0
      backend/src/main/java/com/oms/enums/PurchaseOrderStatus.java
  63. 22 0
      backend/src/main/java/com/oms/enums/ReconcileStatus.java
  64. 25 0
      backend/src/main/java/com/oms/enums/RefundStatus.java
  65. 21 0
      backend/src/main/java/com/oms/enums/ReportStatus.java
  66. 23 0
      backend/src/main/java/com/oms/enums/ReturnPackageStatus.java
  67. 22 0
      backend/src/main/java/com/oms/enums/ReturnStatus.java
  68. 22 0
      backend/src/main/java/com/oms/enums/SettlementStatus.java
  69. 23 6
      backend/src/main/java/com/oms/enums/ShippingStatus.java
  70. 24 0
      backend/src/main/java/com/oms/enums/TicketStatus.java
  71. 22 0
      backend/src/main/java/com/oms/enums/TokenStatus.java
  72. 22 0
      backend/src/main/java/com/oms/enums/ValidateStatus.java
  73. 78 17
      backend/src/main/java/com/oms/service/AfterSaleService.java
  74. 5 3
      backend/src/main/java/com/oms/service/AutoReplyRuleService.java
  75. 7 5
      backend/src/main/java/com/oms/service/ChannelMappingService.java
  76. 8 3
      backend/src/main/java/com/oms/service/ChannelService.java
  77. 7 6
      backend/src/main/java/com/oms/service/ChatSessionService.java
  78. 9 8
      backend/src/main/java/com/oms/service/CouponService.java
  79. 11 7
      backend/src/main/java/com/oms/service/DashboardService.java
  80. 23 2
      backend/src/main/java/com/oms/service/FinancePaymentService.java
  81. 21 1
      backend/src/main/java/com/oms/service/FinanceRefundService.java
  82. 104 1
      backend/src/main/java/com/oms/service/InventoryService.java
  83. 5 4
      backend/src/main/java/com/oms/service/InventoryTurnoverService.java
  84. 66 24
      backend/src/main/java/com/oms/service/InvoiceService.java
  85. 68 28
      backend/src/main/java/com/oms/service/IqcService.java
  86. 6 5
      backend/src/main/java/com/oms/service/KnowledgeBaseService.java
  87. 201 43
      backend/src/main/java/com/oms/service/OrdersService.java
  88. 17 16
      backend/src/main/java/com/oms/service/PriceWatchService.java
  89. 10 4
      backend/src/main/java/com/oms/service/PricingRuleService.java
  90. 5 3
      backend/src/main/java/com/oms/service/ProductService.java
  91. 8 7
      backend/src/main/java/com/oms/service/PromotionService.java
  92. 98 5
      backend/src/main/java/com/oms/service/PurchaseArrivalService.java
  93. 200 6
      backend/src/main/java/com/oms/service/PurchaseOrderService.java
  94. 86 24
      backend/src/main/java/com/oms/service/PurchaseRequestService.java
  95. 129 9
      backend/src/main/java/com/oms/service/ReplenishmentPlanService.java
  96. 201 11
      backend/src/main/java/com/oms/service/ReportService.java
  97. 67 24
      backend/src/main/java/com/oms/service/ReturnPackageService.java
  98. 69 8
      backend/src/main/java/com/oms/service/ShippingOrderService.java
  99. 104 8
      backend/src/main/java/com/oms/service/SupplierSettlementService.java
  100. 9 7
      backend/src/main/java/com/oms/service/SysApprovalFlowService.java

+ 12 - 8
backend/src/main/java/com/oms/controller/AfterSaleController.java

@@ -2,7 +2,11 @@ package com.oms.controller;
 
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.dto.AfterSaleDTO;
+import com.oms.dto.ApproveAfterSaleRequest;
 import com.oms.dto.PageResponse;
+import com.oms.dto.RejectAfterSaleRequest;
+import com.oms.dto.ResendOrderRequest;
+import com.oms.dto.ReturnTrackingRequest;
 import com.oms.entity.AfterSale;
 import com.oms.service.AfterSaleService;
 import lombok.RequiredArgsConstructor;
@@ -57,13 +61,13 @@ public class AfterSaleController {
     }
 
     @PostMapping("/{id}/approve")
-    public void approve(@PathVariable Long id, @RequestParam(required = false) String remark) {
-        afterSaleService.approve(id, remark, "SYSTEM");
+    public void approve(@PathVariable Long id, @RequestBody(required = false) ApproveAfterSaleRequest req) {
+        afterSaleService.approve(id, req != null ? req.getRemark() : null, "SYSTEM");
     }
 
     @PostMapping("/{id}/reject")
-    public void reject(@PathVariable Long id, @RequestParam String reason) {
-        afterSaleService.reject(id, reason, "SYSTEM");
+    public void reject(@PathVariable Long id, @RequestBody RejectAfterSaleRequest req) {
+        afterSaleService.reject(id, req.getReason(), "SYSTEM");
     }
 
     @PostMapping("/{id}/process-refund")
@@ -72,8 +76,8 @@ public class AfterSaleController {
     }
 
     @PostMapping("/{id}/submit-return-tracking")
-    public void submitReturnTracking(@PathVariable Long id, @RequestParam String carrier, @RequestParam String trackingNo) {
-        afterSaleService.submitReturnTracking(id, carrier, trackingNo, "SYSTEM");
+    public void submitReturnTracking(@PathVariable Long id, @RequestBody ReturnTrackingRequest req) {
+        afterSaleService.submitReturnTracking(id, req.getCarrier(), req.getTrackingNo(), "SYSTEM");
     }
 
     @PostMapping("/{id}/confirm-receipt")
@@ -82,7 +86,7 @@ public class AfterSaleController {
     }
 
     @PostMapping("/{id}/create-resend")
-    public void createResendOrder(@PathVariable Long id, @RequestParam Long warehouseId, @RequestParam Long skuId, @RequestParam int qty) {
-        afterSaleService.createResendOrder(id, warehouseId, skuId, qty, "SYSTEM");
+    public void createResendOrder(@PathVariable Long id, @RequestBody ResendOrderRequest req) {
+        afterSaleService.createResendOrder(id, req.getWarehouseId(), req.getSkuId(), req.getQty(), "SYSTEM");
     }
 }

+ 18 - 0
backend/src/main/java/com/oms/controller/InventoryController.java

@@ -86,4 +86,22 @@ public class InventoryController {
     public void delete(@PathVariable Long id) {
         inventoryService.delete(id);
     }
+
+    @PostMapping("/{id}/stocktake")
+    public void stocktake(
+            @PathVariable Long id,
+            @RequestParam int actualQty,
+            @RequestParam String operator) {
+        inventoryService.stocktake(id, actualQty, operator);
+    }
+
+    @PostMapping("/transfer")
+    public void transfer(
+            @RequestParam Long skuId,
+            @RequestParam Long fromWarehouseId,
+            @RequestParam Long toWarehouseId,
+            @RequestParam int qty,
+            @RequestParam String operator) {
+        inventoryService.transfer(skuId, fromWarehouseId, toWarehouseId, qty, operator);
+    }
 }

+ 16 - 8
backend/src/main/java/com/oms/controller/OrdersController.java

@@ -3,6 +3,10 @@ package com.oms.controller;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.OrderItemConverter;
 import com.oms.converter.OrdersConverter;
+import com.oms.dto.AssignHandlerRequest;
+import com.oms.dto.AssignWarehouseRequest;
+import com.oms.dto.CancelOrderRequest;
+import com.oms.dto.ConfirmPaymentRequest;
 import com.oms.dto.OrderFilterDTO;
 import com.oms.dto.OrderItemDTO;
 import com.oms.dto.OrderListDTO;
@@ -89,8 +93,12 @@ public class OrdersController {
     }
 
     @PostMapping("/{id}/confirm-payment")
-    public void confirmPayment(@PathVariable Long id) {
-        ordersService.confirmPayment(id, "SYSTEM");
+    public void confirmPayment(@PathVariable Long id, @RequestBody(required = false) ConfirmPaymentRequest req) {
+        if (req != null && req.getPaidAmount() != null) {
+            ordersService.confirmPayment(id, req.getPaidAmount(), req.getTransactionId(), "SYSTEM");
+        } else {
+            ordersService.confirmPayment(id, "SYSTEM");
+        }
     }
 
     @PostMapping("/{id}/allocate")
@@ -99,8 +107,8 @@ public class OrdersController {
     }
 
     @PostMapping("/{id}/cancel")
-    public void cancelOrder(@PathVariable Long id, @RequestParam String reason) {
-        ordersService.cancelOrder(id, reason, "SYSTEM");
+    public void cancelOrder(@PathVariable Long id, @RequestBody CancelOrderRequest req) {
+        ordersService.cancelOrder(id, req.getReason(), "SYSTEM");
     }
 
     @PostMapping("/{id}/confirm-shipped")
@@ -129,13 +137,13 @@ public class OrdersController {
     }
 
     @PutMapping("/{id}/handler")
-    public void assignHandler(@PathVariable Long id, @RequestParam Long handlerId, @RequestParam String handlerName) {
-        ordersService.assignHandler(id, handlerId, handlerName);
+    public void assignHandler(@PathVariable Long id, @RequestBody AssignHandlerRequest req) {
+        ordersService.assignHandler(id, req.getHandlerId(), req.getHandlerName());
     }
 
     @PutMapping("/{id}/warehouse")
-    public void assignWarehouse(@PathVariable Long id, @RequestParam Long warehouseId) {
-        ordersService.assignWarehouse(id, warehouseId);
+    public void assignWarehouse(@PathVariable Long id, @RequestBody AssignWarehouseRequest req) {
+        ordersService.assignWarehouse(id, req.getWarehouseId());
     }
 
     @PostMapping("/sync-random")

+ 30 - 0
backend/src/main/java/com/oms/controller/PurchaseOrderController.java

@@ -57,4 +57,34 @@ public class PurchaseOrderController {
     public void submit(@PathVariable Long id) {
         purchaseOrderService.submit(id);
     }
+
+    @PostMapping("/{id}/approve")
+    public void approve(@PathVariable Long id) {
+        purchaseOrderService.approve(id);
+    }
+
+    @PostMapping("/{id}/reject")
+    public void reject(@PathVariable Long id, @RequestParam(required = false) String reason) {
+        purchaseOrderService.reject(id, reason);
+    }
+
+    @PostMapping("/{id}/place-order")
+    public void placeOrder(@PathVariable Long id) {
+        purchaseOrderService.placeOrder(id);
+    }
+
+    @PostMapping("/{id}/cancel")
+    public void cancel(@PathVariable Long id, @RequestParam(required = false) String reason) {
+        purchaseOrderService.cancel(id, reason);
+    }
+
+    @PostMapping("/{id}/close")
+    public void close(@PathVariable Long id) {
+        purchaseOrderService.close(id);
+    }
+
+    @PostMapping("/{id}/complete")
+    public void complete(@PathVariable Long id) {
+        purchaseOrderService.complete(id);
+    }
 }

+ 2 - 2
backend/src/main/java/com/oms/controller/ReturnPackageController.java

@@ -54,8 +54,8 @@ public class ReturnPackageController {
     }
 
     @PostMapping("/{id}/receive")
-    public void receive(@PathVariable Long id) {
-        returnPackageService.receive(id);
+    public void receive(@PathVariable Long id, @RequestParam(defaultValue = "system") String handler) {
+        returnPackageService.receive(id, handler);
     }
 
     @PostMapping("/{id}/confirm-inspection")

+ 7 - 2
backend/src/main/java/com/oms/controller/SupplierSettlementController.java

@@ -55,11 +55,16 @@ public class SupplierSettlementController {
 
     @PostMapping("/{id}/settle")
     public void settle(@PathVariable Long id) {
-        supplierSettlementService.settle(id);
+        supplierSettlementService.settle(id, "system");
+    }
+
+    @PostMapping("/{id}/partial-settle")
+    public void partialSettle(@PathVariable Long id, @RequestParam java.math.BigDecimal amount) {
+        supplierSettlementService.partialSettle(id, amount, "system");
     }
 
     @PostMapping("/generate")
     public Long generateSettlement(@RequestParam Long supplierId, @RequestParam String period) {
-        return supplierSettlementService.generateSettlement(supplierId, period);
+        return supplierSettlementService.generateSettlement(supplierId, period, null, null);
     }
 }

+ 8 - 0
backend/src/main/java/com/oms/dto/ApproveAfterSaleRequest.java

@@ -0,0 +1,8 @@
+package com.oms.dto;
+
+import lombok.Data;
+
+@Data
+public class ApproveAfterSaleRequest {
+    private String remark;
+}

+ 9 - 0
backend/src/main/java/com/oms/dto/AssignHandlerRequest.java

@@ -0,0 +1,9 @@
+package com.oms.dto;
+
+import lombok.Data;
+
+@Data
+public class AssignHandlerRequest {
+    private Long handlerId;
+    private String handlerName;
+}

+ 8 - 0
backend/src/main/java/com/oms/dto/AssignWarehouseRequest.java

@@ -0,0 +1,8 @@
+package com.oms.dto;
+
+import lombok.Data;
+
+@Data
+public class AssignWarehouseRequest {
+    private Long warehouseId;
+}

+ 8 - 0
backend/src/main/java/com/oms/dto/CancelOrderRequest.java

@@ -0,0 +1,8 @@
+package com.oms.dto;
+
+import lombok.Data;
+
+@Data
+public class CancelOrderRequest {
+    private String reason;
+}

+ 11 - 0
backend/src/main/java/com/oms/dto/ConfirmPaymentRequest.java

@@ -0,0 +1,11 @@
+package com.oms.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+public class ConfirmPaymentRequest {
+    private BigDecimal paidAmount;
+    private String transactionId;
+}

+ 6 - 5
backend/src/main/java/com/oms/dto/OrderListDTO.java

@@ -1,5 +1,6 @@
 package com.oms.dto;
 
+import com.oms.enums.*;
 import lombok.Data;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
@@ -13,12 +14,12 @@ public class OrderListDTO {
     private Long channelId;
     private String channel;
     private String channelName;  // 添加: 渠道名称
-    private String orderStatus;
-    private String shippingStatus;
-    private String paymentStatus;
-    private String refundStatus; // 添加: 退款状态
+    private OrderStatus orderStatus;
+    private ShippingStatus shippingStatus;
+    private PaymentStatus paymentStatus;
+    private RefundStatus refundStatus; // 添加: 退款状态
     private String exceptionTag;
-    private String priority;
+    private Priority priority;
     private String buyer;
     private String buyerEmail;
     private String buyerPhone;

+ 6 - 5
backend/src/main/java/com/oms/dto/OrdersDTO.java

@@ -1,5 +1,6 @@
 package com.oms.dto;
 
+import com.oms.enums.*;
 import lombok.Data;
 import java.math.BigDecimal;
 import java.time.LocalDate;
@@ -12,12 +13,12 @@ public class OrdersDTO {
     private String orderNo;
     private String channelOrderNo;
     private Long channelId;
-    private String orderStatus;
-    private String shippingStatus;
-    private String paymentStatus;
-    private String refundStatus;
+    private OrderStatus orderStatus;
+    private ShippingStatus shippingStatus;
+    private PaymentStatus paymentStatus;
+    private RefundStatus refundStatus;
     private String exceptionTag;
-    private String priority;
+    private Priority priority;
     private LocalDateTime paidAt;
     private LocalDateTime shippedAt;
     private LocalDateTime deliveredAt;

+ 8 - 0
backend/src/main/java/com/oms/dto/RejectAfterSaleRequest.java

@@ -0,0 +1,8 @@
+package com.oms.dto;
+
+import lombok.Data;
+
+@Data
+public class RejectAfterSaleRequest {
+    private String reason;
+}

+ 10 - 0
backend/src/main/java/com/oms/dto/ResendOrderRequest.java

@@ -0,0 +1,10 @@
+package com.oms.dto;
+
+import lombok.Data;
+
+@Data
+public class ResendOrderRequest {
+    private Long warehouseId;
+    private Long skuId;
+    private int qty;
+}

+ 9 - 0
backend/src/main/java/com/oms/dto/ReturnTrackingRequest.java

@@ -0,0 +1,9 @@
+package com.oms.dto;
+
+import lombok.Data;
+
+@Data
+public class ReturnTrackingRequest {
+    private String carrier;
+    private String trackingNo;
+}

+ 2 - 1
backend/src/main/java/com/oms/dto/ShippingOrderDTO.java

@@ -1,5 +1,6 @@
 package com.oms.dto;
 
+import com.oms.enums.ShippingStatus;
 import lombok.Data;
 import java.time.LocalDateTime;
 
@@ -17,7 +18,7 @@ public class ShippingOrderDTO {
     private Long carrierId;
     private String carrierName;
     private String trackingNo;
-    private String shippingStatus;
+    private ShippingStatus shippingStatus;
     private String returnStatus;
     private String returnTrackingNo;
     private LocalDateTime shippedAt;

+ 4 - 2
backend/src/main/java/com/oms/entity/AfterSale.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.AuditStatus;
+import com.oms.enums.RefundStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -38,10 +40,10 @@ public class AfterSale {
     private BigDecimal amount = BigDecimal.ZERO;
 
     @TableField("audit_status")
-    private String auditStatus = "PENDING";
+    private String auditStatus = AuditStatus.PENDING.getCode();
 
     @TableField("refund_status")
-    private String refundStatus = "NONE";
+    private String refundStatus = RefundStatus.NONE.getCode();
 
     @TableField("reason")
     private String reason;

+ 4 - 2
backend/src/main/java/com/oms/entity/ChannelMapping.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.MappingStatus;
+import com.oms.enums.ValidateStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -38,10 +40,10 @@ public class ChannelMapping {
     private String channelCategory;
 
     @TableField("mapping_status")
-    private String mappingStatus = "PENDING";
+    private String mappingStatus = MappingStatus.PENDING.getCode();
 
     @TableField("validate_status")
-    private String validateStatus = "PENDING";
+    private String validateStatus = ValidateStatus.PENDING.getCode();
 
     @TableField("last_sync_at")
     private LocalDateTime lastSyncAt;

+ 2 - 1
backend/src/main/java/com/oms/entity/FinancePayment.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.ReconcileStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -50,7 +51,7 @@ public class FinancePayment {
     private String transactionNo;
 
     @TableField("reconcile_status")
-    private String reconcileStatus = "PENDING";
+    private String reconcileStatus = ReconcileStatus.PENDING.getCode();
 
     @TableField("reconcile_time")
     private LocalDateTime reconcileTime;

+ 2 - 1
backend/src/main/java/com/oms/entity/FinanceRefund.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.RefundStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -41,7 +42,7 @@ public class FinanceRefund {
     private LocalDateTime refundTime;
 
     @TableField("refund_status")
-    private String refundStatus = "PENDING";
+    private String refundStatus = RefundStatus.PENDING.getCode();
 
     @TableField("channel_refund_no")
     private String channelRefundNo;

+ 2 - 1
backend/src/main/java/com/oms/entity/Inventory.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.InventoryWarningStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -37,7 +38,7 @@ public class Inventory {
     private Integer safeStock = 0;
 
     @TableField("warning_status")
-    private String warningStatus = "NORMAL";
+    private String warningStatus = InventoryWarningStatus.NORMAL.getCode();
 
     @TableField("last_change_at")
     private LocalDateTime lastChangeAt;

+ 2 - 1
backend/src/main/java/com/oms/entity/Invoice.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -48,7 +49,7 @@ public class Invoice {
     private LocalDate invoiceDate;
 
     @TableField("status")
-    private String status = "DRAFT";
+    private String status = GenericStatus.DRAFT.getCode();
 
     @TableField("remark")
     private String remark;

+ 2 - 1
backend/src/main/java/com/oms/entity/Iqc.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.AuditStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -37,7 +38,7 @@ public class Iqc {
     private Integer unqualifiedQty = 0;
 
     @TableField("result")
-    private String result = "PENDING";
+    private String result = AuditStatus.PENDING.getCode();
 
     @TableField("inspector_id")
     private Long inspectorId;

+ 2 - 1
backend/src/main/java/com/oms/entity/LogisticsProvider.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -34,7 +35,7 @@ public class LogisticsProvider {
     private String phone;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("tenant_id")
     private String tenantId = "DEFAULT";

+ 6 - 5
backend/src/main/java/com/oms/entity/Orders.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.*;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -27,22 +28,22 @@ public class Orders {
     private Long channelId;
 
     @TableField("order_status")
-    private String orderStatus = "CREATED";
+    private OrderStatus orderStatus = OrderStatus.CREATED;
 
     @TableField("shipping_status")
-    private String shippingStatus = "UNSHIPPED";
+    private ShippingStatus shippingStatus = ShippingStatus.UNSHIPPED;
 
     @TableField("payment_status")
-    private String paymentStatus = "UNPAID";
+    private PaymentStatus paymentStatus = PaymentStatus.UNPAID;
 
     @TableField("refund_status")
-    private String refundStatus = "NONE";
+    private RefundStatus refundStatus = RefundStatus.NONE;
 
     @TableField("exception_tag")
     private String exceptionTag;
 
     @TableField("priority")
-    private String priority = "NORMAL";
+    private Priority priority = Priority.NORMAL;
 
     @TableField("paid_at")
     private LocalDateTime paidAt;

+ 4 - 2
backend/src/main/java/com/oms/entity/Product.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
+import com.oms.enums.ProductStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -44,10 +46,10 @@ public class Product {
     private String translations;
 
     @TableField("channel_status")
-    private String channelStatus = "INACTIVE";
+    private String channelStatus = GenericStatus.INACTIVE.getCode();
 
     @TableField("status")
-    private String status = "DRAFT";
+    private String status = ProductStatus.DRAFT.getCode();
 
     @TableField("owner")
     private String owner;

+ 2 - 1
backend/src/main/java/com/oms/entity/ProductCategory.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -31,7 +32,7 @@ public class ProductCategory {
     private Integer sortOrder = 0;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("tenant_id")
     private String tenantId = "DEFAULT";

+ 2 - 1
backend/src/main/java/com/oms/entity/ProductSku.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -47,7 +48,7 @@ public class ProductSku {
     private BigDecimal height;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("tenant_id")
     private String tenantId = "DEFAULT";

+ 2 - 1
backend/src/main/java/com/oms/entity/PurchaseArrivalItem.java

@@ -3,6 +3,7 @@ package com.oms.entity;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.AuditStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -36,7 +37,7 @@ public class PurchaseArrivalItem {
     private Integer unqualifiedQty = 0;
 
     @TableField("iqc_status")
-    private String iqcStatus = "PENDING";
+    private String iqcStatus = AuditStatus.PENDING.getCode();
 
     @TableField("tenant_id")
     private String tenantId = "DEFAULT";

+ 4 - 2
backend/src/main/java/com/oms/entity/PurchaseOrder.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
+import com.oms.enums.PurchaseOrderStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -45,10 +47,10 @@ public class PurchaseOrder {
     private LocalDate expectedDate;
 
     @TableField("arrival_progress")
-    private String arrivalProgress = "PENDING";
+    private String arrivalProgress = GenericStatus.PENDING.getCode();
 
     @TableField("status")
-    private String status = "DRAFT";
+    private String status = PurchaseOrderStatus.DRAFT.getCode();
 
     @TableField("remark")
     private String remark;

+ 4 - 2
backend/src/main/java/com/oms/entity/PurchaseRequest.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.AuditStatus;
+import com.oms.enums.Priority;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -37,10 +39,10 @@ public class PurchaseRequest {
     private String reason;
 
     @TableField("urgency")
-    private String urgency = "NORMAL";
+    private String urgency = Priority.NORMAL.getCode();
 
     @TableField("status")
-    private String status = "PENDING";
+    private String status = AuditStatus.PENDING.getCode();
 
     @TableField("related_order_id")
     private Long relatedOrderId;

+ 2 - 1
backend/src/main/java/com/oms/entity/ReplenishmentPlan.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -47,7 +48,7 @@ public class ReplenishmentPlan {
     private String suggestedSupplierName;
 
     @TableField("status")
-    private String status = "PENDING";
+    private String status = GenericStatus.PENDING.getCode();
 
     @TableField("tenant_id")
     private String tenantId = "DEFAULT";

+ 5 - 0
backend/src/main/java/com/oms/entity/Report.java

@@ -1,6 +1,7 @@
 package com.oms.entity;
 
 import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
 import lombok.Data;
@@ -19,6 +20,10 @@ public class Report {
     private LocalDateTime startDate;
     private LocalDateTime endDate;
     private String status;
+
+    @TableField("data")
+    private String data;
+
     private String createdBy;
     private LocalDateTime createdAt;
     private String updatedBy;

+ 2 - 1
backend/src/main/java/com/oms/entity/ReturnPackage.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -25,7 +26,7 @@ public class ReturnPackage {
     private String reason;
 
     @TableField("status")
-    private String status = "PENDING";
+    private String status = GenericStatus.PENDING.getCode();
 
     @TableField("warehouse_id")
     private Long warehouseId;

+ 4 - 2
backend/src/main/java/com/oms/entity/ShippingOrder.java

@@ -4,6 +4,8 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.ReturnStatus;
+import com.oms.enums.ShippingStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -49,10 +51,10 @@ public class ShippingOrder {
     private String trackingNo;
 
     @TableField("shipping_status")
-    private String shippingStatus = "PENDING_PICK";
+    private ShippingStatus shippingStatus = ShippingStatus.PENDING_PICK;
 
     @TableField("return_status")
-    private String returnStatus = "NOT_RETURNED";
+    private String returnStatus = ReturnStatus.NOT_RETURNED.getCode();
 
     @TableField("return_tracking_no")
     private String returnTrackingNo;

+ 2 - 1
backend/src/main/java/com/oms/entity/ShippingTemplate.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -44,7 +45,7 @@ public class ShippingTemplate {
     private BigDecimal remoteSurcharge = BigDecimal.ZERO;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("tenant_id")
     private String tenantId = "DEFAULT";

+ 2 - 1
backend/src/main/java/com/oms/entity/Supplier.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -49,7 +50,7 @@ public class Supplier {
     private String rating;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("related_sku_count")
     private Integer relatedSkuCount = 0;

+ 2 - 1
backend/src/main/java/com/oms/entity/SupplierSettlement.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.SettlementStatus;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -35,7 +36,7 @@ public class SupplierSettlement {
     private BigDecimal paidAmount = BigDecimal.ZERO;
 
     @TableField("status")
-    private String status = "PENDING";
+    private String status = SettlementStatus.PENDING.getCode();
 
     @TableField("paid_at")
     private LocalDateTime paidAt;

+ 2 - 1
backend/src/main/java/com/oms/entity/SupplyCapability.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -40,7 +41,7 @@ public class SupplyCapability {
     private Boolean isDefault = false;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("tenant_id")
     private String tenantId = "DEFAULT";

+ 2 - 1
backend/src/main/java/com/oms/entity/SysApiKey.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -28,7 +29,7 @@ public class SysApiKey {
     private String keyValue;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("expire_at")
     private LocalDateTime expireAt;

+ 2 - 1
backend/src/main/java/com/oms/entity/SysDepartment.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -34,7 +35,7 @@ public class SysDepartment {
     private Integer sortOrder = 0;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("tenant_id")
     private String tenantId = "DEFAULT";

+ 2 - 1
backend/src/main/java/com/oms/entity/SysEmployee.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDate;
@@ -47,7 +48,7 @@ public class SysEmployee {
     private String roleLabel;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("join_date")
     private LocalDate joinDate;

+ 2 - 1
backend/src/main/java/com/oms/entity/SysRole.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -28,7 +29,7 @@ public class SysRole {
     private Integer boundUserCount = 0;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("tenant_id")
     private String tenantId = "DEFAULT";

+ 2 - 1
backend/src/main/java/com/oms/entity/SysUser.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -46,7 +47,7 @@ public class SysUser {
     private Long departmentId;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("last_login_at")
     private LocalDateTime lastLoginAt;

+ 2 - 1
backend/src/main/java/com/oms/entity/Warehouse.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.oms.enums.GenericStatus;
 import lombok.Data;
 
 import java.time.LocalDateTime;
@@ -31,7 +32,7 @@ public class Warehouse {
     private String phone;
 
     @TableField("status")
-    private String status = "ACTIVE";
+    private String status = GenericStatus.ACTIVE.getCode();
 
     @TableField("manager")
     private String manager;

+ 25 - 0
backend/src/main/java/com/oms/enums/AlertStatus.java

@@ -0,0 +1,25 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum AlertStatus {
+    NORMAL("NORMAL", "正常"),
+    ALERT("ALERT", "预警"),
+    CRITICAL("CRITICAL", "严重"),
+    HIGH("HIGH", "偏高"),
+    FAST("FAST", "周转快"),
+    SLOW("SLOW", "周转慢");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    AlertStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/ArrivalStatus.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum ArrivalStatus {
+    PENDING("PENDING", "待到货"),
+    ARRIVED("ARRIVED", "已到货"),
+    PARTIAL("PARTIAL", "部分到货");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    ArrivalStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 23 - 0
backend/src/main/java/com/oms/enums/AuditStatus.java

@@ -0,0 +1,23 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum AuditStatus {
+    PENDING("PENDING", "待审核"),
+    APPROVED("APPROVED", "已通过"),
+    REJECTED("REJECTED", "已拒绝"),
+    PROCESSED("PROCESSED", "已处理");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    AuditStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 23 - 0
backend/src/main/java/com/oms/enums/ChannelSyncStatus.java

@@ -0,0 +1,23 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum ChannelSyncStatus {
+    SYNCING("SYNCING", "同步中"),
+    PAUSED("PAUSED", "已暂停"),
+    ERROR("ERROR", "异常"),
+    IDLE("IDLE", "空闲");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    ChannelSyncStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/ChatStatus.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum ChatStatus {
+    WAITING("WAITING", "等待中"),
+    ASSIGNED("ASSIGNED", "已分配"),
+    CLOSED("CLOSED", "已关闭");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    ChatStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 23 - 0
backend/src/main/java/com/oms/enums/CouponStatus.java

@@ -0,0 +1,23 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum CouponStatus {
+    ACTIVE("ACTIVE", "启用"),
+    DRAFT("DRAFT", "草稿"),
+    INACTIVE("INACTIVE", "停用"),
+    EXHAUSTED("EXHAUSTED", "已用尽");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    CouponStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 29 - 0
backend/src/main/java/com/oms/enums/GenericStatus.java

@@ -0,0 +1,29 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+/**
+ * 通用状态枚举,用于 Warehouse/Supplier/Channel/SysUser/SysEmployee 等实体
+ */
+@Getter
+public enum GenericStatus {
+    ACTIVE("ACTIVE", "启用"),
+    INACTIVE("INACTIVE", "停用"),
+    PENDING("PENDING", "待审核"),
+    DRAFT("DRAFT", "草稿"),
+    LOCKED("LOCKED", "锁定"),
+    ENABLED("ENABLED", "已启用"),
+    DISABLED("DISABLED", "已禁用");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    GenericStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/HandleResult.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum HandleResult {
+    RESTOCK("RESTOCK", "入库"),
+    SCRAP("SCRAP", "报废"),
+    RETURN_SUPPLIER("RETURN_SUPPLIER", "退回供应商");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    HandleResult(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 21 - 0
backend/src/main/java/com/oms/enums/InventoryWarningStatus.java

@@ -0,0 +1,21 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum InventoryWarningStatus {
+    NORMAL("NORMAL", "正常"),
+    LOW_STOCK("LOW_STOCK", "低于安全库存");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    InventoryWarningStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/InvoiceStatus.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum InvoiceStatus {
+    DRAFT("DRAFT", "草稿"),
+    ISSUED("ISSUED", "已开票"),
+    VOID("VOID", "已作废");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    InvoiceStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/MappingStatus.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum MappingStatus {
+    MAPPED("MAPPED", "已映射"),
+    UNMAPPED("UNMAPPED", "未映射"),
+    PENDING("PENDING", "待校验");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    MappingStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 24 - 8
backend/src/main/java/com/oms/enums/OrderStatus.java

@@ -1,11 +1,27 @@
 package com.oms.enums;
 
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
 public enum OrderStatus {
-    CREATED,
-    PAID,
-    ALLOCATED,
-    SHIPPED,
-    DELIVERED,
-    COMPLETED,
-    CANCELLED
-}
+    CREATED("CREATED", "待支付"),
+    PAID("PAID", "已支付"),
+    ALLOCATED("ALLOCATED", "已分配"),
+    SHIPPED("SHIPPED", "已发货"),
+    DELIVERED("DELIVERED", "已签收"),
+    COMPLETED("COMPLETED", "已完成"),
+    CANCELLED("CANCELLED", "已取消"),
+    REFUNDED("REFUNDED", "已退款");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    OrderStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 20 - 4
backend/src/main/java/com/oms/enums/PaymentStatus.java

@@ -1,7 +1,23 @@
 package com.oms.enums;
 
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
 public enum PaymentStatus {
-    UNPAID,
-    PENDING,
-    PAID
-}
+    UNPAID("UNPAID", "待支付"),
+    PENDING("PENDING", "待确认"),
+    PARTIAL_PAID("PARTIAL_PAID", "部分支付"),
+    PAID("PAID", "已支付");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    PaymentStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 23 - 0
backend/src/main/java/com/oms/enums/Priority.java

@@ -0,0 +1,23 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum Priority {
+    NORMAL("NORMAL", "普通"),
+    URGENT("URGENT", "加急"),
+    HIGH("HIGH", "高"),
+    LOW("LOW", "低");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    Priority(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/ProductStatus.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum ProductStatus {
+    DRAFT("DRAFT", "草稿"),
+    LISTED("LISTED", "已上架"),
+    DELISTED("DELISTED", "已下架");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    ProductStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 29 - 0
backend/src/main/java/com/oms/enums/PurchaseOrderStatus.java

@@ -0,0 +1,29 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum PurchaseOrderStatus {
+    DRAFT("DRAFT", "草稿"),
+    SUBMITTED("SUBMITTED", "已提交"),
+    APPROVED("APPROVED", "已审批"),
+    ORDERED("ORDERED", "已下单"),
+    PARTIAL_ARRIVED("PARTIAL_ARRIVED", "部分到货"),
+    FULLY_ARRIVED("FULLY_ARRIVED", "全部到货"),
+    CLOSED("CLOSED", "已关闭"),
+    REJECTED("REJECTED", "已拒绝"),
+    CANCELLED("CANCELLED", "已取消"),
+    COMPLETED("COMPLETED", "已完成");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    PurchaseOrderStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/ReconcileStatus.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum ReconcileStatus {
+    PENDING("PENDING", "待对账"),
+    RECONCILED("RECONCILED", "已对账"),
+    DISCREPANCY("DISCREPANCY", "有差异");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    ReconcileStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 25 - 0
backend/src/main/java/com/oms/enums/RefundStatus.java

@@ -0,0 +1,25 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum RefundStatus {
+    NONE("NONE", "未退款"),
+    PENDING("PENDING", "待退款"),
+    REFUNDED("REFUNDED", "已退款"),
+    WAITING_INBOUND("WAITING_INBOUND", "已退货待入库"),
+    RESENT("RESENT", "已补发"),
+    COMPLETED("COMPLETED", "已完成");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    RefundStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 21 - 0
backend/src/main/java/com/oms/enums/ReportStatus.java

@@ -0,0 +1,21 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum ReportStatus {
+    DRAFT("DRAFT", "草稿"),
+    GENERATED("GENERATED", "已生成");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    ReportStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 23 - 0
backend/src/main/java/com/oms/enums/ReturnPackageStatus.java

@@ -0,0 +1,23 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum ReturnPackageStatus {
+    PENDING("PENDING", "待收货"),
+    RECEIVED("RECEIVED", "已签收"),
+    INSPECTED("INSPECTED", "已质检"),
+    CLOSED("CLOSED", "已关闭");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    ReturnPackageStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/ReturnStatus.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum ReturnStatus {
+    NOT_RETURNED("NOT_RETURNED", "未退货"),
+    RETURNED("RETURNED", "已退货"),
+    RETURNING("RETURNING", "退货中");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    ReturnStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/SettlementStatus.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum SettlementStatus {
+    PENDING("PENDING", "待结算"),
+    PAID("PAID", "已结算"),
+    PARTIAL("PARTIAL", "部分结算");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    SettlementStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 23 - 6
backend/src/main/java/com/oms/enums/ShippingStatus.java

@@ -1,9 +1,26 @@
 package com.oms.enums;
 
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
 public enum ShippingStatus {
-    UNSHIPPED,
-    PROCESSING,
-    SHIPPED,
-    IN_TRANSIT,
-    DELIVERED
-}
+    UNSHIPPED("UNSHIPPED", "未发货"),
+    PROCESSING("PROCESSING", "处理中"),
+    PENDING_PICK("PENDING_PICK", "待拣货"),
+    PENDING_SHIP("PENDING_SHIP", "待发货"),
+    SHIPPED("SHIPPED", "已发货"),
+    IN_TRANSIT("IN_TRANSIT", "运输中"),
+    DELIVERED("DELIVERED", "已签收");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    ShippingStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 24 - 0
backend/src/main/java/com/oms/enums/TicketStatus.java

@@ -0,0 +1,24 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum TicketStatus {
+    OPEN("OPEN", "待处理"),
+    ASSIGNED("ASSIGNED", "已分配"),
+    IN_PROGRESS("IN_PROGRESS", "处理中"),
+    RESOLVED("RESOLVED", "已解决"),
+    CLOSED("CLOSED", "已关闭");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    TicketStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/TokenStatus.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum TokenStatus {
+    VALID("VALID", "有效"),
+    INVALID("INVALID", "无效"),
+    EXPIRED("EXPIRED", "已过期");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    TokenStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 22 - 0
backend/src/main/java/com/oms/enums/ValidateStatus.java

@@ -0,0 +1,22 @@
+package com.oms.enums;
+
+import com.baomidou.mybatisplus.annotation.EnumValue;
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.Getter;
+
+@Getter
+public enum ValidateStatus {
+    PASS("PASS", "通过"),
+    FAIL("FAIL", "失败"),
+    PENDING("PENDING", "待校验");
+
+    @EnumValue
+    @JsonValue
+    private final String code;
+    private final String label;
+
+    ValidateStatus(String code, String label) {
+        this.code = code;
+        this.label = label;
+    }
+}

+ 78 - 17
backend/src/main/java/com/oms/service/AfterSaleService.java

@@ -6,6 +6,8 @@ import com.oms.converter.AfterSaleConverter;
 import com.oms.dto.AfterSaleDTO;
 import com.oms.entity.AfterSale;
 import com.oms.entity.Orders;
+import com.oms.enums.AuditStatus;
+import com.oms.enums.RefundStatus;
 import com.oms.mapper.AfterSaleMapper;
 import com.oms.mapper.OrdersMapper;
 import lombok.RequiredArgsConstructor;
@@ -13,6 +15,8 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @Service
 @RequiredArgsConstructor
@@ -21,6 +25,21 @@ public class AfterSaleService {
     private final AfterSaleConverter converter;
     private final OrdersMapper ordersMapper;
     private final OrderStatusEventService eventService;
+    private final OrdersService ordersService;
+
+    // 售后退款状态机:PENDING → WAITING_INBOUND → REFUNDED / RESENT
+    private static final Map<String, Set<String>> REFUND_ALLOWED = Map.of(
+        RefundStatus.NONE.getCode(), Set.of(RefundStatus.PENDING.getCode()),
+        RefundStatus.PENDING.getCode(), Set.of(RefundStatus.WAITING_INBOUND.getCode(), RefundStatus.REFUNDED.getCode(), RefundStatus.RESENT.getCode()),
+        RefundStatus.WAITING_INBOUND.getCode(), Set.of(RefundStatus.REFUNDED.getCode()),
+        RefundStatus.REFUNDED.getCode(), Set.of(RefundStatus.COMPLETED.getCode())
+    );
+
+    private void validateRefundTransition(String current, String target) {
+        if (!REFUND_ALLOWED.getOrDefault(current, Set.of()).contains(target)) {
+            throw new IllegalStateException("退款状态不能从 " + current + " 变更为 " + target);
+        }
+    }
 
     public Page<AfterSaleDTO> getPage(int page, int size) {
         Page<AfterSale> pageResult = mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<AfterSale>().orderByDesc(AfterSale::getCreatedAt));
@@ -41,44 +60,86 @@ public class AfterSaleService {
     @Transactional
     public void approve(Long id, String remark, String operator) {
         AfterSale as = mapper.selectById(id);
-        if (!"PENDING".equals(as.getAuditStatus())) throw new IllegalStateException("Only pending can be approved");
-        as.setAuditStatus("APPROVED"); as.setAuditRemark(remark); mapper.updateById(as);
-        eventService.logEvent(as.getOrderId(), "APPROVED", "审核通过", "售后单已通过", "primary", operator);
+        if (!AuditStatus.PENDING.getCode().equals(as.getAuditStatus())) throw new IllegalStateException("Only pending can be approved");
+        as.setAuditStatus(AuditStatus.APPROVED.getCode()); as.setAuditRemark(remark); mapper.updateById(as);
+        eventService.logEvent(as.getOrderId(), AuditStatus.APPROVED.name(), "审核通过", "售后单已通过", "primary", operator);
     }
     @Transactional
     public void reject(Long id, String reason, String operator) {
         AfterSale as = mapper.selectById(id);
-        if (!"PENDING".equals(as.getAuditStatus())) throw new IllegalStateException("Only pending can be rejected");
-        as.setAuditStatus("REJECTED"); as.setAuditRemark(reason); mapper.updateById(as);
-        eventService.logEvent(as.getOrderId(), "REJECTED", "审核拒绝", reason, "danger", operator);
+        if (!AuditStatus.PENDING.getCode().equals(as.getAuditStatus())) throw new IllegalStateException("Only pending can be rejected");
+        as.setAuditStatus(AuditStatus.REJECTED.getCode()); as.setAuditRemark(reason); mapper.updateById(as);
+        eventService.logEvent(as.getOrderId(), AuditStatus.REJECTED.name(), "审核拒绝", reason, "danger", operator);
     }
     @Transactional
     public void processRefund(Long id, String operator) {
         AfterSale as = mapper.selectById(id);
-        if (!"APPROVED".equals(as.getAuditStatus())) throw new IllegalStateException("Not approved");
-        as.setRefundStatus("REFUNDED"); mapper.updateById(as);
+        if (as == null) throw new IllegalArgumentException("售后单不存在");
+        if (!AuditStatus.APPROVED.getCode().equals(as.getAuditStatus())) throw new IllegalStateException("售后单未审批通过");
+        validateRefundTransition(as.getRefundStatus(), RefundStatus.REFUNDED.getCode());
+        as.setRefundStatus(RefundStatus.REFUNDED.getCode());
+        mapper.updateById(as);
         Orders o = ordersMapper.selectById(as.getOrderId());
-        if (o != null) { o.setRefundStatus("REFUNDED"); ordersMapper.updateById(o); }
-        eventService.logEvent(as.getOrderId(), "REFUNDED", "退款完成", "金额 " + as.getAmount(), "success", operator);
+        if (o != null) {
+            o.setRefundStatus(RefundStatus.REFUNDED);
+            ordersMapper.updateById(o);
+        }
+        eventService.logEvent(as.getOrderId(), RefundStatus.REFUNDED.name(), "退款完成", "金额 " + as.getAmount(), "success", operator);
     }
+
     @Transactional
     public void submitReturnTracking(Long id, String carrier, String trackingNo, String operator) {
         AfterSale as = mapper.selectById(id);
-        as.setReturnCarrier(carrier); as.setReturnTrackingNo(trackingNo); as.setRefundStatus("WAITING_INBOUND"); mapper.updateById(as);
-        eventService.logEvent(as.getOrderId(), "WAITING_INBOUND", "退货物流已录入", carrier + " " + trackingNo, "primary", operator);
+        if (as == null) throw new IllegalArgumentException("售后单不存在");
+        validateRefundTransition(as.getRefundStatus(), RefundStatus.WAITING_INBOUND.getCode());
+        as.setReturnCarrier(carrier);
+        as.setReturnTrackingNo(trackingNo);
+        as.setRefundStatus(RefundStatus.WAITING_INBOUND.getCode());
+        mapper.updateById(as);
+        eventService.logEvent(as.getOrderId(), RefundStatus.WAITING_INBOUND.name(), "退货物流已录入", carrier + " " + trackingNo, "primary", operator);
     }
+
     @Transactional
     public void confirmReceipt(Long id, String operator) {
         AfterSale as = mapper.selectById(id);
-        as.setRefundStatus("REFUNDED"); mapper.updateById(as);
+        if (as == null) throw new IllegalArgumentException("售后单不存在");
+        validateRefundTransition(as.getRefundStatus(), RefundStatus.REFUNDED.getCode());
+        as.setRefundStatus(RefundStatus.REFUNDED.getCode());
+        mapper.updateById(as);
         Orders o = ordersMapper.selectById(as.getOrderId());
-        if (o != null) { o.setRefundStatus("REFUNDED"); ordersMapper.updateById(o); }
-        eventService.logEvent(as.getOrderId(), "REFUNDED", "确认入库并退款", "退货已入库", "success", operator);
+        if (o != null) {
+            o.setRefundStatus(RefundStatus.REFUNDED);
+            ordersMapper.updateById(o);
+        }
+        eventService.logEvent(as.getOrderId(), RefundStatus.REFUNDED.name(), "确认入库并退款", "退货已入库", "success", operator);
     }
+
     @Transactional
     public void createResendOrder(Long id, Long warehouseId, Long skuId, int qty, String operator) {
         AfterSale as = mapper.selectById(id);
-        as.setRefundStatus("RESENT"); as.setResendWarehouseId(warehouseId); as.setResendSkuId(skuId); mapper.updateById(as);
-        eventService.logEvent(as.getOrderId(), "RESENT", "补发单已生成", "仓库:" + warehouseId, "primary", operator);
+        if (as == null) throw new IllegalArgumentException("售后单不存在");
+        if (!AuditStatus.APPROVED.getCode().equals(as.getAuditStatus())) throw new IllegalStateException("售后单未审批通过");
+        validateRefundTransition(as.getRefundStatus(), RefundStatus.RESENT.getCode());
+
+        // 创建补发订单
+        Orders originalOrder = ordersMapper.selectById(as.getOrderId());
+        Orders resendOrder = new Orders();
+        resendOrder.setChannelId(originalOrder != null ? originalOrder.getChannelId() : null);
+        resendOrder.setWarehouseId(warehouseId);
+        resendOrder.setBuyer(originalOrder != null ? originalOrder.getBuyer() : null);
+        resendOrder.setBuyerId(originalOrder != null ? originalOrder.getBuyerId() : null);
+        resendOrder.setReceiverName(originalOrder != null ? originalOrder.getReceiverName() : null);
+        resendOrder.setReceiverPhone(originalOrder != null ? originalOrder.getReceiverPhone() : null);
+        resendOrder.setReceiverAddress(originalOrder != null ? originalOrder.getReceiverAddress() : null);
+        resendOrder.setCurrency(originalOrder != null ? originalOrder.getCurrency() : "USD");
+        resendOrder.setInternalRemark("售后补发单,原售后单: " + as.getAfterSaleNo());
+        Long resendOrderId = ordersService.save(resendOrder);
+
+        as.setRefundStatus(RefundStatus.RESENT.getCode());
+        as.setResendWarehouseId(warehouseId);
+        as.setResendSkuId(skuId);
+        as.setResendOrderId(resendOrderId);
+        mapper.updateById(as);
+        eventService.logEvent(as.getOrderId(), RefundStatus.RESENT.name(), "补发单已生成", "补发订单ID:" + resendOrderId, "primary", operator);
     }
 }

+ 5 - 3
backend/src/main/java/com/oms/service/AutoReplyRuleService.java

@@ -10,6 +10,8 @@ import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import com.oms.enums.GenericStatus;
+
 import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Comparator;
@@ -44,7 +46,7 @@ public class AutoReplyRuleService {
 
     public List<AutoReplyRule> getActiveRules() {
         return mapper.selectList(new LambdaQueryWrapper<AutoReplyRule>()
-                .eq(AutoReplyRule::getStatus, "ACTIVE")
+                .eq(AutoReplyRule::getStatus, GenericStatus.ACTIVE.getCode())
                 .orderByAsc(AutoReplyRule::getPriority));
     }
 
@@ -52,7 +54,7 @@ public class AutoReplyRuleService {
     public Long save(AutoReplyRule entity) {
         validateRule(entity);
         if (entity.getStatus() == null) {
-            entity.setStatus("INACTIVE");
+            entity.setStatus(GenericStatus.INACTIVE.getCode());
         }
         if (entity.getHitCount() == null) {
             entity.setHitCount(0);
@@ -79,7 +81,7 @@ public class AutoReplyRuleService {
     public void toggleEnabled(Long id, boolean enabled) {
         AutoReplyRule r = mapper.selectById(id);
         if (r == null) throw new IllegalArgumentException("Rule not found");
-        r.setStatus(enabled ? "ACTIVE" : "INACTIVE");
+        r.setStatus(enabled ? GenericStatus.ACTIVE.getCode() : GenericStatus.INACTIVE.getCode());
         mapper.updateById(r);
     }
 

+ 7 - 5
backend/src/main/java/com/oms/service/ChannelMappingService.java

@@ -7,6 +7,8 @@ import com.oms.dto.ChannelMappingDTO;
 import com.oms.entity.ChannelMapping;
 import com.oms.entity.ProductSku;
 import com.oms.mapper.ChannelMappingMapper;
+import com.oms.enums.MappingStatus;
+import com.oms.enums.ValidateStatus;
 import com.oms.mapper.ProductSkuMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -42,15 +44,15 @@ public class ChannelMappingService {
         if (cm.getChannelCategory() == null || cm.getChannelCategory().isEmpty()) errors.add("渠道类目未填写");
         if (cm.getChannelPrice() == null) errors.add("渠道价格未设置");
         String result;
-        if (errors.isEmpty()) { cm.setValidateStatus("PASS"); cm.setLastValidateResult("校验通过"); result = "校验通过"; }
-        else { cm.setValidateStatus("FAIL"); cm.setLastValidateResult(String.join(";", errors)); result = String.join(";", errors); }
+        if (errors.isEmpty()) { cm.setValidateStatus(ValidateStatus.PASS.getCode()); cm.setLastValidateResult("校验通过"); result = "校验通过"; }
+        else { cm.setValidateStatus(ValidateStatus.FAIL.getCode()); cm.setLastValidateResult(String.join(";", errors)); result = String.join(";", errors); }
         mapper.updateById(cm); return result;
     }
     @Transactional
     public void publish(Long id) {
         ChannelMapping cm = mapper.selectById(id);
-        if (!"PASS".equals(cm.getValidateStatus())) throw new IllegalStateException("Must validate first");
-        cm.setMappingStatus("MAPPED"); cm.setLastSyncAt(LocalDateTime.now()); mapper.updateById(cm);
+        if (!ValidateStatus.PASS.getCode().equals(cm.getValidateStatus())) throw new IllegalStateException("Must validate first");
+        cm.setMappingStatus(MappingStatus.MAPPED.getCode()); cm.setLastSyncAt(LocalDateTime.now()); mapper.updateById(cm);
     }
     @Transactional
     public List<Long> autoMap(Long productId, Long channelId) {
@@ -61,7 +63,7 @@ public class ChannelMappingService {
             if (existing != null) continue;
             ChannelMapping cm = new ChannelMapping();
             cm.setProductId(productId); cm.setSkuId(sku.getId()); cm.setChannelId(channelId); cm.setInternalSku(sku.getSku());
-            cm.setMappingStatus("MAPPED"); cm.setValidateStatus("PENDING");
+            cm.setMappingStatus(MappingStatus.MAPPED.getCode()); cm.setValidateStatus(ValidateStatus.PENDING.getCode());
             mapper.insert(cm); created.add(cm.getId());
         }
         return created;

+ 8 - 3
backend/src/main/java/com/oms/service/ChannelService.java

@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.ChannelConverter;
 import com.oms.dto.ChannelDTO;
 import com.oms.entity.Channel;
+import com.oms.enums.ChannelSyncStatus;
+import com.oms.enums.TokenStatus;
 import com.oms.mapper.ChannelMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -33,14 +35,17 @@ public class ChannelService {
 
     @Transactional
     public void testConnection(Long id) {
-        Channel c = mapper.selectById(id); c.setTokenStatus("VALID"); c.setErrorMessage(null); c.setLastSyncAt(LocalDateTime.now()); mapper.updateById(c);
+        Channel c = mapper.selectById(id); c.setTokenStatus(TokenStatus.VALID.getCode()); c.setErrorMessage(null); c.setLastSyncAt(LocalDateTime.now()); mapper.updateById(c);
     }
     @Transactional
     public void toggleSync(Long id, boolean enabled) {
-        Channel c = mapper.selectById(id); c.setSyncEnabled(enabled); c.setSyncStatus(enabled ? "SYNCING" : "PAUSED"); mapper.updateById(c);
+        Channel c = mapper.selectById(id);
+        c.setSyncEnabled(enabled);
+        c.setSyncStatus(enabled ? ChannelSyncStatus.SYNCING.getCode() : ChannelSyncStatus.PAUSED.getCode());
+        mapper.updateById(c);
     }
     @Transactional
     public void reauthorize(Long id) {
-        Channel c = mapper.selectById(id); c.setAccessToken(null); c.setRefreshToken(null); c.setTokenStatus("INVALID"); mapper.updateById(c);
+        Channel c = mapper.selectById(id); c.setAccessToken(null); c.setRefreshToken(null); c.setTokenStatus(TokenStatus.INVALID.getCode()); mapper.updateById(c);
     }
 }

+ 7 - 6
backend/src/main/java/com/oms/service/ChatSessionService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.ChatSessionConverter;
 import com.oms.dto.ChatSessionDTO;
 import com.oms.entity.ChatSession;
+import com.oms.enums.ChatStatus;
 import com.oms.mapper.ChatSessionMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -47,14 +48,14 @@ public class ChatSessionService {
 
     public List<ChatSession> getWaitingSessions() {
         return mapper.selectList(new LambdaQueryWrapper<ChatSession>()
-                .eq(ChatSession::getStatus, "WAITING")
+                .eq(ChatSession::getStatus, ChatStatus.WAITING.getCode())
                 .orderByAsc(ChatSession::getCreatedAt));
     }
 
     @Transactional
     public Long save(ChatSession entity) {
         if (entity.getStatus() == null) {
-            entity.setStatus("WAITING");
+            entity.setStatus(ChatStatus.WAITING.getCode());
         }
         if (entity.getAiHandled() == null) {
             entity.setAiHandled(1);
@@ -78,7 +79,7 @@ public class ChatSessionService {
     public void closeSession(Long id) {
         ChatSession cs = mapper.selectById(id);
         if (cs == null) throw new IllegalArgumentException("Session not found");
-        cs.setStatus("CLOSED");
+        cs.setStatus(ChatStatus.CLOSED.getCode());
         cs.setEndedAt(LocalDateTime.now());
         mapper.updateById(cs);
     }
@@ -89,7 +90,7 @@ public class ChatSessionService {
         if (cs == null) throw new IllegalArgumentException("Session not found");
         cs.setAgentId(agentId);
         cs.setAgentName(agentName);
-        cs.setStatus("ASSIGNED");
+        cs.setStatus(ChatStatus.ASSIGNED.getCode());
         mapper.updateById(cs);
     }
 
@@ -107,7 +108,7 @@ public class ChatSessionService {
         ChatSession cs = mapper.selectById(sessionId);
         if (cs == null) throw new IllegalArgumentException("Session not found");
         cs.setSatisfaction(rating);
-        cs.setStatus("CLOSED");
+        cs.setStatus(ChatStatus.CLOSED.getCode());
         cs.setEndedAt(LocalDateTime.now());
         mapper.updateById(cs);
     }
@@ -118,7 +119,7 @@ public class ChatSessionService {
         if (cs == null) throw new IllegalArgumentException("Session not found");
         cs.setAgentId(agentId);
         cs.setAgentName(agentName);
-        cs.setStatus("ASSIGNED");
+        cs.setStatus(ChatStatus.ASSIGNED.getCode());
         mapper.updateById(cs);
     }
 }

+ 9 - 8
backend/src/main/java/com/oms/service/CouponService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.CouponConverter;
 import com.oms.dto.CouponDTO;
 import com.oms.entity.Coupon;
+import com.oms.enums.CouponStatus;
 import com.oms.mapper.CouponMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -49,7 +50,7 @@ public class CouponService {
     public List<Coupon> getValidCoupons() {
         LocalDateTime now = LocalDateTime.now();
         return mapper.selectList(new LambdaQueryWrapper<Coupon>()
-                .eq(Coupon::getStatus, "ACTIVE")
+                .eq(Coupon::getStatus, CouponStatus.ACTIVE.getCode())
                 .le(Coupon::getValidStart, now)
                 .ge(Coupon::getValidEnd, now)
                 .apply("total_count IS NULL OR total_count > used_count")
@@ -60,7 +61,7 @@ public class CouponService {
     public Long save(Coupon entity) {
         validateCoupon(entity);
         if (entity.getStatus() == null) {
-            entity.setStatus("DRAFT");
+            entity.setStatus(CouponStatus.DRAFT.getCode());
         }
         if (entity.getUsedCount() == null) {
             entity.setUsedCount(0);
@@ -90,8 +91,8 @@ public class CouponService {
         if (qty <= 0) throw new IllegalArgumentException("Quantity must be positive");
         int current = c.getTotalCount() != null ? c.getTotalCount() : 0;
         c.setTotalCount(current + qty);
-        if (!"ACTIVE".equals(c.getStatus())) {
-            c.setStatus("ACTIVE");
+        if (!CouponStatus.ACTIVE.getCode().equals(c.getStatus())) {
+            c.setStatus(CouponStatus.ACTIVE.getCode());
         }
         mapper.updateById(c);
     }
@@ -101,7 +102,7 @@ public class CouponService {
         Coupon c = mapper.selectById(id);
         if (c == null) throw new IllegalArgumentException("Coupon not found");
         validateCoupon(c);
-        c.setStatus("ACTIVE");
+        c.setStatus(CouponStatus.ACTIVE.getCode());
         mapper.updateById(c);
     }
 
@@ -109,7 +110,7 @@ public class CouponService {
     public void deactivate(Long id) {
         Coupon c = mapper.selectById(id);
         if (c == null) throw new IllegalArgumentException("Coupon not found");
-        c.setStatus("INACTIVE");
+        c.setStatus(CouponStatus.INACTIVE.getCode());
         mapper.updateById(c);
     }
 
@@ -130,7 +131,7 @@ public class CouponService {
             c.setUsedRate(BigDecimal.valueOf(c.getUsedCount() * 100.0 / totalCount));
         }
         if (c.getUsedCount() >= totalCount) {
-            c.setStatus("EXHAUSTED");
+            c.setStatus(CouponStatus.EXHAUSTED.getCode());
         }
         mapper.updateById(c);
         return true;
@@ -139,7 +140,7 @@ public class CouponService {
     public boolean canUse(Long id) {
         Coupon c = mapper.selectById(id);
         if (c == null) return false;
-        if (!"ACTIVE".equals(c.getStatus())) return false;
+        if (!CouponStatus.ACTIVE.getCode().equals(c.getStatus())) return false;
         LocalDateTime now = LocalDateTime.now();
         if (c.getValidStart() != null && now.isBefore(c.getValidStart())) return false;
         if (c.getValidEnd() != null && now.isAfter(c.getValidEnd())) return false;

+ 11 - 7
backend/src/main/java/com/oms/service/DashboardService.java

@@ -3,6 +3,10 @@ package com.oms.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.oms.entity.Inventory;
 import com.oms.entity.Orders;
+import com.oms.enums.AlertStatus;
+import com.oms.enums.OrderStatus;
+import com.oms.enums.PaymentStatus;
+import com.oms.enums.ShippingStatus;
 import com.oms.mapper.InventoryMapper;
 import com.oms.mapper.OrdersMapper;
 import lombok.RequiredArgsConstructor;
@@ -34,13 +38,13 @@ public class DashboardService {
         // Exception orders
         Long exceptionOrders = ordersMapper.selectCount(new LambdaQueryWrapper<Orders>()
                 .ne(Orders::getExceptionTag, "正常")
-                .ne(Orders::getExceptionTag, "NORMAL")
+                .ne(Orders::getExceptionTag, AlertStatus.NORMAL.getCode())
                 .ne(Orders::getExceptionTag, ""));
         result.put("exceptionOrders", exceptionOrders);
 
         // Total GMV (sum of orderAmount for paid orders)
         List<Orders> paidOrders = ordersMapper.selectList(new LambdaQueryWrapper<Orders>()
-                .ne(Orders::getPaymentStatus, "UNPAID"));
+                .ne(Orders::getPaymentStatus, PaymentStatus.UNPAID));
         BigDecimal totalGmv = paidOrders.stream()
                 .map(Orders::getOrderAmount)
                 .filter(Objects::nonNull)
@@ -49,8 +53,8 @@ public class DashboardService {
 
         // Pending shipment
         Long pendingShipment = ordersMapper.selectCount(new LambdaQueryWrapper<Orders>()
-                .eq(Orders::getShippingStatus, "UNSHIPPED")
-                .ne(Orders::getOrderStatus, "CANCELLED"));
+                .eq(Orders::getShippingStatus, ShippingStatus.UNSHIPPED)
+                .ne(Orders::getOrderStatus, OrderStatus.CANCELLED));
         result.put("pendingShipment", pendingShipment);
 
         // Low stock items
@@ -60,12 +64,12 @@ public class DashboardService {
 
         // Orders by status
         List<Map<String, Object>> orderStats = new ArrayList<>();
-        String[] statuses = {"CREATED", "PAID", "ALLOCATED", "SHIPPED", "DELIVERED", "COMPLETED", "CANCELLED"};
-        for (String status : statuses) {
+        OrderStatus[] statuses = {OrderStatus.CREATED, OrderStatus.PAID, OrderStatus.ALLOCATED, OrderStatus.SHIPPED, OrderStatus.DELIVERED, OrderStatus.COMPLETED, OrderStatus.CANCELLED};
+        for (OrderStatus status : statuses) {
             Long count = ordersMapper.selectCount(new LambdaQueryWrapper<Orders>()
                     .eq(Orders::getOrderStatus, status));
             Map<String, Object> stat = new HashMap<>();
-            stat.put("status", status);
+            stat.put("status", status.name());
             stat.put("count", count);
             orderStats.add(stat);
         }

+ 23 - 2
backend/src/main/java/com/oms/service/FinancePaymentService.java

@@ -5,7 +5,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.FinancePaymentConverter;
 import com.oms.dto.FinancePaymentDTO;
 import com.oms.entity.FinancePayment;
+import com.oms.entity.Orders;
+import com.oms.enums.PaymentStatus;
+import com.oms.enums.ReconcileStatus;
 import com.oms.mapper.FinancePaymentMapper;
+import com.oms.mapper.OrdersMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -18,6 +22,7 @@ import java.util.List;
 public class FinancePaymentService {
     private final FinancePaymentMapper mapper;
     private final FinancePaymentConverter converter;
+    private final OrdersMapper ordersMapper;
 
     public Page<FinancePayment> getPage(int page, int size) { return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<FinancePayment>().orderByDesc(FinancePayment::getCreatedAt)); }
     public List<FinancePayment> getAll() { return mapper.selectList(new LambdaQueryWrapper<FinancePayment>().orderByDesc(FinancePayment::getCreatedAt)); }
@@ -25,10 +30,26 @@ public class FinancePaymentService {
     public FinancePaymentDTO getDtoById(Long id) { FinancePayment e = mapper.selectById(id); return e == null ? null : converter.toDto(e); }
     public FinancePayment getByPaymentNo(String no) { return mapper.selectOne(new LambdaQueryWrapper<FinancePayment>().eq(FinancePayment::getPaymentNo, no)); }
     public List<FinancePayment> getByOrderId(Long orderId) { return mapper.selectList(new LambdaQueryWrapper<FinancePayment>().eq(FinancePayment::getOrderId, orderId)); }
-    public List<FinancePayment> getUnreconciled() { return mapper.selectList(new LambdaQueryWrapper<FinancePayment>().eq(FinancePayment::getReconcileStatus, "PENDING").orderByAsc(FinancePayment::getPayTime)); }
+    public List<FinancePayment> getUnreconciled() { return mapper.selectList(new LambdaQueryWrapper<FinancePayment>().eq(FinancePayment::getReconcileStatus, ReconcileStatus.PENDING.getCode()).orderByAsc(FinancePayment::getPayTime)); }
     public Long save(FinancePayment entity) { mapper.insert(entity); return entity.getId(); }
     public void update(FinancePayment entity) { mapper.updateById(entity); }
     public void delete(Long id) { mapper.deleteById(id); }
     @Transactional
-    public void reconcile(Long id) { FinancePayment p = mapper.selectById(id); p.setReconcileStatus("RECONCILED"); p.setReconcileTime(LocalDateTime.now()); mapper.updateById(p); }
+    public void reconcile(Long id) {
+        FinancePayment p = mapper.selectById(id);
+        if (p == null) throw new IllegalArgumentException("支付记录不存在");
+        if (ReconcileStatus.RECONCILED.getCode().equals(p.getReconcileStatus())) {
+            throw new IllegalStateException("该笔支付已对账,不能重复对账");
+        }
+        p.setReconcileStatus(ReconcileStatus.RECONCILED.getCode());
+        p.setReconcileTime(LocalDateTime.now());
+        mapper.updateById(p);
+        if (p.getOrderId() != null) {
+            Orders o = ordersMapper.selectById(p.getOrderId());
+            if (o != null) {
+                o.setPaymentStatus(PaymentStatus.PAID);
+                ordersMapper.updateById(o);
+            }
+        }
+    }
 }

+ 21 - 1
backend/src/main/java/com/oms/service/FinanceRefundService.java

@@ -5,7 +5,10 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.FinanceRefundConverter;
 import com.oms.dto.FinanceRefundDTO;
 import com.oms.entity.FinanceRefund;
+import com.oms.entity.Orders;
+import com.oms.enums.RefundStatus;
 import com.oms.mapper.FinanceRefundMapper;
+import com.oms.mapper.OrdersMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -18,6 +21,7 @@ import java.util.List;
 public class FinanceRefundService {
     private final FinanceRefundMapper mapper;
     private final FinanceRefundConverter converter;
+    private final OrdersMapper ordersMapper;
 
     public Page<FinanceRefund> getPage(int page, int size) { return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<FinanceRefund>().orderByDesc(FinanceRefund::getCreatedAt)); }
     public List<FinanceRefund> getAll() { return mapper.selectList(new LambdaQueryWrapper<FinanceRefund>().orderByDesc(FinanceRefund::getCreatedAt)); }
@@ -29,5 +33,21 @@ public class FinanceRefundService {
     public void update(FinanceRefund entity) { mapper.updateById(entity); }
     public void delete(Long id) { mapper.deleteById(id); }
     @Transactional
-    public void processRefund(Long id) { FinanceRefund r = mapper.selectById(id); r.setRefundStatus("COMPLETED"); r.setRefundTime(LocalDateTime.now()); mapper.updateById(r); }
+    public void processRefund(Long id) {
+        FinanceRefund r = mapper.selectById(id);
+        if (r == null) throw new IllegalArgumentException("退款记录不存在");
+        if (RefundStatus.COMPLETED.getCode().equals(r.getRefundStatus())) {
+            throw new IllegalStateException("该笔退款已处理,不能重复处理");
+        }
+        r.setRefundStatus(RefundStatus.COMPLETED.getCode());
+        r.setRefundTime(LocalDateTime.now());
+        mapper.updateById(r);
+        if (r.getOrderId() != null) {
+            Orders o = ordersMapper.selectById(r.getOrderId());
+            if (o != null) {
+                o.setRefundStatus(RefundStatus.REFUNDED);
+                ordersMapper.updateById(o);
+            }
+        }
+    }
 }

+ 104 - 1
backend/src/main/java/com/oms/service/InventoryService.java

@@ -6,6 +6,7 @@ import com.oms.converter.InventoryConverter;
 import com.oms.dto.InventoryDTO;
 import com.oms.entity.Inventory;
 import com.oms.entity.InventoryLog;
+import com.oms.enums.InventoryWarningStatus;
 import com.oms.mapper.InventoryLogMapper;
 import com.oms.mapper.InventoryMapper;
 import lombok.RequiredArgsConstructor;
@@ -87,40 +88,142 @@ public class InventoryService {
 
     public void recalcWarningStatus(Long id) {
         Inventory inv = mapper.selectById(id);
-        inv.setWarningStatus(inv.getAvailable() < inv.getSafeStock() ? "LOW_STOCK" : "NORMAL");
+        inv.setWarningStatus(inv.getAvailable() < inv.getSafeStock() ? InventoryWarningStatus.LOW_STOCK.getCode() : InventoryWarningStatus.NORMAL.getCode());
         mapper.updateById(inv);
     }
 
     @Transactional
     public void lockInventory(Long id, int qty, String operator) {
         Inventory inventory = mapper.selectById(id);
+        if (inventory == null) throw new IllegalArgumentException("库存记录不存在");
+        if (inventory.getAvailable() < qty) {
+            throw new IllegalStateException("可用库存不足,当前可用: " + inventory.getAvailable() + ",需锁定: " + qty);
+        }
         int before = inventory.getAvailable();
         inventory.setLocked(inventory.getLocked() + qty);
         inventory.setAvailable(inventory.getAvailable() - qty);
         inventory.setLastChangeAt(LocalDateTime.now());
         mapper.updateById(inventory);
         saveInventoryLog(inventory, "ORDER_ALLOCATION", "LOCK", qty, before, inventory.getAvailable(), operator, null, null);
+        recalcWarningStatus(id);
     }
 
     @Transactional
     public void unlockInventory(Long id, int qty, String operator) {
         Inventory inventory = mapper.selectById(id);
+        if (inventory == null) throw new IllegalArgumentException("库存记录不存在");
+        if (inventory.getLocked() < qty) {
+            throw new IllegalStateException("锁定库存不足,当前锁定: " + inventory.getLocked() + ",需解锁: " + qty);
+        }
         int before = inventory.getAvailable();
         inventory.setLocked(inventory.getLocked() - qty);
         inventory.setAvailable(inventory.getAvailable() + qty);
         inventory.setLastChangeAt(LocalDateTime.now());
         mapper.updateById(inventory);
         saveInventoryLog(inventory, "ORDER_ALLOCATION", "UNLOCK", qty, before, inventory.getAvailable(), operator, null, null);
+        recalcWarningStatus(id);
     }
 
     @Transactional
     public void incrementInventory(Long id, int qty, String changeType, String operator, Long orderId, String orderNo) {
         Inventory inventory = mapper.selectById(id);
+        if (inventory == null) throw new IllegalArgumentException("库存记录不存在");
+        if (qty <= 0) throw new IllegalStateException("入库数量必须大于0");
         int before = inventory.getAvailable();
         inventory.setAvailable(inventory.getAvailable() + qty);
         inventory.setLastChangeAt(LocalDateTime.now());
         mapper.updateById(inventory);
         saveInventoryLog(inventory, changeType, "INCREMENT", qty, before, inventory.getAvailable(), operator, orderId, orderNo);
+        recalcWarningStatus(id);
+    }
+
+    @Transactional
+    public void shipInventoryBySku(Long skuId, int qty, String operator) {
+        List<Inventory> list = getBySkuId(skuId);
+        if (list.isEmpty()) throw new IllegalStateException("SKU " + skuId + " 无库存记录");
+        Inventory inventory = list.get(0);
+        if (inventory.getLocked() < qty) {
+            throw new IllegalStateException("锁定库存不足,当前锁定: " + inventory.getLocked() + ",需发货: " + qty);
+        }
+        int beforeLocked = inventory.getLocked();
+        inventory.setLocked(inventory.getLocked() - qty);
+        inventory.setLastChangeAt(LocalDateTime.now());
+        mapper.updateById(inventory);
+        saveInventoryLog(inventory, "ORDER_SHIPMENT", "SHIP", qty, beforeLocked, inventory.getLocked(), operator, null, null);
+    }
+
+    /**
+     * 库存盘点。按实际盘点数量调整库存,记录盘盈/盘亏。
+     *
+     * @param id 库存记录ID
+     * @param actualQty 实际盘点数量
+     * @param operator 操作人
+     */
+    @Transactional
+    public void stocktake(Long id, int actualQty, String operator) {
+        Inventory inventory = mapper.selectById(id);
+        if (inventory == null) throw new IllegalArgumentException("库存记录不存在");
+        if (actualQty < 0) throw new IllegalStateException("盘点数量不能为负数");
+
+        int before = inventory.getAvailable();
+        int diff = actualQty - before;
+        inventory.setAvailable(actualQty);
+        inventory.setLastChangeAt(LocalDateTime.now());
+        mapper.updateById(inventory);
+
+        String changeType = diff > 0 ? "STOCKTAKE_GAIN" : (diff < 0 ? "STOCKTAKE_LOSS" : "STOCKTAKE");
+        saveInventoryLog(inventory, "STOCKTAKE", changeType, Math.abs(diff), before, actualQty, operator, null, null);
+        recalcWarningStatus(id);
+    }
+
+    /**
+     * 库存调拨。从源仓库调拨到目标仓库。
+     *
+     * @param skuId SKU ID
+     * @param fromWarehouseId 源仓库ID
+     * @param toWarehouseId 目标仓库ID
+     * @param qty 调拨数量
+     * @param operator 操作人
+     */
+    @Transactional
+    public void transfer(Long skuId, Long fromWarehouseId, Long toWarehouseId, int qty, String operator) {
+        if (fromWarehouseId.equals(toWarehouseId)) {
+            throw new IllegalStateException("源仓库和目标仓库不能相同");
+        }
+        if (qty <= 0) throw new IllegalStateException("调拨数量必须大于0");
+
+        Inventory fromInv = getBySkuIdAndWarehouseId(skuId, fromWarehouseId);
+        if (fromInv == null || fromInv.getAvailable() < qty) {
+            throw new IllegalStateException("源仓库库存不足");
+        }
+
+        // 扣减源仓库
+        int fromBefore = fromInv.getAvailable();
+        fromInv.setAvailable(fromInv.getAvailable() - qty);
+        fromInv.setLastChangeAt(LocalDateTime.now());
+        mapper.updateById(fromInv);
+        saveInventoryLog(fromInv, "TRANSFER", "TRANSFER_OUT", qty, fromBefore, fromInv.getAvailable(), operator, null, null);
+        recalcWarningStatus(fromInv.getId());
+
+        // 增加目标仓库
+        Inventory toInv = getBySkuIdAndWarehouseId(skuId, toWarehouseId);
+        if (toInv == null) {
+            // 目标仓库无此SKU库存记录,创建一条
+            toInv = new Inventory();
+            toInv.setSkuId(skuId);
+            toInv.setWarehouseId(toWarehouseId);
+            toInv.setAvailable(0);
+            toInv.setLocked(0);
+            toInv.setInbound(0);
+            toInv.setSafeStock(0);
+            mapper.insert(toInv);
+        }
+        int toBefore = toInv.getAvailable();
+        toInv.setAvailable(toInv.getAvailable() + qty);
+        toInv.setLastChangeAt(LocalDateTime.now());
+        mapper.updateById(toInv);
+        saveInventoryLog(toInv, "TRANSFER", "TRANSFER_IN", qty, toBefore, toInv.getAvailable(), operator, null, null);
+        recalcWarningStatus(toInv.getId());
     }
 
     private void saveInventoryLog(Inventory inventory, String source, String changeType, int quantity, int beforeQty, int afterQty, String operator, Long orderId, String orderNo) {

+ 5 - 4
backend/src/main/java/com/oms/service/InventoryTurnoverService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.InventoryTurnoverConverter;
 import com.oms.dto.InventoryTurnoverDTO;
 import com.oms.entity.InventoryTurnover;
+import com.oms.enums.AlertStatus;
 import com.oms.mapper.InventoryTurnoverMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -61,7 +62,7 @@ public class InventoryTurnoverService {
         if (entity.getTurnoverDays() == null) entity.setTurnoverDays(0);
         if (entity.getSalesQty() == null) entity.setSalesQty(0);
         if (entity.getAvgStock() == null) entity.setAvgStock(BigDecimal.ZERO);
-        if (entity.getAlertStatus() == null) entity.setAlertStatus("NORMAL");
+        if (entity.getAlertStatus() == null) entity.setAlertStatus(AlertStatus.NORMAL.getCode());
         mapper.insert(entity);
         return entity.getId();
     }
@@ -92,11 +93,11 @@ public class InventoryTurnoverService {
         }
         
         if (t.getTurnoverDays() != null && t.getTurnoverDays() < 7) {
-            t.setAlertStatus("FAST");
+            t.setAlertStatus(AlertStatus.FAST.getCode());
         } else if (t.getTurnoverDays() != null && t.getTurnoverDays() > 30) {
-            t.setAlertStatus("SLOW");
+            t.setAlertStatus(AlertStatus.SLOW.getCode());
         } else {
-            t.setAlertStatus("NORMAL");
+            t.setAlertStatus(AlertStatus.NORMAL.getCode());
         }
         
         mapper.updateById(t);

+ 66 - 24
backend/src/main/java/com/oms/service/InvoiceService.java

@@ -5,10 +5,14 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.InvoiceConverter;
 import com.oms.dto.InvoiceDTO;
 import com.oms.entity.Invoice;
+import com.oms.enums.InvoiceStatus;
 import com.oms.mapper.InvoiceMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.time.LocalDate;
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Service
@@ -18,40 +22,78 @@ public class InvoiceService {
     private final InvoiceConverter converter;
 
     public Page<Invoice> getPage(int page, int size) {
-        Page<Invoice> pageParam = new Page<>(page, size);
-        return mapper.selectPage(pageParam, new LambdaQueryWrapper<Invoice>()
-                .orderByDesc(Invoice::getCreatedAt));
-    }
-
-    public List<Invoice> getAll() {
-        return mapper.selectList(new LambdaQueryWrapper<Invoice>()
-                .orderByDesc(Invoice::getCreatedAt));
-    }
-
-    public Invoice getById(Long id) {
-        return mapper.selectById(id);
-    }
-
-    public InvoiceDTO getDtoById(Long id) {
-        Invoice entity = mapper.selectById(id);
-        return entity == null ? null : converter.toDto(entity);
-    }
-
-    public Invoice getByInvoiceNo(String invoiceNo) {
-        return mapper.selectOne(new LambdaQueryWrapper<Invoice>()
-                .eq(Invoice::getInvoiceNo, invoiceNo));
+        return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<Invoice>().orderByDesc(Invoice::getCreatedAt));
     }
+    public List<Invoice> getAll() { return mapper.selectList(new LambdaQueryWrapper<Invoice>().orderByDesc(Invoice::getCreatedAt)); }
+    public Invoice getById(Long id) { return mapper.selectById(id); }
+    public InvoiceDTO getDtoById(Long id) { Invoice entity = mapper.selectById(id); return entity == null ? null : converter.toDto(entity); }
+    public Invoice getByInvoiceNo(String invoiceNo) { return mapper.selectOne(new LambdaQueryWrapper<Invoice>().eq(Invoice::getInvoiceNo, invoiceNo)); }
+    public Invoice getByOrderId(Long orderId) { return mapper.selectOne(new LambdaQueryWrapper<Invoice>().eq(Invoice::getOrderId, orderId)); }
 
+    @Transactional
     public Long save(Invoice entity) {
+        validateInvoice(entity);
+        if (entity.getStatus() == null) entity.setStatus(InvoiceStatus.DRAFT.getCode());
+        // 自动计算税额
+        if (entity.getAmount() != null && entity.getTaxRate() != null && entity.getTaxAmount() == null) {
+            entity.setTaxAmount(entity.getAmount().multiply(entity.getTaxRate()));
+        }
+        entity.setCreatedAt(LocalDateTime.now());
+        entity.setUpdatedAt(LocalDateTime.now());
         mapper.insert(entity);
         return entity.getId();
     }
 
+    @Transactional
     public void update(Invoice entity) {
+        validateInvoice(entity);
+        entity.setUpdatedAt(LocalDateTime.now());
         mapper.updateById(entity);
     }
 
     public void delete(Long id) { mapper.deleteById(id); }
-    public Invoice getByOrderId(Long orderId) { return mapper.selectOne(new LambdaQueryWrapper<Invoice>().eq(Invoice::getOrderId, orderId)); }
-    @org.springframework.transaction.annotation.Transactional public void issue(Long id) { Invoice inv = mapper.selectById(id); inv.setStatus("ISSUED"); inv.setInvoiceDate(java.time.LocalDate.now()); mapper.updateById(inv); }
+
+    /**
+     * 开票。只有草稿状态的发票才能开票。
+     */
+    @Transactional
+    public void issue(Long id) {
+        Invoice inv = mapper.selectById(id);
+        if (inv == null) throw new IllegalArgumentException("发票不存在");
+        if (!InvoiceStatus.DRAFT.getCode().equals(inv.getStatus())) {
+            throw new IllegalStateException("只有草稿状态的发票才能开票");
+        }
+        inv.setStatus(InvoiceStatus.ISSUED.getCode());
+        inv.setInvoiceDate(LocalDate.now());
+        inv.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(inv);
+    }
+
+    /**
+     * 作废发票。
+     */
+    @Transactional
+    public void voidInvoice(Long id, String reason) {
+        Invoice inv = mapper.selectById(id);
+        if (inv == null) throw new IllegalArgumentException("发票不存在");
+        if (InvoiceStatus.VOID.getCode().equals(inv.getStatus())) {
+            throw new IllegalStateException("发票已作废");
+        }
+        inv.setStatus(InvoiceStatus.VOID.getCode());
+        inv.setRemark(reason);
+        inv.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(inv);
+    }
+
+    private void validateInvoice(Invoice inv) {
+        if (inv.getInvoiceType() == null || inv.getInvoiceType().isBlank()) {
+            throw new IllegalArgumentException("发票类型不能为空");
+        }
+        if (inv.getAmount() == null || inv.getAmount().signum() <= 0) {
+            throw new IllegalArgumentException("发票金额必须大于0");
+        }
+        if (inv.getBuyerName() == null || inv.getBuyerName().isBlank()) {
+            throw new IllegalArgumentException("购买方名称不能为空");
+        }
+    }
 }

+ 68 - 28
backend/src/main/java/com/oms/service/IqcService.java

@@ -5,10 +5,19 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.IqcConverter;
 import com.oms.dto.IqcDTO;
 import com.oms.entity.Iqc;
+import com.oms.entity.PurchaseArrival;
+import com.oms.entity.PurchaseArrivalItem;
+import com.oms.enums.AuditStatus;
+import com.oms.enums.ValidateStatus;
 import com.oms.mapper.IqcMapper;
+import com.oms.mapper.PurchaseArrivalMapper;
+import com.oms.mapper.PurchaseArrivalItemMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.List;
 
 @Service
@@ -16,42 +25,73 @@ import java.util.List;
 public class IqcService {
     private final IqcMapper mapper;
     private final IqcConverter converter;
+    private final PurchaseArrivalMapper arrivalMapper;
+    private final PurchaseArrivalItemMapper arrivalItemMapper;
+    private final PurchaseArrivalService purchaseArrivalService;
 
     public Page<Iqc> getPage(int page, int size) {
-        Page<Iqc> pageParam = new Page<>(page, size);
-        return mapper.selectPage(pageParam, new LambdaQueryWrapper<Iqc>()
-                .orderByDesc(Iqc::getCreatedAt));
+        return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<Iqc>().orderByDesc(Iqc::getCreatedAt));
     }
+    public List<Iqc> getAll() { return mapper.selectList(new LambdaQueryWrapper<Iqc>().orderByDesc(Iqc::getCreatedAt)); }
+    public Iqc getById(Long id) { return mapper.selectById(id); }
+    public IqcDTO getDtoById(Long id) { Iqc entity = mapper.selectById(id); return entity == null ? null : converter.toDto(entity); }
+    public Iqc getByIqcNo(String iqcNo) { return mapper.selectOne(new LambdaQueryWrapper<Iqc>().eq(Iqc::getIqcNo, iqcNo)); }
+    public List<Iqc> getByArrivalId(Long arrivalId) { return mapper.selectList(new LambdaQueryWrapper<Iqc>().eq(Iqc::getArrivalId, arrivalId).orderByDesc(Iqc::getCreatedAt)); }
+    public Long save(Iqc entity) { mapper.insert(entity); return entity.getId(); }
+    public void update(Iqc entity) { mapper.updateById(entity); }
+    public void delete(Long id) { mapper.deleteById(id); }
 
-    public List<Iqc> getAll() {
-        return mapper.selectList(new LambdaQueryWrapper<Iqc>()
-                .orderByDesc(Iqc::getCreatedAt));
-    }
+    /**
+     * 执行质检。根据合格/不合格数量,联动到货项和库存。
+     *
+     * @param id 质检单ID
+     * @param result 质检结果:PASS / FAIL
+     * @param inspector 质检员
+     */
+    @Transactional
+    public void inspect(Long id, String result, String inspector) {
+        Iqc iqc = mapper.selectById(id);
+        if (iqc == null) throw new IllegalArgumentException("质检单不存在");
 
-    public Iqc getById(Long id) {
-        return mapper.selectById(id);
-    }
+        iqc.setResult(result);
+        iqc.setInspectorName(inspector);
+        iqc.setInspectTime(LocalDateTime.now());
+        mapper.updateById(iqc);
 
-    public IqcDTO getDtoById(Long id) {
-        Iqc entity = mapper.selectById(id);
-        return entity == null ? null : converter.toDto(entity);
-    }
+        // 联动到货项的质检状态
+        if (iqc.getArrivalId() != null) {
+            List<PurchaseArrivalItem> items = arrivalItemMapper.selectList(
+                new LambdaQueryWrapper<PurchaseArrivalItem>()
+                    .eq(PurchaseArrivalItem::getArrivalId, iqc.getArrivalId()));
 
-    public Iqc getByIqcNo(String iqcNo) {
-        return mapper.selectOne(new LambdaQueryWrapper<Iqc>()
-                .eq(Iqc::getIqcNo, iqcNo));
-    }
+            List<Long> passItemIds = new ArrayList<>();
+            List<Long> failItemIds = new ArrayList<>();
 
-    public Long save(Iqc entity) {
-        mapper.insert(entity);
-        return entity.getId();
-    }
+            for (PurchaseArrivalItem item : items) {
+                if (AuditStatus.PENDING.getCode().equals(item.getIqcStatus())) {
+                    // 根据质检结果分配到货项
+                    if (ValidateStatus.PASS.getCode().equals(result)) {
+                        // 全部合格:合格数量 = 实际到货数量
+                        item.setQualifiedQty(item.getActualQty());
+                        item.setUnqualifiedQty(0);
+                        passItemIds.add(item.getId());
+                    } else {
+                        // 全部不合格
+                        item.setQualifiedQty(0);
+                        item.setUnqualifiedQty(item.getActualQty());
+                        failItemIds.add(item.getId());
+                    }
+                }
+            }
 
-    public void update(Iqc entity) {
-        mapper.updateById(entity);
+            // 批量更新质检通过的到货项 → 自动入库 + 更新采购单进度
+            if (!passItemIds.isEmpty()) {
+                purchaseArrivalService.iqcPass(iqc.getArrivalId(), passItemIds, inspector);
+            }
+            // 批量更新质检失败的到货项 → 不入库,更新采购单进度
+            if (!failItemIds.isEmpty()) {
+                purchaseArrivalService.iqcFail(iqc.getArrivalId(), failItemIds, inspector);
+            }
+        }
     }
-
-    public void delete(Long id) { mapper.deleteById(id); }
-    public List<Iqc> getByArrivalId(Long arrivalId) { return mapper.selectList(new LambdaQueryWrapper<Iqc>().eq(Iqc::getArrivalId, arrivalId).orderByDesc(Iqc::getCreatedAt)); }
-    @org.springframework.transaction.annotation.Transactional public void inspect(Long id, String result, String inspector) { Iqc iqc = mapper.selectById(id); iqc.setResult(result); iqc.setInspectorName(inspector); iqc.setInspectTime(java.time.LocalDateTime.now()); mapper.updateById(iqc); }
 }

+ 6 - 5
backend/src/main/java/com/oms/service/KnowledgeBaseService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.KnowledgeBaseConverter;
 import com.oms.dto.KnowledgeBaseDTO;
 import com.oms.entity.KnowledgeBase;
+import com.oms.enums.GenericStatus;
 import com.oms.mapper.KnowledgeBaseMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -44,7 +45,7 @@ public class KnowledgeBaseService {
     public List<KnowledgeBase> getByCategoryId(Long categoryId) {
         return mapper.selectList(new LambdaQueryWrapper<KnowledgeBase>()
                 .eq(KnowledgeBase::getCategoryId, categoryId)
-                .eq(KnowledgeBase::getStatus, "ENABLED")
+                .eq(KnowledgeBase::getStatus, GenericStatus.ENABLED.getCode())
                 .orderByDesc(KnowledgeBase::getClicks));
     }
 
@@ -52,7 +53,7 @@ public class KnowledgeBaseService {
     public Long save(KnowledgeBase entity) {
         validate(entity);
         if (entity.getStatus() == null) {
-            entity.setStatus("ENABLED");
+            entity.setStatus(GenericStatus.ENABLED.getCode());
         }
         if (entity.getClicks() == null) {
             entity.setClicks(0);
@@ -79,14 +80,14 @@ public class KnowledgeBaseService {
     public void toggleEnabled(Long id, boolean enabled) {
         KnowledgeBase kb = mapper.selectById(id);
         if (kb == null) throw new IllegalArgumentException("Knowledge item not found");
-        kb.setStatus(enabled ? "ENABLED" : "DISABLED");
+        kb.setStatus(enabled ? GenericStatus.ENABLED.getCode() : GenericStatus.DISABLED.getCode());
         mapper.updateById(kb);
     }
 
     public List<KnowledgeBase> search(String query) {
         if (query == null || query.isBlank()) return new ArrayList<>();
         return mapper.selectList(new LambdaQueryWrapper<KnowledgeBase>()
-                .eq(KnowledgeBase::getStatus, "ENABLED")
+                .eq(KnowledgeBase::getStatus, GenericStatus.ENABLED.getCode())
                 .like(KnowledgeBase::getQuestion, query)
                 .or()
                 .like(KnowledgeBase::getAnswer, query)
@@ -114,7 +115,7 @@ public class KnowledgeBaseService {
 
     public List<KnowledgeBase> getPopular(int limit) {
         return mapper.selectList(new LambdaQueryWrapper<KnowledgeBase>()
-                .eq(KnowledgeBase::getStatus, "ENABLED")
+                .eq(KnowledgeBase::getStatus, GenericStatus.ENABLED.getCode())
                 .orderByDesc(KnowledgeBase::getClicks)
                 .last("LIMIT " + limit));
     }

+ 201 - 43
backend/src/main/java/com/oms/service/OrdersService.java

@@ -8,14 +8,21 @@ import com.oms.dto.OrderFilterDTO;
 import com.oms.dto.OrderItemDTO;
 import com.oms.dto.OrdersDTO;
 import com.oms.entity.*;
+import com.oms.enums.*;
 import com.oms.mapper.OrdersMapper;
+import com.oms.mapper.ProductSkuMapper;
+import com.oms.mapper.ChannelMapper;
+import com.oms.mapper.WarehouseMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.*;
+import java.util.concurrent.atomic.AtomicLong;
 
 @Service
 @RequiredArgsConstructor
@@ -28,18 +35,24 @@ public class OrdersService {
     private final OrderStatusEventService eventService;
     private final OrderOperationLogService logService;
     private final InventoryService inventoryService;
-
-    private static final Map<String, Set<String>> ALLOWED = Map.of(
-        "CREATED", Set.of("PAID", "CANCELLED"),
-        "PAID", Set.of("ALLOCATED", "CANCELLED"),
-        "ALLOCATED", Set.of("SHIPPED", "CANCELLED"),
-        "SHIPPED", Set.of("DELIVERED"),
-        "DELIVERED", Set.of("COMPLETED"),
-        "COMPLETED", Set.of("REFUNDED")
+    private final ChannelMapper channelMapper;
+    private final WarehouseMapper warehouseMapper;
+    private final ProductSkuMapper productSkuMapper;
+
+    private static final AtomicLong ORDER_SEQ = new AtomicLong(System.currentTimeMillis() % 10000);
+    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+    private static final Map<OrderStatus, Set<OrderStatus>> ALLOWED = Map.of(
+        OrderStatus.CREATED, Set.of(OrderStatus.PAID, OrderStatus.CANCELLED),
+        OrderStatus.PAID, Set.of(OrderStatus.ALLOCATED, OrderStatus.CANCELLED),
+        OrderStatus.ALLOCATED, Set.of(OrderStatus.SHIPPED, OrderStatus.CANCELLED),
+        OrderStatus.SHIPPED, Set.of(OrderStatus.DELIVERED),
+        OrderStatus.DELIVERED, Set.of(OrderStatus.COMPLETED),
+        OrderStatus.COMPLETED, Set.of(OrderStatus.REFUNDED)
     );
 
-    private void validateTransition(String current, String target) {
-        if ("CANCELLED".equals(target)) return;
+    private void validateTransition(OrderStatus current, OrderStatus target) {
+        if (OrderStatus.CANCELLED == target) return;
         if (!ALLOWED.getOrDefault(current, Set.of()).contains(target))
             throw new IllegalStateException("Cannot transition from " + current + " to " + target);
     }
@@ -96,7 +109,47 @@ public class OrdersService {
         return e == null ? null : converter.toDto(e);
     }
 
-    public Long save(Orders entity) { mapper.insert(entity); return entity.getId(); }
+    @Transactional
+    public Long save(Orders entity) {
+        // 校验渠道
+        if (entity.getChannelId() != null) {
+            Channel channel = channelMapper.selectById(entity.getChannelId());
+            if (channel == null) throw new IllegalStateException("渠道不存在: " + entity.getChannelId());
+        }
+        // 校验仓库
+        if (entity.getWarehouseId() != null) {
+            Warehouse wh = warehouseMapper.selectById(entity.getWarehouseId());
+            if (wh == null) throw new IllegalStateException("仓库不存在: " + entity.getWarehouseId());
+        }
+        // 自动生成订单号
+        if (entity.getOrderNo() == null || entity.getOrderNo().isEmpty()) {
+            entity.setOrderNo(generateOrderNo(entity.getChannelId()));
+        }
+        // 设置默认状态
+        if (entity.getOrderStatus() == null) entity.setOrderStatus(OrderStatus.CREATED);
+        if (entity.getShippingStatus() == null) entity.setShippingStatus(ShippingStatus.UNSHIPPED);
+        if (entity.getPaymentStatus() == null) entity.setPaymentStatus(PaymentStatus.UNPAID);
+        if (entity.getRefundStatus() == null) entity.setRefundStatus(RefundStatus.NONE);
+        if (entity.getCurrency() == null) entity.setCurrency("USD");
+        if (entity.getExchangeRate() == null) entity.setExchangeRate(BigDecimal.ONE);
+        entity.setCreatedAt(LocalDateTime.now());
+        entity.setUpdatedAt(LocalDateTime.now());
+        mapper.insert(entity);
+        return entity.getId();
+    }
+
+    private String generateOrderNo(Long channelId) {
+        String channelCode = "OMS";
+        if (channelId != null) {
+            Channel ch = channelMapper.selectById(channelId);
+            if (ch != null && ch.getChannelCode() != null) {
+                channelCode = ch.getChannelCode().toUpperCase();
+            }
+        }
+        String date = LocalDate.now().format(DATE_FMT);
+        long seq = ORDER_SEQ.incrementAndGet() % 100000;
+        return channelCode + "-" + date + "-" + String.format("%05d", seq);
+    }
 
     public void update(Orders entity) { mapper.updateById(entity); }
 
@@ -105,86 +158,151 @@ public class OrdersService {
     @Transactional
     public void confirmPayment(Long id, String operator) {
         Orders o = mapper.selectById(id);
-        String prev = o.getOrderStatus();
-        validateTransition(prev, "PAID");
-        o.setOrderStatus("PAID");
-        o.setPaymentStatus("PAID");
+        OrderStatus prev = o.getOrderStatus();
+        validateTransition(prev, OrderStatus.PAID);
+        o.setOrderStatus(OrderStatus.PAID);
+        o.setPaymentStatus(PaymentStatus.PAID);
         o.setPaidAt(LocalDateTime.now());
         mapper.updateById(o);
-        eventService.logEvent(o.getId(), "PAID", "支付确认", "状态 " + prev + " → PAID", "primary", operator);
-        logService.log(o.getId(), "ORDER", "支付确认", "金额 " + o.getOrderAmount(), operator);
+        eventService.logEvent(o.getId(), OrderStatus.PAID.name(), "支付确认", "状态 " + prev + " → PAID", "primary", operator);
+        logService.log(o.getId(), "ORDER", "支付确认", "金额 " + o.getActualPaid(), operator);
         allocateOrder(id, operator);
     }
 
+    @Transactional
+    public void confirmPayment(Long id, BigDecimal paidAmount, String transactionId, String operator) {
+        Orders o = mapper.selectById(id);
+        OrderStatus prev = o.getOrderStatus();
+        validateTransition(prev, OrderStatus.PAID);
+        // 校验支付金额
+        if (paidAmount != null && o.getActualPaid() != null) {
+            int cmp = paidAmount.compareTo(o.getActualPaid());
+            if (cmp < 0) {
+                o.setPaymentStatus(PaymentStatus.PARTIAL_PAID);
+            } else {
+                o.setPaymentStatus(PaymentStatus.PAID);
+            }
+        } else {
+            o.setPaymentStatus(PaymentStatus.PAID);
+        }
+        o.setOrderStatus(OrderStatus.PAID);
+        o.setPaidAt(LocalDateTime.now());
+        if (transactionId != null && !transactionId.isEmpty()) {
+            o.setTransactionId(transactionId);
+        }
+        mapper.updateById(o);
+        eventService.logEvent(o.getId(), OrderStatus.PAID.name(), "支付确认", "状态 " + prev + " → " + o.getOrderStatus() + ", 金额 " + paidAmount, "primary", operator);
+        logService.log(o.getId(), "ORDER", "支付确认", "金额 " + paidAmount + ", 流水号 " + transactionId, operator);
+        if (PaymentStatus.PAID == o.getPaymentStatus()) {
+            allocateOrder(id, operator);
+        }
+    }
+
     @Transactional
     public void allocateOrder(Long id, String operator) {
         Orders o = mapper.selectById(id);
-        validateTransition(o.getOrderStatus(), "ALLOCATED");
+        validateTransition(o.getOrderStatus(), OrderStatus.ALLOCATED);
         List<OrderItem> items = orderItemService.getByOrderId(id);
+        // 校验库存是否充足
+        for (OrderItem item : items) {
+            List<Inventory> invList = inventoryService.getBySkuId(item.getSkuId());
+            if (invList.isEmpty() || invList.get(0).getAvailable() < item.getQty()) {
+                throw new IllegalStateException("SKU " + item.getSkuId() + " 库存不足,无法分配");
+            }
+        }
         for (OrderItem item : items) {
             inventoryService.lockInventoryBySku(item.getSkuId(), item.getQty(), operator);
         }
-        o.setOrderStatus("ALLOCATED");
+        o.setOrderStatus(OrderStatus.ALLOCATED);
         mapper.updateById(o);
-        eventService.logEvent(o.getId(), "ALLOCATED", "库存锁定完成", items.size() + " 个SKU已锁定", "success", operator);
+        eventService.logEvent(o.getId(), OrderStatus.ALLOCATED.name(), "库存锁定完成", items.size() + " 个SKU已锁定", "success", operator);
     }
 
     @Transactional
     public void cancelOrder(Long id, String reason, String operator) {
         Orders o = mapper.selectById(id);
-        if ("CANCELLED".equals(o.getOrderStatus())) throw new IllegalStateException("Already cancelled");
-        if ("ALLOCATED".equals(o.getOrderStatus())) {
+        if (OrderStatus.CANCELLED == o.getOrderStatus()) throw new IllegalStateException("Already cancelled");
+        if (OrderStatus.ALLOCATED == o.getOrderStatus()) {
             for (OrderItem item : orderItemService.getByOrderId(id)) {
                 inventoryService.unlockInventoryBySku(item.getSkuId(), item.getQty(), operator);
             }
         }
-        o.setOrderStatus("CANCELLED");
+        o.setOrderStatus(OrderStatus.CANCELLED);
         mapper.updateById(o);
-        eventService.logEvent(o.getId(), "CANCELLED", "订单已取消", reason, "danger", operator);
+        eventService.logEvent(o.getId(), OrderStatus.CANCELLED.name(), "订单已取消", reason, "danger", operator);
         logService.log(o.getId(), "ORDER", "取消订单", reason, operator);
     }
 
     @Transactional
     public void confirmShipped(Long id, String operator) {
         Orders o = mapper.selectById(id);
-        validateTransition(o.getOrderStatus(), "SHIPPED");
-        o.setOrderStatus("SHIPPED");
-        o.setShippingStatus("SHIPPED");
+        validateTransition(o.getOrderStatus(), OrderStatus.SHIPPED);
+        // 扣减锁定库存
+        List<OrderItem> items = orderItemService.getByOrderId(id);
+        for (OrderItem item : items) {
+            inventoryService.shipInventoryBySku(item.getSkuId(), item.getQty(), operator);
+        }
+        o.setOrderStatus(OrderStatus.SHIPPED);
+        o.setShippingStatus(ShippingStatus.SHIPPED);
         o.setShippedAt(LocalDateTime.now());
         mapper.updateById(o);
-        eventService.logEvent(o.getId(), "SHIPPED", "已发货", "物流信息已录入", "primary", operator);
+        eventService.logEvent(o.getId(), OrderStatus.SHIPPED.name(), "已发货", "物流信息已录入", "primary", operator);
     }
 
     @Transactional
     public void confirmDelivered(Long id, String operator) {
         Orders o = mapper.selectById(id);
-        validateTransition(o.getOrderStatus(), "DELIVERED");
-        o.setOrderStatus("DELIVERED");
+        validateTransition(o.getOrderStatus(), OrderStatus.DELIVERED);
+        o.setOrderStatus(OrderStatus.DELIVERED);
         o.setDeliveredAt(LocalDateTime.now());
         mapper.updateById(o);
-        eventService.logEvent(o.getId(), "DELIVERED", "已签收", "买家已确认收货", "success", operator);
+        eventService.logEvent(o.getId(), OrderStatus.DELIVERED.name(), "已签收", "买家已确认收货", "success", operator);
     }
 
     @Transactional
     public void completeOrder(Long id, String operator) {
         Orders o = mapper.selectById(id);
-        validateTransition(o.getOrderStatus(), "COMPLETED");
-        o.setOrderStatus("COMPLETED");
+        validateTransition(o.getOrderStatus(), OrderStatus.COMPLETED);
+        o.setOrderStatus(OrderStatus.COMPLETED);
         mapper.updateById(o);
-        eventService.logEvent(o.getId(), "COMPLETED", "订单完成", "订单已完结", "success", operator);
+        eventService.logEvent(o.getId(), OrderStatus.COMPLETED.name(), "订单完成", "订单已完结", "success", operator);
     }
 
     @Transactional
     public Long splitOrder(Long id, List<Map<String, Object>> splits, String operator) {
         Orders orig = mapper.selectById(id);
         List<OrderItem> origItems = orderItemService.getByOrderId(id);
+        // 校验原订单状态:只有已支付/已分配的订单可以拆单
+        if (!Set.of(OrderStatus.PAID, OrderStatus.ALLOCATED).contains(orig.getOrderStatus())) {
+            throw new IllegalStateException("只有已支付或已分配的订单可以拆单");
+        }
+        // 校验拆单数量不超过原订单
+        for (Map<String, Object> s : splits) {
+            Long skuId = Long.valueOf(s.get("skuId").toString());
+            int splitQty = Integer.parseInt(s.get("qty").toString());
+            OrderItem origItem = origItems.stream().filter(i -> i.getSkuId().equals(skuId)).findFirst().orElse(null);
+            if (origItem == null) throw new IllegalStateException("SKU " + skuId + " 不在原订单中");
+            if (splitQty <= 0 || splitQty >= origItem.getQty()) {
+                throw new IllegalStateException("拆单数量必须大于0且小于原数量");
+            }
+        }
+        // 校验库存是否足够分别履约
+        for (Map<String, Object> s : splits) {
+            Long skuId = Long.valueOf(s.get("skuId").toString());
+            int splitQty = Integer.parseInt(s.get("qty").toString());
+            List<Inventory> invList = inventoryService.getBySkuId(skuId);
+            if (invList.isEmpty() || invList.get(0).getAvailable() < splitQty) {
+                throw new IllegalStateException("SKU " + skuId + " 库存不足,无法拆单");
+            }
+        }
+
         Orders child = new Orders();
         child.setOrderNo(orig.getOrderNo() + "-S");
         child.setChannelOrderNo(orig.getChannelOrderNo());
         child.setChannelId(orig.getChannelId());
-        child.setOrderStatus("CREATED");
+        child.setOrderStatus(OrderStatus.CREATED);
         child.setPaymentStatus(orig.getPaymentStatus());
-        child.setShippingStatus("UNSHIPPED");
+        child.setShippingStatus(ShippingStatus.UNSHIPPED);
         child.setBuyer(orig.getBuyer());
         child.setBuyerId(orig.getBuyerId());
         child.setReceiverName(orig.getReceiverName());
@@ -193,6 +311,8 @@ public class OrdersService {
         child.setCurrency(orig.getCurrency());
         child.setParentOrderId(orig.getId());
         child.setWarehouseId(orig.getWarehouseId());
+        child.setCreatedAt(LocalDateTime.now());
+        child.setUpdatedAt(LocalDateTime.now());
         mapper.insert(child);
 
         BigDecimal amount = BigDecimal.ZERO;
@@ -213,19 +333,50 @@ public class OrdersService {
                     orderItemService.save(ci);
                     amount = amount.add(ci.getSubtotal());
                     count += qty;
+                    // 原订单减少数量
+                    oi.setQty(oi.getQty() - qty);
+                    oi.setSubtotal(oi.getPrice().multiply(BigDecimal.valueOf(oi.getQty())));
+                    orderItemService.update(oi);
                     break;
                 }
             }
         }
         child.setOrderAmount(amount);
         child.setItemCount(count);
+        // 按比例分摊运费和税
+        BigDecimal ratio = amount.divide(orig.getOrderAmount(), 4, java.math.RoundingMode.HALF_UP);
+        child.setShippingFee(orig.getShippingFee().multiply(ratio).setScale(2, java.math.RoundingMode.HALF_UP));
+        child.setTaxAmount(orig.getTaxAmount().multiply(ratio).setScale(2, java.math.RoundingMode.HALF_UP));
+        child.setActualPaid(amount.add(child.getShippingFee()).add(child.getTaxAmount()));
         mapper.updateById(child);
+
+        // 更新原订单金额
+        orderItemService.calculateTotals(id);
+        orig.setActualPaid(orig.getOrderAmount().add(orig.getShippingFee()).add(orig.getTaxAmount()));
+        mapper.updateById(orig);
+
         eventService.logEvent(orig.getId(), "SPLIT", "拆单完成", "子订单 " + child.getOrderNo(), "primary", operator);
         return child.getId();
     }
 
     @Transactional
     public void mergeOrders(Long sourceId, Long targetId, String operator) {
+        Orders source = mapper.selectById(sourceId);
+        Orders target = mapper.selectById(targetId);
+        // 合单校验:同买家、同仓库、同币种、均未发货
+        if (OrderStatus.CREATED != source.getOrderStatus() && OrderStatus.PAID != source.getOrderStatus()) {
+            throw new IllegalStateException("来源订单状态不允许合单");
+        }
+        if (OrderStatus.CREATED != target.getOrderStatus() && OrderStatus.PAID != target.getOrderStatus()) {
+            throw new IllegalStateException("目标订单状态不允许合单");
+        }
+        if (source.getWarehouseId() != null && !source.getWarehouseId().equals(target.getWarehouseId())) {
+            throw new IllegalStateException("不同仓库的订单不能合并");
+        }
+        if (source.getCurrency() != null && !source.getCurrency().equals(target.getCurrency())) {
+            throw new IllegalStateException("不同币种的订单不能合并");
+        }
+
         for (OrderItem item : orderItemService.getByOrderId(sourceId)) {
             OrderItem moved = new OrderItem();
             moved.setOrderId(targetId);
@@ -237,11 +388,18 @@ public class OrdersService {
             moved.setSubtotal(item.getSubtotal());
             orderItemService.save(moved);
         }
-        Orders source = mapper.selectById(sourceId);
-        source.setOrderStatus("CANCELLED");
+        source.setOrderStatus(OrderStatus.CANCELLED);
         source.setMergeOrderId(targetId);
+        source.setUpdatedAt(LocalDateTime.now());
         mapper.updateById(source);
         orderItemService.calculateTotals(targetId);
+        // 合单后重新计算运费(简单策略:取两者较大值)
+        target = mapper.selectById(targetId);
+        BigDecimal newShipping = source.getShippingFee().max(target.getShippingFee());
+        target.setShippingFee(newShipping);
+        target.setActualPaid(target.getOrderAmount().add(newShipping).add(target.getTaxAmount()));
+        target.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(target);
         eventService.logEvent(targetId, "MERGED", "合单完成", "合并来源 " + source.getOrderNo(), "primary", operator);
     }
 
@@ -268,12 +426,12 @@ public class OrdersService {
         order.setOrderNo("OMS-" + System.currentTimeMillis());
         order.setChannelOrderNo("CH" + System.currentTimeMillis());
         order.setChannelId((long) (Math.random() * 3 + 1));
-        order.setOrderStatus("CREATED");
-        order.setShippingStatus("UNSHIPPED");
-        order.setPaymentStatus("UNPAID");
-        order.setRefundStatus("NONE");
+        order.setOrderStatus(OrderStatus.CREATED);
+        order.setShippingStatus(ShippingStatus.UNSHIPPED);
+        order.setPaymentStatus(PaymentStatus.UNPAID);
+        order.setRefundStatus(RefundStatus.NONE);
         order.setExceptionTag(Math.random() > 0.85 ? "地址需复核" : null);
-        order.setPriority(Math.random() > 0.9 ? "URGENT" : "NORMAL");
+        order.setPriority(Math.random() > 0.9 ? Priority.URGENT : Priority.NORMAL);
 
         int buyerIdx = (int) (Math.random() * BUYERS.length);
         order.setBuyer(BUYERS[buyerIdx]);

+ 17 - 16
backend/src/main/java/com/oms/service/PriceWatchService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.PriceWatchConverter;
 import com.oms.dto.PriceWatchDTO;
 import com.oms.entity.PriceWatch;
+import com.oms.enums.AlertStatus;
 import com.oms.mapper.PriceWatchMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -55,7 +56,7 @@ public class PriceWatchService {
 
     public List<PriceWatch> getAlerts() {
         return mapper.selectList(new LambdaQueryWrapper<PriceWatch>()
-                .eq(PriceWatch::getAlertStatus, "ALERT")
+                .eq(PriceWatch::getAlertStatus, AlertStatus.ALERT.getCode())
                 .orderByDesc(PriceWatch::getCreatedAt));
     }
 
@@ -63,7 +64,7 @@ public class PriceWatchService {
     public Long save(PriceWatch entity) {
         validatePriceWatch(entity);
         if (entity.getAlertStatus() == null) {
-            entity.setAlertStatus("NORMAL");
+            entity.setAlertStatus(AlertStatus.NORMAL.getCode());
         }
         entity.setLastUpdate(LocalDateTime.now());
         mapper.insert(entity);
@@ -112,40 +113,40 @@ public class PriceWatchService {
         if (pw == null) return "NOT_FOUND";
         
         if (pw.getCompetitorPrice() == null || pw.getLocalPrice() == null) {
-            return "NORMAL";
+            return AlertStatus.NORMAL.getCode();
         }
-        
+
         BigDecimal diff = pw.getCompetitorPrice().subtract(pw.getLocalPrice());
         BigDecimal diffPercent = diff.divide(pw.getLocalPrice(), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100));
-        
+
         if (diffPercent.compareTo(BigDecimal.valueOf(-10)) < 0) {
-            return "CRITICAL";
+            return AlertStatus.CRITICAL.getCode();
         } else if (diffPercent.compareTo(BigDecimal.valueOf(-5)) < 0) {
-            return "ALERT";
+            return AlertStatus.ALERT.getCode();
         } else if (diffPercent.compareTo(BigDecimal.valueOf(5)) > 0) {
-            return "HIGH";
+            return AlertStatus.HIGH.getCode();
         }
-        return "NORMAL";
+        return AlertStatus.NORMAL.getCode();
     }
 
     private void recalculateAlert(PriceWatch pw) {
         if (pw.getCompetitorPrice() == null || pw.getLocalPrice() == null) {
-            pw.setAlertStatus("NORMAL");
+            pw.setAlertStatus(AlertStatus.NORMAL.getCode());
             pw.setPriceDiff(BigDecimal.ZERO);
             return;
         }
-        
+
         BigDecimal diff = pw.getCompetitorPrice().subtract(pw.getLocalPrice());
         pw.setPriceDiff(diff);
-        
+
         BigDecimal diffPercent = diff.divide(pw.getLocalPrice(), 4, RoundingMode.HALF_UP).multiply(BigDecimal.valueOf(100));
-        
+
         if (diffPercent.compareTo(BigDecimal.valueOf(-10)) < 0) {
-            pw.setAlertStatus("CRITICAL");
+            pw.setAlertStatus(AlertStatus.CRITICAL.getCode());
         } else if (diffPercent.compareTo(BigDecimal.valueOf(-5)) < 0) {
-            pw.setAlertStatus("ALERT");
+            pw.setAlertStatus(AlertStatus.ALERT.getCode());
         } else {
-            pw.setAlertStatus("NORMAL");
+            pw.setAlertStatus(AlertStatus.NORMAL.getCode());
         }
     }
 

+ 10 - 4
backend/src/main/java/com/oms/service/PricingRuleService.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.oms.entity.PricingRule;
 import com.oms.mapper.PricingRuleMapper;
 import com.oms.dto.PricingRuleDTO;
+import com.oms.enums.GenericStatus;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import java.math.BigDecimal;
@@ -39,7 +40,7 @@ public class PricingRuleService {
         rule.setDiscountRate(dto.getDiscountRate());
         rule.setFixedPrice(dto.getFixedPrice());
         rule.setPriority(dto.getPriority());
-        rule.setStatus("ACTIVE");
+        rule.setStatus(GenericStatus.ACTIVE.getCode());
         rule.setStartDate(dto.getStartDate());
         rule.setEndDate(dto.getEndDate());
         rule.setCreatedAt(LocalDateTime.now());
@@ -73,11 +74,16 @@ public class PricingRuleService {
         LambdaQueryWrapper<PricingRule> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(PricingRule::getProductSkuId, skuId.toString())
                .eq(PricingRule::getChannelId, channelId)
-               .eq(PricingRule::getStatus, "ACTIVE");
+               .eq(PricingRule::getStatus, GenericStatus.ACTIVE.getCode());
         PricingRule rule = mapper.selectOne(wrapper);
-        
+
         if (rule == null) return basePrice;
-        
+
+        // 校验规则有效期
+        LocalDateTime now = LocalDateTime.now();
+        if (rule.getStartDate() != null && now.isBefore(rule.getStartDate())) return basePrice;
+        if (rule.getEndDate() != null && now.isAfter(rule.getEndDate())) return basePrice;
+
         if (rule.getFixedPrice() != null) {
             return rule.getFixedPrice();
         }

+ 5 - 3
backend/src/main/java/com/oms/service/ProductService.java

@@ -7,6 +7,8 @@ import com.oms.dto.ProductDTO;
 import com.oms.dto.ProductFilterDTO;
 import com.oms.entity.Product;
 import com.oms.entity.ProductSku;
+import com.oms.enums.GenericStatus;
+import com.oms.enums.ProductStatus;
 import com.oms.mapper.ProductMapper;
 import com.oms.mapper.ProductSkuMapper;
 import lombok.RequiredArgsConstructor;
@@ -44,10 +46,10 @@ public class ProductService {
         Product p = mapper.selectById(id);
         Long skuCount = skuMapper.selectCount(new LambdaQueryWrapper<ProductSku>().eq(ProductSku::getProductId, id));
         if (skuCount == 0) throw new IllegalStateException("Cannot publish without SKUs");
-        p.setStatus("LISTED"); mapper.updateById(p);
+        p.setStatus(ProductStatus.LISTED.getCode()); mapper.updateById(p);
     }
     @Transactional
-    public void unpublish(Long id) { Product p = mapper.selectById(id); p.setStatus("DELISTED"); mapper.updateById(p); }
+    public void unpublish(Long id) { Product p = mapper.selectById(id); p.setStatus(ProductStatus.DELISTED.getCode()); mapper.updateById(p); }
 
     @Transactional
     public Long copy(Long id) {
@@ -56,7 +58,7 @@ public class ProductService {
         c.setSpu(o.getSpu() + "-COPY-" + System.currentTimeMillis() % 10000); c.setTitle(o.getTitle() + " (副本)");
         c.setSubtitle(o.getSubtitle()); c.setCategoryId(o.getCategoryId()); c.setBrand(o.getBrand()); c.setTags(o.getTags());
         c.setDescription(o.getDescription()); c.setSpecs(o.getSpecs()); c.setTranslations(o.getTranslations());
-        c.setStatus("DRAFT"); c.setChannelStatus("INACTIVE"); c.setOwner(o.getOwner()); c.setImage(o.getImage());
+        c.setStatus(ProductStatus.DRAFT.getCode()); c.setChannelStatus(GenericStatus.INACTIVE.getCode()); c.setOwner(o.getOwner()); c.setImage(o.getImage());
         c.setImages(o.getImages()); c.setSkuCount(0);
         mapper.insert(c); return c.getId();
     }

+ 8 - 7
backend/src/main/java/com/oms/service/PromotionService.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.PromotionConverter;
 import com.oms.dto.PromotionDTO;
 import com.oms.entity.Promotion;
+import com.oms.enums.GenericStatus;
 import com.oms.mapper.PromotionMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -50,7 +51,7 @@ public class PromotionService {
         LocalDateTime now = LocalDateTime.now();
         return mapper.selectList(new LambdaQueryWrapper<Promotion>()
                 .eq(Promotion::getChannelId, channelId)
-                .eq(Promotion::getStatus, "ACTIVE")
+                .eq(Promotion::getStatus, GenericStatus.ACTIVE.getCode())
                 .le(Promotion::getStartTime, now)
                 .ge(Promotion::getEndTime, now)
                 .orderByDesc(Promotion::getCreatedAt));
@@ -60,7 +61,7 @@ public class PromotionService {
     public Long save(Promotion entity) {
         validatePromotion(entity);
         if (entity.getStatus() == null) {
-            entity.setStatus("DRAFT");
+            entity.setStatus(GenericStatus.DRAFT.getCode());
         }
         mapper.insert(entity);
         return entity.getId();
@@ -91,7 +92,7 @@ public class PromotionService {
         if (p.getEndTime().isBefore(now)) {
             throw new IllegalStateException("Cannot activate after end date");
         }
-        p.setStatus("ACTIVE");
+        p.setStatus(GenericStatus.ACTIVE.getCode());
         mapper.updateById(p);
     }
 
@@ -99,7 +100,7 @@ public class PromotionService {
     public void deactivate(Long id) {
         Promotion p = mapper.selectById(id);
         if (p == null) throw new IllegalArgumentException("Promotion not found");
-        p.setStatus("INACTIVE");
+        p.setStatus(GenericStatus.INACTIVE.getCode());
         mapper.updateById(p);
     }
 
@@ -108,13 +109,13 @@ public class PromotionService {
         Promotion p = mapper.selectById(id);
         if (p == null) throw new IllegalArgumentException("Promotion not found");
         validatePromotion(p);
-        p.setStatus("PENDING");
+        p.setStatus(GenericStatus.PENDING.getCode());
         mapper.updateById(p);
     }
 
     public BigDecimal calculateDiscount(Long promotionId, BigDecimal originalPrice) {
         Promotion p = mapper.selectById(promotionId);
-        if (p == null || !"ACTIVE".equals(p.getStatus())) {
+        if (p == null || !GenericStatus.ACTIVE.getCode().equals(p.getStatus())) {
             return originalPrice;
         }
         LocalDateTime now = LocalDateTime.now();
@@ -142,7 +143,7 @@ public class PromotionService {
     public boolean isValid(Long promotionId) {
         Promotion p = mapper.selectById(promotionId);
         if (p == null) return false;
-        if (!"ACTIVE".equals(p.getStatus())) return false;
+        if (!GenericStatus.ACTIVE.getCode().equals(p.getStatus())) return false;
         LocalDateTime now = LocalDateTime.now();
         if (p.getStartTime() != null && now.isBefore(p.getStartTime())) return false;
         if (p.getEndTime() != null && now.isAfter(p.getEndTime())) return false;

+ 98 - 5
backend/src/main/java/com/oms/service/PurchaseArrivalService.java

@@ -1,16 +1,22 @@
 package com.oms.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.PurchaseArrivalConverter;
 import com.oms.dto.PurchaseArrivalDTO;
 import com.oms.entity.PurchaseArrival;
 import com.oms.entity.PurchaseArrivalItem;
+import com.oms.entity.PurchaseOrderItem;
+import com.oms.enums.ArrivalStatus;
+import com.oms.enums.ValidateStatus;
 import com.oms.mapper.PurchaseArrivalMapper;
 import com.oms.mapper.PurchaseArrivalItemMapper;
+import com.oms.mapper.PurchaseOrderItemMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Service
@@ -19,8 +25,12 @@ public class PurchaseArrivalService {
     private final PurchaseArrivalMapper mapper;
     private final PurchaseArrivalConverter converter;
     private final PurchaseArrivalItemMapper itemMapper;
+    private final PurchaseOrderItemMapper poItemMapper;
     private final PurchaseOrderService purchaseOrderService;
 
+    public Page<PurchaseArrival> getPage(int page, int size) {
+        return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<PurchaseArrival>().orderByDesc(PurchaseArrival::getCreatedAt));
+    }
     public PurchaseArrival getById(Long id) { return mapper.selectById(id); }
     public PurchaseArrivalDTO getDtoById(Long id) { PurchaseArrival e = mapper.selectById(id); return e == null ? null : converter.toDto(e); }
     public List<PurchaseArrival> getByPurchaseOrderId(Long poId) { return mapper.selectList(new LambdaQueryWrapper<PurchaseArrival>().eq(PurchaseArrival::getPurchaseOrderId, poId).orderByDesc(PurchaseArrival::getCreatedAt)); }
@@ -30,15 +40,98 @@ public class PurchaseArrivalService {
     public void update(PurchaseArrival entity) { mapper.updateById(entity); }
     public void delete(Long id) { mapper.deleteById(id); }
 
+    /**
+     * 确认到货。校验到货数量不超过采购数量,记录到货项,更新采购单项累计到货,更新采购单进度。
+     */
     @Transactional
     public void confirmArrival(Long id, List<PurchaseArrivalItem> items, String operator) {
-        PurchaseArrival a = mapper.selectById(id); a.setStatus("ARRIVED"); mapper.updateById(a);
-        for (PurchaseArrivalItem item : items) { item.setArrivalId(id); itemMapper.insert(item);
-            if (item.getQualifiedQty() > 0) purchaseOrderService.updateInventoryOnArrival(item.getSkuId(), a.getWarehouseId(), item.getQualifiedQty(), operator, a.getPurchaseOrderId(), a.getArrivalNo());
+        PurchaseArrival a = mapper.selectById(id);
+        if (a == null) throw new IllegalArgumentException("到货单不存在");
+        if (ArrivalStatus.ARRIVED.getCode().equals(a.getStatus())) {
+            throw new IllegalStateException("该到货单已确认,不能重复确认");
+        }
+        a.setStatus(ArrivalStatus.ARRIVED.getCode());
+        a.setArrivedAt(LocalDateTime.now());
+        mapper.updateById(a);
+
+        for (PurchaseArrivalItem item : items) {
+            item.setArrivalId(id);
+            // 校验到货数量不超过采购数量
+            if (item.getPurchaseOrderItemId() != null) {
+                PurchaseOrderItem poItem = poItemMapper.selectById(item.getPurchaseOrderItemId());
+                if (poItem != null) {
+                    int remaining = poItem.getQty() - poItem.getArrivedQty();
+                    if (item.getActualQty() > remaining) {
+                        throw new IllegalStateException("SKU " + poItem.getSku() + " 到货数量(" + item.getActualQty()
+                            + ")超过剩余采购数量(" + remaining + ")");
+                    }
+                    item.setExpectedQty(poItem.getQty());
+                }
+            }
+            itemMapper.insert(item);
         }
+
+        // 更新采购单进度
+        purchaseOrderService.updateArrivalProgress(a.getPurchaseOrderId());
     }
+
+    /**
+     * 质检通过:更新到货项合格数量,更新采购单项合格数量,自动入库,更新采购单进度。
+     */
     @Transactional
-    public void iqcPass(Long arrivalId, List<Long> itemIds) { for (Long iid : itemIds) { PurchaseArrivalItem i = itemMapper.selectById(iid); i.setIqcStatus("PASS"); itemMapper.updateById(i); } }
+    public void iqcPass(Long arrivalId, List<Long> itemIds, String operator) {
+        PurchaseArrival arrival = mapper.selectById(arrivalId);
+        for (Long iid : itemIds) {
+            PurchaseArrivalItem item = itemMapper.selectById(iid);
+            if (item == null) continue;
+
+            // 如果合格数量未设置,默认全部合格
+            if (item.getQualifiedQty() == null || item.getQualifiedQty() == 0) {
+                item.setQualifiedQty(item.getActualQty());
+                item.setUnqualifiedQty(0);
+            }
+            item.setIqcStatus(ValidateStatus.PASS.getCode());
+            itemMapper.updateById(item);
+
+            // 更新采购单项累计合格数量
+            if (item.getPurchaseOrderItemId() != null) {
+                purchaseOrderService.addItemArrivedQty(item.getPurchaseOrderItemId(), 0, item.getQualifiedQty());
+            }
+
+            // 合格数量自动入库
+            if (item.getQualifiedQty() > 0) {
+                purchaseOrderService.updateInventoryOnArrival(
+                    item.getSkuId(), arrival.getWarehouseId(), item.getQualifiedQty(),
+                    operator, arrival.getPurchaseOrderId(), arrival.getArrivalNo());
+            }
+        }
+
+        // 更新采购单进度
+        purchaseOrderService.updateArrivalProgress(arrival.getPurchaseOrderId());
+    }
+
+    /**
+     * 质检不合格:更新到货项不合格数量,更新采购单项(合格数量为0),不入库。
+     */
     @Transactional
-    public void iqcFail(Long arrivalId, List<Long> itemIds) { for (Long iid : itemIds) { PurchaseArrivalItem i = itemMapper.selectById(iid); i.setIqcStatus("FAIL"); itemMapper.updateById(i); } }
+    public void iqcFail(Long arrivalId, List<Long> itemIds, String operator) {
+        PurchaseArrival arrival = mapper.selectById(arrivalId);
+        for (Long iid : itemIds) {
+            PurchaseArrivalItem item = itemMapper.selectById(iid);
+            if (item == null) continue;
+
+            item.setQualifiedQty(0);
+            item.setUnqualifiedQty(item.getActualQty());
+            item.setIqcStatus(ValidateStatus.FAIL.getCode());
+            itemMapper.updateById(item);
+
+            // 更新采购单项(到货数量计入,但合格数量为0)
+            if (item.getPurchaseOrderItemId() != null) {
+                purchaseOrderService.addItemArrivedQty(item.getPurchaseOrderItemId(), item.getActualQty(), 0);
+            }
+        }
+
+        // 更新采购单进度
+        purchaseOrderService.updateArrivalProgress(arrival.getPurchaseOrderId());
+    }
 }

+ 200 - 6
backend/src/main/java/com/oms/service/PurchaseOrderService.java

@@ -7,6 +7,8 @@ import com.oms.dto.PurchaseOrderDTO;
 import com.oms.entity.Inventory;
 import com.oms.entity.PurchaseOrder;
 import com.oms.entity.PurchaseOrderItem;
+import com.oms.enums.ArrivalStatus;
+import com.oms.enums.PurchaseOrderStatus;
 import com.oms.mapper.PurchaseOrderMapper;
 import com.oms.mapper.PurchaseOrderItemMapper;
 import lombok.RequiredArgsConstructor;
@@ -14,7 +16,13 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
 
 @Service
 @RequiredArgsConstructor
@@ -24,28 +32,214 @@ public class PurchaseOrderService {
     private final PurchaseOrderItemMapper itemMapper;
     private final InventoryService inventoryService;
 
+    private static final AtomicLong PO_SEQ = new AtomicLong(System.currentTimeMillis() % 10000);
+    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
+
+    // 状态机:每个状态允许跳转到哪些状态
+    private static final Map<PurchaseOrderStatus, Set<PurchaseOrderStatus>> ALLOWED = Map.of(
+        PurchaseOrderStatus.DRAFT, Set.of(PurchaseOrderStatus.SUBMITTED, PurchaseOrderStatus.CANCELLED),
+        PurchaseOrderStatus.SUBMITTED, Set.of(PurchaseOrderStatus.APPROVED, PurchaseOrderStatus.REJECTED),
+        PurchaseOrderStatus.APPROVED, Set.of(PurchaseOrderStatus.ORDERED, PurchaseOrderStatus.CANCELLED),
+        PurchaseOrderStatus.ORDERED, Set.of(PurchaseOrderStatus.PARTIAL_ARRIVED, PurchaseOrderStatus.FULLY_ARRIVED, PurchaseOrderStatus.CANCELLED),
+        PurchaseOrderStatus.PARTIAL_ARRIVED, Set.of(PurchaseOrderStatus.FULLY_ARRIVED, PurchaseOrderStatus.CLOSED),
+        PurchaseOrderStatus.FULLY_ARRIVED, Set.of(PurchaseOrderStatus.CLOSED, PurchaseOrderStatus.COMPLETED)
+    );
+
+    private void validateTransition(PurchaseOrderStatus current, PurchaseOrderStatus target) {
+        if (PurchaseOrderStatus.CANCELLED == target) return;
+        if (!ALLOWED.getOrDefault(current, Set.of()).contains(target))
+            throw new IllegalStateException("Cannot transition from " + current + " to " + target);
+    }
+
+    // ===== 基础 CRUD =====
+
     public Page<PurchaseOrder> getPage(int page, int size) { return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<PurchaseOrder>().orderByDesc(PurchaseOrder::getCreatedAt)); }
     public List<PurchaseOrder> getAll() { return mapper.selectList(new LambdaQueryWrapper<PurchaseOrder>().orderByDesc(PurchaseOrder::getCreatedAt)); }
     public PurchaseOrder getById(Long id) { return mapper.selectById(id); }
     public PurchaseOrderDTO getDtoById(Long id) { PurchaseOrder e = mapper.selectById(id); return e == null ? null : converter.toDto(e); }
     public List<PurchaseOrder> getBySupplierId(Long supplierId) { return mapper.selectList(new LambdaQueryWrapper<PurchaseOrder>().eq(PurchaseOrder::getSupplierId, supplierId).orderByDesc(PurchaseOrder::getCreatedAt)); }
-    public Long save(PurchaseOrder entity) { mapper.insert(entity); return entity.getId(); }
+    public PurchaseOrder getByPoNo(String poNo) { return mapper.selectOne(new LambdaQueryWrapper<PurchaseOrder>().eq(PurchaseOrder::getPoNo, poNo)); }
+
+    @Transactional
+    public Long save(PurchaseOrder entity) {
+        if (entity.getPoNo() == null || entity.getPoNo().isEmpty()) {
+            entity.setPoNo(generatePoNo());
+        }
+        if (entity.getStatus() == null) entity.setStatus(PurchaseOrderStatus.DRAFT.getCode());
+        if (entity.getArrivalProgress() == null) entity.setArrivalProgress(ArrivalStatus.PENDING.getCode());
+        entity.setCreatedAt(LocalDateTime.now());
+        entity.setUpdatedAt(LocalDateTime.now());
+        mapper.insert(entity);
+        return entity.getId();
+    }
+
     public void update(PurchaseOrder entity) { mapper.updateById(entity); }
     public void delete(Long id) { mapper.deleteById(id); }
-    public PurchaseOrder getByPoNo(String poNo) { return mapper.selectOne(new LambdaQueryWrapper<PurchaseOrder>().eq(PurchaseOrder::getPoNo, poNo)); }
 
+    private String generatePoNo() {
+        String date = LocalDate.now().format(DATE_FMT);
+        long seq = PO_SEQ.incrementAndGet() % 100000;
+        return "PO-" + date + "-" + String.format("%05d", seq);
+    }
+
+    // ===== 状态流转 =====
+
+    /** 提交审批 */
+    @Transactional
+    public void submit(Long id) {
+        PurchaseOrder po = mapper.selectById(id);
+        validateTransition(PurchaseOrderStatus.valueOf(po.getStatus()), PurchaseOrderStatus.SUBMITTED);
+        po.setStatus(PurchaseOrderStatus.SUBMITTED.getCode());
+        po.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(po);
+    }
+
+    /** 审批通过 */
+    @Transactional
+    public void approve(Long id) {
+        PurchaseOrder po = mapper.selectById(id);
+        validateTransition(PurchaseOrderStatus.valueOf(po.getStatus()), PurchaseOrderStatus.APPROVED);
+        po.setStatus(PurchaseOrderStatus.APPROVED.getCode());
+        po.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(po);
+    }
+
+    /** 审批拒绝 */
+    @Transactional
+    public void reject(Long id, String reason) {
+        PurchaseOrder po = mapper.selectById(id);
+        validateTransition(PurchaseOrderStatus.valueOf(po.getStatus()), PurchaseOrderStatus.REJECTED);
+        po.setStatus(PurchaseOrderStatus.REJECTED.getCode());
+        po.setRemark(reason);
+        po.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(po);
+    }
+
+    /** 下单给供应商 */
     @Transactional
-    public void submit(Long id) { PurchaseOrder po = mapper.selectById(id); if (!"DRAFT".equals(po.getStatus())) throw new IllegalStateException("Only draft can submit"); po.setStatus("SUBMITTED"); mapper.updateById(po); }
+    public void placeOrder(Long id) {
+        PurchaseOrder po = mapper.selectById(id);
+        validateTransition(PurchaseOrderStatus.valueOf(po.getStatus()), PurchaseOrderStatus.ORDERED);
+        po.setStatus(PurchaseOrderStatus.ORDERED.getCode());
+        po.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(po);
+    }
+
+    /** 取消采购单 */
+    @Transactional
+    public void cancel(Long id, String reason) {
+        PurchaseOrder po = mapper.selectById(id);
+        PurchaseOrderStatus current = PurchaseOrderStatus.valueOf(po.getStatus());
+        if (current == PurchaseOrderStatus.CLOSED || current == PurchaseOrderStatus.COMPLETED) {
+            throw new IllegalStateException("已完成或已关闭的采购单不能取消");
+        }
+        po.setStatus(PurchaseOrderStatus.CANCELLED.getCode());
+        po.setRemark(reason);
+        po.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(po);
+    }
+
+    /** 关闭采购单 */
+    @Transactional
+    public void close(Long id) {
+        PurchaseOrder po = mapper.selectById(id);
+        PurchaseOrderStatus current = PurchaseOrderStatus.valueOf(po.getStatus());
+        validateTransition(current, PurchaseOrderStatus.CLOSED);
+        // 校验所有到货项质检已完成
+        List<PurchaseOrderItem> items = itemMapper.selectList(
+            new LambdaQueryWrapper<PurchaseOrderItem>().eq(PurchaseOrderItem::getPurchaseOrderId, id));
+        for (PurchaseOrderItem item : items) {
+            if (item.getArrivedQty() > 0 && item.getQualifiedQty() == 0) {
+                throw new IllegalStateException("SKU " + item.getSku() + " 尚有到货未完成质检");
+            }
+        }
+        po.setStatus(PurchaseOrderStatus.CLOSED.getCode());
+        po.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(po);
+    }
+
+    /** 完成采购单 */
+    @Transactional
+    public void complete(Long id) {
+        PurchaseOrder po = mapper.selectById(id);
+        validateTransition(PurchaseOrderStatus.valueOf(po.getStatus()), PurchaseOrderStatus.COMPLETED);
+        po.setStatus(PurchaseOrderStatus.COMPLETED.getCode());
+        po.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(po);
+    }
+
+    // ===== 到货进度更新 =====
+
+    /**
+     * 到货后更新采购单进度。由 PurchaseArrivalService.confirmArrival() 调用。
+     * 根据所有 item 的 arrivedQty vs qty 判断是部分到货还是全部到货。
+     */
+    @Transactional
+    public void updateArrivalProgress(Long poId) {
+        PurchaseOrder po = mapper.selectById(poId);
+        if (po == null) return;
+
+        List<PurchaseOrderItem> items = itemMapper.selectList(
+            new LambdaQueryWrapper<PurchaseOrderItem>().eq(PurchaseOrderItem::getPurchaseOrderId, poId));
+
+        boolean anyArrived = false;
+        boolean allFullyArrived = true;
+        for (PurchaseOrderItem item : items) {
+            if (item.getArrivedQty() > 0) anyArrived = true;
+            if (item.getArrivedQty() < item.getQty()) allFullyArrived = false;
+        }
+
+        PurchaseOrderStatus current = PurchaseOrderStatus.valueOf(po.getStatus());
+        if (allFullyArrived) {
+            po.setArrivalProgress(ArrivalStatus.ARRIVED.getCode());
+            if (current == PurchaseOrderStatus.ORDERED || current == PurchaseOrderStatus.PARTIAL_ARRIVED) {
+                po.setStatus(PurchaseOrderStatus.FULLY_ARRIVED.getCode());
+            }
+        } else if (anyArrived) {
+            po.setArrivalProgress(ArrivalStatus.PARTIAL.getCode());
+            if (current == PurchaseOrderStatus.ORDERED) {
+                po.setStatus(PurchaseOrderStatus.PARTIAL_ARRIVED.getCode());
+            }
+        }
+
+        po.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(po);
+    }
+
+    /**
+     * 更新采购单项的累计到货数量和合格数量。由 PurchaseArrivalService 和 IqcService 调用。
+     */
+    @Transactional
+    public void addItemArrivedQty(Long itemId, int arrivedQty, int qualifiedQty) {
+        PurchaseOrderItem item = itemMapper.selectById(itemId);
+        if (item == null) return;
+        item.setArrivedQty(item.getArrivedQty() + arrivedQty);
+        item.setQualifiedQty(item.getQualifiedQty() + qualifiedQty);
+        itemMapper.updateById(item);
+    }
+
+    // ===== 到货入库 =====
 
     @Transactional
     public void updateInventoryOnArrival(Long skuId, Long warehouseId, int qualifiedQty, String operator, Long orderId, String orderNo) {
         Inventory inv = inventoryService.getBySkuIdAndWarehouseId(skuId, warehouseId);
-        if (inv != null) { inventoryService.incrementInventory(inv.getId(), qualifiedQty, "PURCHASE_ARRIVAL", operator, orderId, orderNo); inventoryService.recalcWarningStatus(inv.getId()); }
+        if (inv != null) {
+            inventoryService.incrementInventory(inv.getId(), qualifiedQty, "PURCHASE_ARRIVAL", operator, orderId, orderNo);
+            inventoryService.recalcWarningStatus(inv.getId());
+        }
     }
 
+    // ===== 金额计算 =====
+
     public void calculateAmount(Long id) {
-        List<PurchaseOrderItem> items = itemMapper.selectList(new LambdaQueryWrapper<PurchaseOrderItem>().eq(PurchaseOrderItem::getPurchaseOrderId, id));
+        List<PurchaseOrderItem> items = itemMapper.selectList(
+            new LambdaQueryWrapper<PurchaseOrderItem>().eq(PurchaseOrderItem::getPurchaseOrderId, id));
         BigDecimal total = items.stream().map(PurchaseOrderItem::getSubtotal).reduce(BigDecimal.ZERO, BigDecimal::add);
-        PurchaseOrder po = mapper.selectById(id); if (po != null) { po.setAmount(total); po.setSkuCount(items.size()); mapper.updateById(po); }
+        PurchaseOrder po = mapper.selectById(id);
+        if (po != null) {
+            po.setAmount(total);
+            po.setSkuCount(items.size());
+            mapper.updateById(po);
+        }
     }
 }

+ 86 - 24
backend/src/main/java/com/oms/service/PurchaseRequestService.java

@@ -4,11 +4,19 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.PurchaseRequestConverter;
 import com.oms.dto.PurchaseRequestDTO;
+import com.oms.entity.PurchaseOrder;
 import com.oms.entity.PurchaseRequest;
+import com.oms.enums.AuditStatus;
+import com.oms.enums.GenericStatus;
+import com.oms.enums.Priority;
+import com.oms.enums.PurchaseOrderStatus;
 import com.oms.mapper.PurchaseRequestMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Service
@@ -16,42 +24,96 @@ import java.util.List;
 public class PurchaseRequestService {
     private final PurchaseRequestMapper mapper;
     private final PurchaseRequestConverter converter;
+    private final PurchaseOrderService purchaseOrderService;
 
     public Page<PurchaseRequest> getPage(int page, int size) {
-        Page<PurchaseRequest> pageParam = new Page<>(page, size);
-        return mapper.selectPage(pageParam, new LambdaQueryWrapper<PurchaseRequest>()
-                .orderByDesc(PurchaseRequest::getCreatedAt));
-    }
-
-    public List<PurchaseRequest> getAll() {
-        return mapper.selectList(new LambdaQueryWrapper<PurchaseRequest>()
-                .orderByDesc(PurchaseRequest::getCreatedAt));
-    }
-
-    public PurchaseRequest getById(Long id) {
-        return mapper.selectById(id);
-    }
-
-    public PurchaseRequestDTO getDtoById(Long id) {
-        PurchaseRequest entity = mapper.selectById(id);
-        return entity == null ? null : converter.toDto(entity);
-    }
-
-    public PurchaseRequest getByRequestNo(String requestNo) {
-        return mapper.selectOne(new LambdaQueryWrapper<PurchaseRequest>()
-                .eq(PurchaseRequest::getRequestNo, requestNo));
+        return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<PurchaseRequest>().orderByDesc(PurchaseRequest::getCreatedAt));
     }
+    public List<PurchaseRequest> getAll() { return mapper.selectList(new LambdaQueryWrapper<PurchaseRequest>().orderByDesc(PurchaseRequest::getCreatedAt)); }
+    public PurchaseRequest getById(Long id) { return mapper.selectById(id); }
+    public PurchaseRequestDTO getDtoById(Long id) { PurchaseRequest entity = mapper.selectById(id); return entity == null ? null : converter.toDto(entity); }
+    public PurchaseRequest getByRequestNo(String requestNo) { return mapper.selectOne(new LambdaQueryWrapper<PurchaseRequest>().eq(PurchaseRequest::getRequestNo, requestNo)); }
 
+    @Transactional
     public Long save(PurchaseRequest entity) {
+        if (entity.getRequestNo() == null || entity.getRequestNo().isEmpty()) {
+            entity.setRequestNo("PR-" + System.currentTimeMillis());
+        }
+        if (entity.getStatus() == null) entity.setStatus(AuditStatus.PENDING.getCode());
+        if (entity.getUrgency() == null) entity.setUrgency(Priority.NORMAL.getCode());
+        entity.setCreatedAt(LocalDateTime.now());
+        entity.setUpdatedAt(LocalDateTime.now());
         mapper.insert(entity);
         return entity.getId();
     }
 
+    @Transactional
     public void update(PurchaseRequest entity) {
+        entity.setUpdatedAt(LocalDateTime.now());
         mapper.updateById(entity);
     }
 
     public void delete(Long id) { mapper.deleteById(id); }
-    @org.springframework.transaction.annotation.Transactional public void approve(Long id) { PurchaseRequest pr = mapper.selectById(id); pr.setStatus("APPROVED"); mapper.updateById(pr); }
-    @org.springframework.transaction.annotation.Transactional public void reject(Long id, String reason) { PurchaseRequest pr = mapper.selectById(id); pr.setStatus("REJECTED"); pr.setRemark(reason); mapper.updateById(pr); }
+
+    /**
+     * 审批通过。只有待审核状态才能审批。
+     */
+    @Transactional
+    public void approve(Long id) {
+        PurchaseRequest pr = mapper.selectById(id);
+        if (pr == null) throw new IllegalArgumentException("采购需求不存在");
+        if (!AuditStatus.PENDING.getCode().equals(pr.getStatus())) {
+            throw new IllegalStateException("只有待审核的需求才能审批");
+        }
+        pr.setStatus(AuditStatus.APPROVED.getCode());
+        pr.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(pr);
+    }
+
+    /**
+     * 审批拒绝。
+     */
+    @Transactional
+    public void reject(Long id, String reason) {
+        PurchaseRequest pr = mapper.selectById(id);
+        if (pr == null) throw new IllegalArgumentException("采购需求不存在");
+        if (!AuditStatus.PENDING.getCode().equals(pr.getStatus())) {
+            throw new IllegalStateException("只有待审核的需求才能拒绝");
+        }
+        pr.setStatus(AuditStatus.REJECTED.getCode());
+        pr.setRemark(reason);
+        pr.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(pr);
+    }
+
+    /**
+     * 从已审批的采购需求生成采购单。
+     * 将同一供应商的需求合并为一张采购单。
+     */
+    @Transactional
+    public Long generatePurchaseOrder(Long requestId, Long supplierId, Long warehouseId) {
+        PurchaseRequest pr = mapper.selectById(requestId);
+        if (pr == null) throw new IllegalArgumentException("采购需求不存在");
+        if (!AuditStatus.APPROVED.getCode().equals(pr.getStatus())) {
+            throw new IllegalStateException("只有已审批的需求才能生成采购单");
+        }
+
+        PurchaseOrder po = new PurchaseOrder();
+        po.setSupplierId(supplierId);
+        po.setWarehouseId(warehouseId);
+        po.setStatus(PurchaseOrderStatus.DRAFT.getCode());
+        po.setCurrency("CNY");
+        po.setRemark("由采购需求 " + pr.getRequestNo() + " 生成");
+        po.setCreatedAt(LocalDateTime.now());
+        po.setUpdatedAt(LocalDateTime.now());
+        Long poId = purchaseOrderService.save(po);
+
+        // 更新需求状态为已处理
+        pr.setStatus(AuditStatus.PROCESSED.getCode());
+        pr.setRelatedOrderId(poId);
+        pr.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(pr);
+
+        return poId;
+    }
 }

+ 129 - 9
backend/src/main/java/com/oms/service/ReplenishmentPlanService.java

@@ -3,14 +3,20 @@ package com.oms.service;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.ReplenishmentPlanConverter;
-import com.oms.entity.Inventory;
-import com.oms.entity.ReplenishmentPlan;
-import com.oms.entity.SupplyCapability;
+import com.oms.entity.*;
+import com.oms.enums.AuditStatus;
+import com.oms.enums.GenericStatus;
+import com.oms.enums.PurchaseOrderStatus;
 import com.oms.mapper.ReplenishmentPlanMapper;
+import com.oms.mapper.PurchaseOrderItemMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.ArrayList;
 import java.util.List;
 
 @Service
@@ -20,6 +26,9 @@ public class ReplenishmentPlanService {
     private final ReplenishmentPlanConverter converter;
     private final InventoryService inventoryService;
     private final SupplyCapabilityService supplyCapabilityService;
+    private final PurchaseOrderService purchaseOrderService;
+    private final ProductSkuService productSkuService;
+    private final PurchaseOrderItemMapper purchaseOrderItemMapper;
 
     public Page<ReplenishmentPlan> getPage(int page, int size) { return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<ReplenishmentPlan>().orderByDesc(ReplenishmentPlan::getCreatedAt)); }
     public List<ReplenishmentPlan> getAll() { return mapper.selectList(new LambdaQueryWrapper<ReplenishmentPlan>().orderByDesc(ReplenishmentPlan::getCreatedAt)); }
@@ -29,21 +38,132 @@ public class ReplenishmentPlanService {
     public void update(ReplenishmentPlan entity) { mapper.updateById(entity); }
     public void delete(Long id) { mapper.deleteById(id); }
 
+    /**
+     * 生成补货建议。综合考虑:
+     * - 当前可用库存
+     * - 在途库存(已下单未到货)
+     * - 安全库存
+     * - 默认供应商的 MOQ 和采购提前期
+     */
     @Transactional
     public int generateSuggestions() {
         List<Inventory> lowStock = inventoryService.getLowStock();
         int count = 0;
         for (Inventory inv : lowStock) {
-            ReplenishmentPlan existing = mapper.selectOne(new LambdaQueryWrapper<ReplenishmentPlan>().eq(ReplenishmentPlan::getSkuId, inv.getSkuId()).eq(ReplenishmentPlan::getWarehouseId, inv.getWarehouseId()).eq(ReplenishmentPlan::getStatus, "PENDING"));
+            // 跳过已有待处理建议的 SKU
+            ReplenishmentPlan existing = mapper.selectOne(new LambdaQueryWrapper<ReplenishmentPlan>()
+                .eq(ReplenishmentPlan::getSkuId, inv.getSkuId())
+                .eq(ReplenishmentPlan::getWarehouseId, inv.getWarehouseId())
+                .eq(ReplenishmentPlan::getStatus, GenericStatus.PENDING.getCode()));
             if (existing != null) continue;
-            int suggestedQty = inv.getSafeStock() - inv.getAvailable() + inv.getLocked();
+
+            // 计算建议数量 = 安全库存 - 可用库存 + 已锁定
+            int baseQty = inv.getSafeStock() - inv.getAvailable() + inv.getLocked();
+            // 如果有在途库存,减少建议数量
+            int inTransit = inv.getInbound() != null ? inv.getInbound() : 0;
+            int suggestedQty = Math.max(baseQty - inTransit, 0);
+
+            // 获取默认供应商信息
             SupplyCapability def = supplyCapabilityService.getDefaultSupplier(inv.getSkuId());
+            if (def != null && def.getMoq() != null && def.getMoq() > 0 && suggestedQty < def.getMoq()) {
+                suggestedQty = def.getMoq(); // 满足最小起订量
+            }
+
+            if (suggestedQty <= 0) continue;
+
             ReplenishmentPlan plan = new ReplenishmentPlan();
-            plan.setSkuId(inv.getSkuId()); plan.setWarehouseId(inv.getWarehouseId()); plan.setSafeStock(inv.getSafeStock());
-            plan.setCurrentStock(inv.getAvailable()); plan.setSuggestedQty(Math.max(suggestedQty, 0)); plan.setStatus("PENDING");
-            if (def != null) plan.setSuggestedSupplierId(def.getSupplierId());
-            mapper.insert(plan); count++;
+            plan.setSkuId(inv.getSkuId());
+            plan.setProductTitle(inv.getProductTitle());
+            plan.setWarehouseId(inv.getWarehouseId());
+            plan.setSafeStock(inv.getSafeStock());
+            plan.setCurrentStock(inv.getAvailable());
+            plan.setOnTheWay(inTransit);
+            plan.setSuggestedQty(suggestedQty);
+            plan.setStatus(GenericStatus.PENDING.getCode());
+            if (def != null) {
+                plan.setSuggestedSupplierId(def.getSupplierId());
+                plan.setAvgDailySales(def.getLeadTime() != null ? BigDecimal.valueOf(def.getLeadTime()) : null);
+            }
+            mapper.insert(plan);
+            count++;
         }
         return count;
     }
+
+    /**
+     * 一键从补货建议生成采购单。
+     * 将多条同一供应商的建议合并为一张采购单。
+     */
+    @Transactional
+    public List<Long> generatePurchaseOrders(List<Long> planIds, String operator) {
+        List<Long> poIds = new ArrayList<>();
+
+        // 按供应商分组
+        java.util.Map<Long, List<ReplenishmentPlan>> bySupplier = new java.util.HashMap<>();
+        for (Long planId : planIds) {
+            ReplenishmentPlan plan = mapper.selectById(planId);
+            if (plan == null || !GenericStatus.PENDING.getCode().equals(plan.getStatus())) {
+                throw new IllegalStateException("建议 " + planId + " 不存在或非待处理状态");
+            }
+            Long supplierId = plan.getSuggestedSupplierId();
+            if (supplierId == null) {
+                throw new IllegalStateException("建议 " + planId + " 未指定供应商,无法生成采购单");
+            }
+            bySupplier.computeIfAbsent(supplierId, k -> new java.util.ArrayList<>()).add(plan);
+        }
+
+        // 每个供应商生成一张采购单
+        for (java.util.Map.Entry<Long, List<ReplenishmentPlan>> entry : bySupplier.entrySet()) {
+            Long supplierId = entry.getKey();
+            List<ReplenishmentPlan> plans = entry.getValue();
+
+            PurchaseOrder po = new PurchaseOrder();
+            po.setSupplierId(supplierId);
+            po.setWarehouseId(plans.get(0).getWarehouseId());
+            po.setStatus(PurchaseOrderStatus.DRAFT.getCode());
+            po.setArrivalProgress(AuditStatus.PENDING.getCode());
+            po.setCurrency("CNY");
+            po.setCreatedAt(LocalDateTime.now());
+            po.setUpdatedAt(LocalDateTime.now());
+            purchaseOrderService.save(po);
+
+            BigDecimal totalAmount = BigDecimal.ZERO;
+            int skuCount = 0;
+            for (ReplenishmentPlan plan : plans) {
+                // 获取建议采购价
+                BigDecimal price = BigDecimal.ZERO;
+                SupplyCapability cap = supplyCapabilityService.getDefaultSupplier(plan.getSkuId());
+                if (cap != null) {
+                    price = supplyCapabilityService.calculateTierPrice(cap.getId(), plan.getSuggestedQty());
+                }
+
+                PurchaseOrderItem item = new PurchaseOrderItem();
+                item.setPurchaseOrderId(po.getId());
+                item.setSkuId(plan.getSkuId());
+                item.setProductTitle(plan.getProductTitle());
+                item.setQty(plan.getSuggestedQty());
+                item.setPrice(price);
+                item.setSubtotal(price.multiply(BigDecimal.valueOf(plan.getSuggestedQty())));
+                item.setArrivedQty(0);
+                item.setQualifiedQty(0);
+                purchaseOrderItemMapper.insert(item);
+                totalAmount = totalAmount.add(item.getSubtotal());
+                skuCount++;
+            }
+
+            // 更新采购单总金额和 SKU 数量
+            po.setAmount(totalAmount);
+            po.setSkuCount(skuCount);
+            purchaseOrderService.update(po);
+            poIds.add(po.getId());
+
+            // 更新补货建议状态
+            for (ReplenishmentPlan plan : plans) {
+                plan.setStatus(GenericStatus.ACTIVE.getCode()); // ACTIVE = 已生成采购单
+                mapper.updateById(plan);
+            }
+        }
+
+        return poIds;
+    }
 }

+ 201 - 11
backend/src/main/java/com/oms/service/ReportService.java

@@ -1,18 +1,28 @@
 package com.oms.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.oms.entity.Report;
-import com.oms.mapper.ReportMapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.oms.entity.*;
+import com.oms.enums.*;
+import com.oms.mapper.*;
 import com.oms.dto.ReportDTO;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
 import java.time.LocalDateTime;
-import java.util.List;
+import java.util.*;
 
 @Service
 @RequiredArgsConstructor
 public class ReportService {
     private final ReportMapper mapper;
+    private final OrdersMapper ordersMapper;
+    private final InventoryMapper inventoryMapper;
+    private final PurchaseOrderMapper purchaseOrderMapper;
+    private final FinancePaymentMapper financePaymentMapper;
+    private final ObjectMapper objectMapper = new ObjectMapper();
 
     public List<Report> listReports(String reportType, String status) {
         LambdaQueryWrapper<Report> wrapper = new LambdaQueryWrapper<>();
@@ -22,9 +32,7 @@ public class ReportService {
         return mapper.selectList(wrapper);
     }
 
-    public Report getReport(Long id) {
-        return mapper.selectById(id);
-    }
+    public Report getReport(Long id) { return mapper.selectById(id); }
 
     public Report createReport(ReportDTO dto) {
         Report report = new Report();
@@ -34,7 +42,7 @@ public class ReportService {
         report.setPeriodType(dto.getPeriodType());
         report.setStartDate(dto.getStartDate());
         report.setEndDate(dto.getEndDate());
-        report.setStatus("DRAFT");
+        report.setStatus(ReportStatus.DRAFT.getCode());
         report.setCreatedAt(LocalDateTime.now());
         mapper.insert(report);
         return report;
@@ -53,16 +61,198 @@ public class ReportService {
         return report;
     }
 
-    public void deleteReport(Long id) {
-        mapper.deleteById(id);
-    }
+    public void deleteReport(Long id) { mapper.deleteById(id); }
 
+    /**
+     * 生成报表:根据 reportType 调用不同的聚合逻辑,计算真实数据并存储。
+     */
     public Report generateReport(Long id) {
         Report report = mapper.selectById(id);
         if (report == null) return null;
-        report.setStatus("GENERATED");
+
+        Map<String, Object> data;
+        switch (report.getReportType() != null ? report.getReportType() : "") {
+            case "SALES": data = generateSalesReport(report); break;
+            case "INVENTORY": data = generateInventoryReport(report); break;
+            case "PURCHASE": data = generatePurchaseReport(report); break;
+            case "PROFIT": data = generateProfitReport(report); break;
+            default: data = generateSalesReport(report); break;
+        }
+
+        try {
+            report.setData(objectMapper.writeValueAsString(data));
+        } catch (Exception e) {
+            report.setData("{}");
+        }
+        report.setStatus(ReportStatus.GENERATED.getCode());
         report.setUpdatedAt(LocalDateTime.now());
         mapper.updateById(report);
         return report;
     }
+
+    /**
+     * 销售报表:GMV、订单量、客单价、退款率、按渠道/国家分布
+     */
+    private Map<String, Object> generateSalesReport(Report report) {
+        Map<String, Object> result = new HashMap<>();
+        LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<>();
+        if (report.getStartDate() != null) wrapper.ge(Orders::getCreatedAt, report.getStartDate());
+        if (report.getEndDate() != null) wrapper.le(Orders::getCreatedAt, report.getEndDate());
+        wrapper.ne(Orders::getOrderStatus, OrderStatus.CANCELLED);
+        List<Orders> orders = ordersMapper.selectList(wrapper);
+
+        int totalOrders = orders.size();
+        BigDecimal totalGmv = orders.stream().map(Orders::getOrderAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+        long paidOrders = orders.stream().filter(o -> PaymentStatus.PAID == o.getPaymentStatus() || PaymentStatus.PARTIAL_PAID == o.getPaymentStatus()).count();
+        long refundOrders = orders.stream().filter(o -> o.getRefundStatus() != null && o.getRefundStatus() != RefundStatus.NONE).count();
+
+        result.put("totalOrders", totalOrders);
+        result.put("totalGmv", totalGmv);
+        result.put("paidOrders", paidOrders);
+        result.put("avgOrderValue", paidOrders > 0 ? totalGmv.divide(BigDecimal.valueOf(paidOrders), 2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
+        result.put("refundRate", totalOrders > 0 ? BigDecimal.valueOf(refundOrders * 100.0 / totalOrders).setScale(2, RoundingMode.HALF_UP) : BigDecimal.ZERO);
+
+        // 按渠道分布
+        Map<Long, BigDecimal> gmvByChannel = new HashMap<>();
+        Map<Long, Integer> countByChannel = new HashMap<>();
+        for (Orders o : orders) {
+            Long chId = o.getChannelId() != null ? o.getChannelId() : 0L;
+            gmvByChannel.merge(chId, o.getOrderAmount() != null ? o.getOrderAmount() : BigDecimal.ZERO, BigDecimal::add);
+            countByChannel.merge(chId, 1, Integer::sum);
+        }
+        result.put("gmvByChannel", gmvByChannel);
+        result.put("countByChannel", countByChannel);
+
+        // 按国家分布
+        Map<String, Integer> countByCountry = new HashMap<>();
+        for (Orders o : orders) {
+            String country = o.getBuyerCountry() != null ? o.getBuyerCountry() : "Unknown";
+            countByCountry.merge(country, 1, Integer::sum);
+        }
+        result.put("countByCountry", countByCountry);
+
+        return result;
+    }
+
+    /**
+     * 库存报表:按仓库/SKU 展示可用/锁定/在途/安全库存
+     */
+    private Map<String, Object> generateInventoryReport(Report report) {
+        Map<String, Object> result = new HashMap<>();
+        List<Inventory> inventories = inventoryMapper.selectList(new LambdaQueryWrapper<>());
+
+        int totalSku = inventories.size();
+        int totalAvailable = inventories.stream().mapToInt(i -> i.getAvailable() != null ? i.getAvailable() : 0).sum();
+        int totalLocked = inventories.stream().mapToInt(i -> i.getLocked() != null ? i.getLocked() : 0).sum();
+        int totalInbound = inventories.stream().mapToInt(i -> i.getInbound() != null ? i.getInbound() : 0).sum();
+        long lowStockCount = inventories.stream().filter(i -> i.getAvailable() != null && i.getSafeStock() != null && i.getAvailable() < i.getSafeStock()).count();
+
+        result.put("totalSku", totalSku);
+        result.put("totalAvailable", totalAvailable);
+        result.put("totalLocked", totalLocked);
+        result.put("totalInbound", totalInbound);
+        result.put("lowStockCount", lowStockCount);
+
+        // 按仓库分布
+        Map<Long, Map<String, Object>> byWarehouse = new HashMap<>();
+        for (Inventory inv : inventories) {
+            Long whId = inv.getWarehouseId() != null ? inv.getWarehouseId() : 0L;
+            Map<String, Object> whData = byWarehouse.computeIfAbsent(whId, k -> new HashMap<>());
+            whData.merge("available", inv.getAvailable() != null ? inv.getAvailable() : 0, (a, b) -> (int) a + (int) b);
+            whData.merge("locked", inv.getLocked() != null ? inv.getLocked() : 0, (a, b) -> (int) a + (int) b);
+            whData.merge("skuCount", 1, (a, b) -> (int) a + (int) b);
+        }
+        result.put("byWarehouse", byWarehouse);
+
+        // 低库存明细
+        List<Map<String, Object>> lowStockList = new ArrayList<>();
+        for (Inventory inv : inventories) {
+            if (inv.getAvailable() != null && inv.getSafeStock() != null && inv.getAvailable() < inv.getSafeStock()) {
+                Map<String, Object> item = new HashMap<>();
+                item.put("skuId", inv.getSkuId());
+                item.put("productTitle", inv.getProductTitle());
+                item.put("available", inv.getAvailable());
+                item.put("safeStock", inv.getSafeStock());
+                item.put("warehouseId", inv.getWarehouseId());
+                lowStockList.add(item);
+            }
+        }
+        result.put("lowStockItems", lowStockList);
+
+        return result;
+    }
+
+    /**
+     * 采购报表:采购金额、到货率、供应商分布
+     */
+    private Map<String, Object> generatePurchaseReport(Report report) {
+        Map<String, Object> result = new HashMap<>();
+        LambdaQueryWrapper<PurchaseOrder> wrapper = new LambdaQueryWrapper<>();
+        if (report.getStartDate() != null) wrapper.ge(PurchaseOrder::getCreatedAt, report.getStartDate());
+        if (report.getEndDate() != null) wrapper.le(PurchaseOrder::getCreatedAt, report.getEndDate());
+        wrapper.ne(PurchaseOrder::getStatus, PurchaseOrderStatus.CANCELLED);
+        List<PurchaseOrder> pos = purchaseOrderMapper.selectList(wrapper);
+
+        int totalPo = pos.size();
+        BigDecimal totalAmount = pos.stream().map(PurchaseOrder::getAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+        long completedPo = pos.stream().filter(p -> PurchaseOrderStatus.FULLY_ARRIVED.getCode().equals(p.getStatus())
+            || PurchaseOrderStatus.CLOSED.getCode().equals(p.getStatus())
+            || PurchaseOrderStatus.COMPLETED.getCode().equals(p.getStatus())).count();
+
+        result.put("totalPo", totalPo);
+        result.put("totalAmount", totalAmount);
+        result.put("completedPo", completedPo);
+        result.put("completionRate", totalPo > 0 ? BigDecimal.valueOf(completedPo * 100.0 / totalPo).setScale(1, RoundingMode.HALF_UP) : BigDecimal.ZERO);
+
+        // 按状态分布
+        Map<String, Integer> countByStatus = new HashMap<>();
+        for (PurchaseOrder po : pos) {
+            countByStatus.merge(po.getStatus(), 1, Integer::sum);
+        }
+        result.put("countByStatus", countByStatus);
+
+        // 按供应商分布
+        Map<Long, BigDecimal> amountBySupplier = new HashMap<>();
+        for (PurchaseOrder po : pos) {
+            Long supId = po.getSupplierId() != null ? po.getSupplierId() : 0L;
+            amountBySupplier.merge(supId, po.getAmount() != null ? po.getAmount() : BigDecimal.ZERO, BigDecimal::add);
+        }
+        result.put("amountBySupplier", amountBySupplier);
+
+        return result;
+    }
+
+    /**
+     * 利润报表:订单收入 - 退款 - 商品成本(简化:基于订单金额和退款金额)
+     */
+    private Map<String, Object> generateProfitReport(Report report) {
+        Map<String, Object> result = new HashMap<>();
+        LambdaQueryWrapper<Orders> wrapper = new LambdaQueryWrapper<>();
+        if (report.getStartDate() != null) wrapper.ge(Orders::getCreatedAt, report.getStartDate());
+        if (report.getEndDate() != null) wrapper.le(Orders::getCreatedAt, report.getEndDate());
+        wrapper.ne(Orders::getOrderStatus, OrderStatus.CANCELLED);
+        List<Orders> orders = ordersMapper.selectList(wrapper);
+
+        BigDecimal totalRevenue = orders.stream().map(Orders::getActualPaid).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal totalShipping = orders.stream().map(Orders::getShippingFee).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal totalTax = orders.stream().map(Orders::getTaxAmount).filter(Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        // 退款金额(从有退款状态的订单获取)
+        BigDecimal totalRefund = orders.stream()
+            .filter(o -> o.getRefundStatus() != null && o.getRefundStatus() != RefundStatus.NONE)
+            .map(o -> o.getActualPaid() != null ? o.getActualPaid() : BigDecimal.ZERO)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        BigDecimal netRevenue = totalRevenue.subtract(totalRefund);
+
+        result.put("totalRevenue", totalRevenue);
+        result.put("totalShipping", totalShipping);
+        result.put("totalTax", totalTax);
+        result.put("totalRefund", totalRefund);
+        result.put("netRevenue", netRevenue);
+        result.put("orderCount", orders.size());
+        result.put("refundOrderCount", orders.stream().filter(o -> o.getRefundStatus() != null && o.getRefundStatus() != RefundStatus.NONE).count());
+
+        return result;
+    }
 }

+ 67 - 24
backend/src/main/java/com/oms/service/ReturnPackageService.java

@@ -5,10 +5,14 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.ReturnPackageConverter;
 import com.oms.dto.ReturnPackageDTO;
 import com.oms.entity.ReturnPackage;
+import com.oms.enums.HandleResult;
+import com.oms.enums.ReturnPackageStatus;
 import com.oms.mapper.ReturnPackageMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
+import java.time.LocalDateTime;
 import java.util.List;
 
 @Service
@@ -17,41 +21,80 @@ public class ReturnPackageService {
     private final ReturnPackageMapper mapper;
     private final ReturnPackageConverter converter;
 
+    // 退货包裹状态:PENDING → RECEIVED → INSPECTED → CLOSED
     public Page<ReturnPackage> getPage(int page, int size) {
-        Page<ReturnPackage> pageParam = new Page<>(page, size);
-        return mapper.selectPage(pageParam, new LambdaQueryWrapper<ReturnPackage>()
-                .orderByDesc(ReturnPackage::getCreatedAt));
-    }
-
-    public List<ReturnPackage> getAll() {
-        return mapper.selectList(new LambdaQueryWrapper<ReturnPackage>()
-                .orderByDesc(ReturnPackage::getCreatedAt));
-    }
-
-    public ReturnPackage getById(Long id) {
-        return mapper.selectById(id);
-    }
-
-    public ReturnPackageDTO getDtoById(Long id) {
-        ReturnPackage entity = mapper.selectById(id);
-        return entity == null ? null : converter.toDto(entity);
-    }
-
-    public ReturnPackage getByReturnNo(String returnNo) {
-        return mapper.selectOne(new LambdaQueryWrapper<ReturnPackage>()
-                .eq(ReturnPackage::getReturnNo, returnNo));
+        return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<ReturnPackage>().orderByDesc(ReturnPackage::getCreatedAt));
     }
+    public List<ReturnPackage> getAll() { return mapper.selectList(new LambdaQueryWrapper<ReturnPackage>().orderByDesc(ReturnPackage::getCreatedAt)); }
+    public ReturnPackage getById(Long id) { return mapper.selectById(id); }
+    public ReturnPackageDTO getDtoById(Long id) { ReturnPackage entity = mapper.selectById(id); return entity == null ? null : converter.toDto(entity); }
+    public ReturnPackage getByReturnNo(String returnNo) { return mapper.selectOne(new LambdaQueryWrapper<ReturnPackage>().eq(ReturnPackage::getReturnNo, returnNo)); }
 
+    @Transactional
     public Long save(ReturnPackage entity) {
+        if (entity.getReturnNo() == null || entity.getReturnNo().isEmpty()) {
+            entity.setReturnNo("RET-" + System.currentTimeMillis());
+        }
+        if (entity.getStatus() == null) entity.setStatus(ReturnPackageStatus.PENDING.getCode());
+        entity.setCreatedAt(LocalDateTime.now());
+        entity.setUpdatedAt(LocalDateTime.now());
         mapper.insert(entity);
         return entity.getId();
     }
 
+    @Transactional
     public void update(ReturnPackage entity) {
+        entity.setUpdatedAt(LocalDateTime.now());
         mapper.updateById(entity);
     }
 
     public void delete(Long id) { mapper.deleteById(id); }
-    @org.springframework.transaction.annotation.Transactional public void receive(Long id) { ReturnPackage rp = mapper.selectById(id); rp.setStatus("RECEIVED"); mapper.updateById(rp); }
-    @org.springframework.transaction.annotation.Transactional public void confirmInspection(Long id, String result) { ReturnPackage rp = mapper.selectById(id); rp.setStatus("INSPECTED"); rp.setHandleResult(result); mapper.updateById(rp); }
+
+    /**
+     * 签收退货包裹。只有待收货状态才能签收。
+     */
+    @Transactional
+    public void receive(Long id, String handler) {
+        ReturnPackage rp = mapper.selectById(id);
+        if (rp == null) throw new IllegalArgumentException("退货包裹不存在");
+        if (!ReturnPackageStatus.PENDING.getCode().equals(rp.getStatus())) {
+            throw new IllegalStateException("只有待收货状态的包裹才能签收");
+        }
+        rp.setStatus(ReturnPackageStatus.RECEIVED.getCode());
+        rp.setHandlerName(handler);
+        rp.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(rp);
+    }
+
+    /**
+     * 质检确认。记录处理结果。
+     * @param result 处理结果:RESTOCK(入库) / SCRAP(报废) / RETURN_SUPPLIER(退回供应商)
+     */
+    @Transactional
+    public void confirmInspection(Long id, String result) {
+        ReturnPackage rp = mapper.selectById(id);
+        if (rp == null) throw new IllegalArgumentException("退货包裹不存在");
+        if (!ReturnPackageStatus.RECEIVED.getCode().equals(rp.getStatus())) {
+            throw new IllegalStateException("只有已签收的包裹才能质检");
+        }
+        rp.setStatus(ReturnPackageStatus.INSPECTED.getCode());
+        rp.setHandleResult(result);
+        rp.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(rp);
+    }
+
+    /**
+     * 关闭退货单。
+     */
+    @Transactional
+    public void close(Long id) {
+        ReturnPackage rp = mapper.selectById(id);
+        if (rp == null) throw new IllegalArgumentException("退货包裹不存在");
+        if (!ReturnPackageStatus.INSPECTED.getCode().equals(rp.getStatus())) {
+            throw new IllegalStateException("只有已质检的包裹才能关闭");
+        }
+        rp.setStatus(ReturnPackageStatus.CLOSED.getCode());
+        rp.setUpdatedAt(LocalDateTime.now());
+        mapper.updateById(rp);
+    }
 }

+ 69 - 8
backend/src/main/java/com/oms/service/ShippingOrderService.java

@@ -5,7 +5,11 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.ShippingOrderConverter;
 import com.oms.dto.ShippingOrderDTO;
 import com.oms.entity.Orders;
+import com.oms.entity.OrderItem;
 import com.oms.entity.ShippingOrder;
+import com.oms.enums.OrderStatus;
+import com.oms.enums.ReturnStatus;
+import com.oms.enums.ShippingStatus;
 import com.oms.mapper.ShippingOrderMapper;
 import com.oms.mapper.OrdersMapper;
 import lombok.RequiredArgsConstructor;
@@ -14,6 +18,8 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 @Service
 @RequiredArgsConstructor
@@ -22,6 +28,21 @@ public class ShippingOrderService {
     private final ShippingOrderConverter converter;
     private final OrdersMapper ordersMapper;
     private final OrderStatusEventService eventService;
+    private final InventoryService inventoryService;
+    private final OrderItemService orderItemService;
+
+    // 发货单状态机:PENDING_PICK → PENDING_SHIP → SHIPPED → DELIVERED
+    private static final Map<ShippingStatus, Set<ShippingStatus>> ALLOWED = Map.of(
+        ShippingStatus.PENDING_PICK, Set.of(ShippingStatus.PENDING_SHIP),
+        ShippingStatus.PENDING_SHIP, Set.of(ShippingStatus.SHIPPED),
+        ShippingStatus.SHIPPED, Set.of(ShippingStatus.DELIVERED)
+    );
+
+    private void validateTransition(ShippingStatus current, ShippingStatus target) {
+        if (!ALLOWED.getOrDefault(current, Set.of()).contains(target)) {
+            throw new IllegalStateException("发货单状态不能从 " + current + " 变更为 " + target);
+        }
+    }
 
     public Page<ShippingOrder> getPage(int page, int size) {
         return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<ShippingOrder>().orderByDesc(ShippingOrder::getCreatedAt));
@@ -38,29 +59,69 @@ public class ShippingOrderService {
     @Transactional
     public void submitTracking(Long id, String carrier, String trackingNo, String operator) {
         ShippingOrder so = mapper.selectById(id);
-        so.setCarrierName(carrier); so.setTrackingNo(trackingNo); so.setShippingStatus("PENDING_SHIP"); mapper.updateById(so);
-        eventService.logEvent(so.getOrderId(), "PENDING_SHIP", "运单号已录入", carrier + " " + trackingNo, "primary", operator);
+        if (so == null) throw new IllegalArgumentException("发货单不存在");
+        validateTransition(so.getShippingStatus(), ShippingStatus.PENDING_SHIP);
+        so.setCarrierName(carrier);
+        so.setTrackingNo(trackingNo);
+        so.setShippingStatus(ShippingStatus.PENDING_SHIP);
+        mapper.updateById(so);
+        eventService.logEvent(so.getOrderId(), ShippingStatus.PENDING_SHIP.name(), "运单号已录入", carrier + " " + trackingNo, "primary", operator);
     }
+
     @Transactional
     public void confirmShipped(Long id, String operator) {
         ShippingOrder so = mapper.selectById(id);
-        if (so.getTrackingNo() == null || so.getTrackingNo().isEmpty()) throw new IllegalStateException("Tracking number required");
-        so.setShippingStatus("SHIPPED"); so.setReturnStatus("RETURNED"); so.setShippedAt(LocalDateTime.now()); mapper.updateById(so);
+        if (so == null) throw new IllegalArgumentException("发货单不存在");
+        if (so.getTrackingNo() == null || so.getTrackingNo().isEmpty()) throw new IllegalStateException("运单号不能为空");
+        validateTransition(so.getShippingStatus(), ShippingStatus.SHIPPED);
+        so.setShippingStatus(ShippingStatus.SHIPPED);
+        so.setShippedAt(LocalDateTime.now());
+        mapper.updateById(so);
+        Orders o = ordersMapper.selectById(so.getOrderId());
+        if (o != null) {
+            // 扣减锁定库存、释放已锁定数量
+            List<OrderItem> items = orderItemService.getByOrderId(o.getId());
+            for (OrderItem item : items) {
+                inventoryService.shipInventoryBySku(item.getSkuId(), item.getQty(), operator);
+            }
+            o.setShippingStatus(ShippingStatus.SHIPPED);
+            o.setOrderStatus(OrderStatus.SHIPPED);
+            o.setTrackingNo(so.getTrackingNo());
+            o.setCarrierName(so.getCarrierName());
+            o.setShippedAt(LocalDateTime.now());
+            ordersMapper.updateById(o);
+        }
+        eventService.logEvent(so.getOrderId(), ShippingStatus.SHIPPED.name(), "已发货", so.getShipmentNo() + " 已发出", "primary", operator);
+    }
+
+    @Transactional
+    public void confirmDelivered(Long id, String operator) {
+        ShippingOrder so = mapper.selectById(id);
+        if (so == null) throw new IllegalArgumentException("发货单不存在");
+        validateTransition(so.getShippingStatus(), ShippingStatus.DELIVERED);
+        so.setShippingStatus(ShippingStatus.DELIVERED);
+        mapper.updateById(so);
         Orders o = ordersMapper.selectById(so.getOrderId());
-        if (o != null) { o.setShippingStatus("SHIPPED"); o.setOrderStatus("SHIPPED"); o.setTrackingNo(so.getTrackingNo()); o.setCarrierName(so.getCarrierName()); ordersMapper.updateById(o); }
-        eventService.logEvent(so.getOrderId(), "SHIPPED", "已发货", so.getShipmentNo() + " 已发出", "primary", operator);
+        if (o != null) {
+            o.setShippingStatus(ShippingStatus.DELIVERED);
+            ordersMapper.updateById(o);
+        }
+        eventService.logEvent(so.getOrderId(), ShippingStatus.DELIVERED.name(), "已签收", so.getShipmentNo() + " 已签收", "success", operator);
     }
+
     @Transactional
     public void retryReturn(Long id) {
         ShippingOrder so = mapper.selectById(id);
-        so.setReturnStatus("RETURNED"); mapper.updateById(so);
+        if (so == null) throw new IllegalArgumentException("发货单不存在");
+        so.setReturnStatus(ReturnStatus.RETURNED.getCode());
+        mapper.updateById(so);
     }
     @Transactional
     public Long generateForOrder(Orders order, int skuCount, int expectedQty) {
         ShippingOrder so = new ShippingOrder();
         so.setShipmentNo("SHP-" + System.currentTimeMillis()); so.setOrderId(order.getId()); so.setOrderNo(order.getOrderNo());
         so.setWarehouseId(order.getWarehouseId()); so.setSkuCount(skuCount); so.setExpectedQty(expectedQty); so.setActualQty(expectedQty);
-        so.setShippingStatus("PENDING_PICK"); so.setReturnStatus("NOT_RETURNED");
+        so.setShippingStatus(ShippingStatus.PENDING_PICK); so.setReturnStatus(ReturnStatus.NOT_RETURNED.getCode());
         mapper.insert(so); return so.getId();
     }
 }

+ 104 - 8
backend/src/main/java/com/oms/service/SupplierSettlementService.java

@@ -6,6 +6,8 @@ import com.oms.converter.SupplierSettlementConverter;
 import com.oms.dto.SupplierSettlementDTO;
 import com.oms.entity.PurchaseOrder;
 import com.oms.entity.SupplierSettlement;
+import com.oms.enums.PurchaseOrderStatus;
+import com.oms.enums.SettlementStatus;
 import com.oms.mapper.SupplierSettlementMapper;
 import com.oms.mapper.PurchaseOrderMapper;
 import lombok.RequiredArgsConstructor;
@@ -14,7 +16,9 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicLong;
 
 @Service
 @RequiredArgsConstructor
@@ -23,6 +27,8 @@ public class SupplierSettlementService {
     private final SupplierSettlementConverter converter;
     private final PurchaseOrderMapper purchaseOrderMapper;
 
+    private static final AtomicLong STL_SEQ = new AtomicLong(System.currentTimeMillis() % 10000);
+
     public Page<SupplierSettlement> getPage(int page, int size) { return mapper.selectPage(new Page<>(page, size), new LambdaQueryWrapper<SupplierSettlement>().orderByDesc(SupplierSettlement::getCreatedAt)); }
     public List<SupplierSettlement> getAll() { return mapper.selectList(new LambdaQueryWrapper<SupplierSettlement>().orderByDesc(SupplierSettlement::getCreatedAt)); }
     public SupplierSettlement getById(Long id) { return mapper.selectById(id); }
@@ -32,18 +38,108 @@ public class SupplierSettlementService {
     public void update(SupplierSettlement entity) { mapper.updateById(entity); }
     public void delete(Long id) { mapper.deleteById(id); }
 
+    /**
+     * 全额结算:标记为已结清。
+     */
+    @Transactional
+    public void settle(Long id, String operator) {
+        SupplierSettlement s = mapper.selectById(id);
+        if (!SettlementStatus.PENDING.getCode().equals(s.getStatus())) {
+            throw new IllegalStateException("只有待结算的单据才能结算");
+        }
+        s.setPaidAmount(s.getPayableAmount());
+        s.setStatus(SettlementStatus.PAID.getCode());
+        s.setPaidAt(LocalDateTime.now());
+        mapper.updateById(s);
+    }
+
+    /**
+     * 部分结算:记录已付金额,更新状态为部分结算或已结清。
+     */
     @Transactional
-    public void settle(Long id) { SupplierSettlement s = mapper.selectById(id); s.setStatus("PAID"); s.setPaidAt(LocalDateTime.now()); mapper.updateById(s); }
+    public void partialSettle(Long id, BigDecimal amount, String operator) {
+        SupplierSettlement s = mapper.selectById(id);
+        if (!SettlementStatus.PENDING.getCode().equals(s.getStatus())
+            && !SettlementStatus.PARTIAL.getCode().equals(s.getStatus())) {
+            throw new IllegalStateException("当前状态不允许结算");
+        }
+        if (amount == null || amount.compareTo(BigDecimal.ZERO) <= 0) {
+            throw new IllegalStateException("结算金额必须大于0");
+        }
+
+        BigDecimal newPaid = s.getPaidAmount().add(amount);
+        if (newPaid.compareTo(s.getPayableAmount()) > 0) {
+            throw new IllegalStateException("累计付款金额(" + newPaid + ")超过应付金额(" + s.getPayableAmount() + ")");
+        }
+
+        s.setPaidAmount(newPaid);
+        if (newPaid.compareTo(s.getPayableAmount()) >= 0) {
+            s.setStatus(SettlementStatus.PAID.getCode());
+            s.setPaidAt(LocalDateTime.now());
+        } else {
+            s.setStatus(SettlementStatus.PARTIAL.getCode());
+        }
+        mapper.updateById(s);
+    }
 
+    /**
+     * 生成供应商结算单。
+     * 筛选指定供应商在指定时间范围内已完成/已关闭的采购单,汇总应付金额。
+     *
+     * @param supplierId 供应商ID
+     * @param period 结算账期(如 "2026-06")
+     * @param periodStart 账期开始时间(可选,null 则不限)
+     * @param periodEnd 账期结束时间(可选,null 则不限)
+     */
     @Transactional
-    public Long generateSettlement(Long supplierId, String period) {
-        List<PurchaseOrder> pos = purchaseOrderMapper.selectList(new LambdaQueryWrapper<PurchaseOrder>().eq(PurchaseOrder::getSupplierId, supplierId).eq(PurchaseOrder::getStatus, "COMPLETED"));
-        BigDecimal payable = pos.stream().map(PurchaseOrder::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
+    public Long generateSettlement(Long supplierId, String period, LocalDateTime periodStart, LocalDateTime periodEnd) {
+        // 查询该供应商已完成/已关闭的采购单
+        LambdaQueryWrapper<PurchaseOrder> wrapper = new LambdaQueryWrapper<PurchaseOrder>()
+            .eq(PurchaseOrder::getSupplierId, supplierId)
+            .in(PurchaseOrder::getStatus,
+                PurchaseOrderStatus.CLOSED.getCode(),
+                PurchaseOrderStatus.COMPLETED.getCode(),
+                PurchaseOrderStatus.FULLY_ARRIVED.getCode());
+        if (periodStart != null) wrapper.ge(PurchaseOrder::getCreatedAt, periodStart);
+        if (periodEnd != null) wrapper.le(PurchaseOrder::getCreatedAt, periodEnd);
+
+        List<PurchaseOrder> pos = purchaseOrderMapper.selectList(wrapper);
+        if (pos.isEmpty()) {
+            throw new IllegalStateException("没有符合条件的采购单可结算");
+        }
+
+        // 检查这些采购单是否已被其他结算单引用
+        for (PurchaseOrder po : pos) {
+            List<SupplierSettlement> existing = mapper.selectList(
+                new LambdaQueryWrapper<SupplierSettlement>()
+                    .like(SupplierSettlement::getPurchaseOrderIds, String.valueOf(po.getId()))
+                    .ne(SupplierSettlement::getStatus, SettlementStatus.PAID.getCode()));
+            if (!existing.isEmpty()) {
+                throw new IllegalStateException("采购单 " + po.getPoNo() + " 已有未结清的结算单");
+            }
+        }
+
+        BigDecimal payable = pos.stream()
+            .map(po -> po.getAmount() != null ? po.getAmount() : BigDecimal.ZERO)
+            .reduce(BigDecimal.ZERO, BigDecimal::add);
+
         StringBuilder poIds = new StringBuilder();
-        for (PurchaseOrder po : pos) { if (poIds.length() > 0) poIds.append(","); poIds.append(po.getId()); }
+        for (PurchaseOrder po : pos) {
+            if (poIds.length() > 0) poIds.append(",");
+            poIds.append(po.getId());
+        }
+
         SupplierSettlement s = new SupplierSettlement();
-        s.setSettlementNo("STL-" + System.currentTimeMillis()); s.setSupplierId(supplierId); s.setPeriod(period);
-        s.setPurchaseOrderIds(poIds.toString()); s.setPayableAmount(payable); s.setPaidAmount(BigDecimal.ZERO); s.setStatus("PENDING");
-        mapper.insert(s); return s.getId();
+        s.setSettlementNo("STL-" + System.currentTimeMillis() % 100000);
+        s.setSupplierId(supplierId);
+        s.setPeriod(period);
+        s.setPurchaseOrderIds(poIds.toString());
+        s.setPayableAmount(payable);
+        s.setPaidAmount(BigDecimal.ZERO);
+        s.setStatus(SettlementStatus.PENDING.getCode());
+        s.setCreatedAt(LocalDateTime.now());
+        s.setUpdatedAt(LocalDateTime.now());
+        mapper.insert(s);
+        return s.getId();
     }
 }

+ 9 - 7
backend/src/main/java/com/oms/service/SysApprovalFlowService.java

@@ -5,6 +5,8 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.oms.converter.SysApprovalFlowConverter;
 import com.oms.dto.SysApprovalFlowDTO;
 import com.oms.entity.SysApprovalFlow;
+import com.oms.enums.AuditStatus;
+import com.oms.enums.GenericStatus;
 import com.oms.mapper.SysApprovalFlowMapper;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -46,14 +48,14 @@ public class SysApprovalFlowService {
 
     public List<SysApprovalFlow> getActiveFlows() {
         return mapper.selectList(new LambdaQueryWrapper<SysApprovalFlow>()
-                .eq(SysApprovalFlow::getStatus, "ACTIVE")
+                .eq(SysApprovalFlow::getStatus, GenericStatus.ACTIVE.getCode())
                 .orderByDesc(SysApprovalFlow::getCreatedAt));
     }
 
     @Transactional
     public Long save(SysApprovalFlow entity) {
         if (entity.getStatus() == null) {
-            entity.setStatus("ACTIVE");
+            entity.setStatus(GenericStatus.ACTIVE.getCode());
         }
         validateFlow(entity);
         mapper.insert(entity);
@@ -75,7 +77,7 @@ public class SysApprovalFlowService {
     public void submit(Long id) {
         SysApprovalFlow af = mapper.selectById(id);
         if (af == null) throw new IllegalArgumentException("Approval flow not found");
-        af.setStatus("PENDING");
+        af.setStatus(AuditStatus.PENDING.getCode());
         mapper.updateById(af);
     }
 
@@ -83,7 +85,7 @@ public class SysApprovalFlowService {
     public void approve(Long id) {
         SysApprovalFlow af = mapper.selectById(id);
         if (af == null) throw new IllegalArgumentException("Approval flow not found");
-        af.setStatus("APPROVED");
+        af.setStatus(AuditStatus.APPROVED.getCode());
         mapper.updateById(af);
     }
 
@@ -91,7 +93,7 @@ public class SysApprovalFlowService {
     public void reject(Long id) {
         SysApprovalFlow af = mapper.selectById(id);
         if (af == null) throw new IllegalArgumentException("Approval flow not found");
-        af.setStatus("REJECTED");
+        af.setStatus(AuditStatus.REJECTED.getCode());
         mapper.updateById(af);
     }
 
@@ -100,7 +102,7 @@ public class SysApprovalFlowService {
         SysApprovalFlow af = mapper.selectById(id);
         if (af == null) throw new IllegalArgumentException("Approval flow not found");
         validateFlow(af);
-        af.setStatus("ACTIVE");
+        af.setStatus(GenericStatus.ACTIVE.getCode());
         mapper.updateById(af);
     }
 
@@ -108,7 +110,7 @@ public class SysApprovalFlowService {
     public void deactivate(Long id) {
         SysApprovalFlow af = mapper.selectById(id);
         if (af == null) throw new IllegalArgumentException("Approval flow not found");
-        af.setStatus("INACTIVE");
+        af.setStatus(GenericStatus.INACTIVE.getCode());
         mapper.updateById(af);
     }
 

Some files were not shown because too many files changed in this diff