Browse Source

feat: 库存管理 - 入库单日期/金额/经办人 + 手动出库 + 订单快递费

Xiaogang Liao 3 months ago
parent
commit
61ae11bdfc

+ 9 - 1
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -7828,7 +7828,15 @@ private void runMigration100() {
 			log.info("已创建inventory_inbound_items表");
 			log.info("已创建inventory_inbound_items表");
 		} catch (Exception e) { /* 表已存在,忽略 */ }
 		} catch (Exception e) { /* 表已存在,忽略 */ }
 
 
-		// 迁移174: product_skus 添加 min_stock_alert 列(SKU级别库存预警阈值,配合库存管理模块)
+		// 迁移174: product_skus 添加 min_stock_alert 和 min_quantity 列(SKU库存预警+最小购买数,配合库存管理模块)
 		ensureColumn("product_skus", "min_stock_alert", "INT DEFAULT 0 COMMENT '库存预警阈值(低于此值预警)'");
 		ensureColumn("product_skus", "min_stock_alert", "INT DEFAULT 0 COMMENT '库存预警阈值(低于此值预警)'");
+		ensureColumn("product_skus", "min_quantity", "INT DEFAULT 1 COMMENT '最小购买数量限制'");
+
+		// 迁移175: inventory_inbound_orders 添加 inbound_date 和 total_amount 列(入库单扩展)
+		ensureColumn("inventory_inbound_orders", "inbound_date", "DATE COMMENT '入库日期'");
+		ensureColumn("inventory_inbound_orders", "total_amount", "INT DEFAULT 0 COMMENT '入库总金额(分)'");
+
+		// 迁移176: product_orders 添加 shipping_cost 列(快递费)
+		ensureColumn("product_orders", "shipping_cost", "INT DEFAULT 0 COMMENT '快递费(分)'");
 	}
 	}
 }
 }

+ 42 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminInventoryController.java

@@ -8,6 +8,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
+import java.text.SimpleDateFormat;
+import java.util.Date;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
@@ -28,9 +30,16 @@ public class AdminInventoryController {
         Long supplierId = params.get("supplierId") != null ? Long.valueOf(params.get("supplierId").toString()) : null;
         Long supplierId = params.get("supplierId") != null ? Long.valueOf(params.get("supplierId").toString()) : null;
         String supplierName = (String) params.get("supplierName");
         String supplierName = (String) params.get("supplierName");
         String remark = (String) params.get("remark");
         String remark = (String) params.get("remark");
+        Integer totalAmount = params.get("totalAmount") != null ? Integer.valueOf(params.get("totalAmount").toString()) : null;
+        Date inboundDate = null;
+        if (params.get("inboundDate") != null) {
+            try {
+                inboundDate = new SimpleDateFormat("yyyy-MM-dd").parse(params.get("inboundDate").toString());
+            } catch (Exception ignored) {}
+        }
         @SuppressWarnings("unchecked")
         @SuppressWarnings("unchecked")
         List<Map<String, Object>> items = (List<Map<String, Object>>) params.get("items");
         List<Map<String, Object>> items = (List<Map<String, Object>>) params.get("items");
-        return inventoryService.createInboundOrder(supplierId, supplierName, items, adminId, remark);
+        return inventoryService.createInboundOrder(supplierId, supplierName, items, adminId, remark, inboundDate, totalAmount);
     }
     }
 
 
     @Operation(summary = "入库单列表")
     @Operation(summary = "入库单列表")
@@ -166,4 +175,36 @@ public class AdminInventoryController {
         if (bundleProductId == null) return Result.error("bundleProductId不能为空");
         if (bundleProductId == null) return Result.error("bundleProductId不能为空");
         return inventoryService.checkBundleStock(bundleProductId, quantity);
         return inventoryService.checkBundleStock(bundleProductId, quantity);
     }
     }
+
+    // ========== 手动出库 ==========
+
+    @Operation(summary = "手动出库")
+    @PostMapping("/outbound/create")
+    public Result<String> createManualOutbound(@RequestBody Map<String, Object> params,
+                                                @RequestAttribute("userId") Long adminId) {
+        Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null;
+        Long skuId = params.get("skuId") != null ? Long.valueOf(params.get("skuId").toString()) : null;
+        Integer quantity = params.get("quantity") != null ? Integer.valueOf(params.get("quantity").toString()) : null;
+        String reason = params.get("reason") != null ? params.get("reason").toString() : null;
+        String remark = (String) params.get("remark");
+        Date outboundDate = null;
+        if (params.get("outboundDate") != null) {
+            try {
+                outboundDate = new SimpleDateFormat("yyyy-MM-dd").parse(params.get("outboundDate").toString());
+            } catch (Exception ignored) {}
+        }
+        if (productId == null) return Result.error("productId不能为空");
+        if (quantity == null || quantity <= 0) return Result.error("数量无效");
+        return inventoryService.createManualOutbound(productId, skuId, quantity, reason, adminId, outboundDate, remark);
+    }
+
+    @Operation(summary = "手动出库记录列表")
+    @PostMapping("/outbound/list")
+    public Result<Map<String, Object>> listManualOutbound(@RequestBody Map<String, Object> params) {
+        int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
+        int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
+        String startDate = (String) params.get("startDate");
+        String endDate = (String) params.get("endDate");
+        return inventoryService.listManualOutboundTransactions(page, size, startDate, endDate);
+    }
 }
 }

+ 15 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductOrderController.java

@@ -44,14 +44,27 @@ public class AdminProductOrderController {
         return orderService.adminOrderDetail(orderNo);
         return orderService.adminOrderDetail(orderNo);
     }
     }
 
 
-    @Operation(summary = "订单发货")
+    @Operation(summary = "订单发货(含快递单号+快递费)")
     @PostMapping("/ship")
     @PostMapping("/ship")
     public Result<String> ship(@RequestBody Map<String, Object> params,
     public Result<String> ship(@RequestBody Map<String, Object> params,
                                @RequestAttribute("userId") Long adminId) {
                                @RequestAttribute("userId") Long adminId) {
         String orderNo = (String) params.get("orderNo");
         String orderNo = (String) params.get("orderNo");
         String trackingNumber = (String) params.get("trackingNumber");
         String trackingNumber = (String) params.get("trackingNumber");
         String expressCompany = (String) params.get("expressCompany");
         String expressCompany = (String) params.get("expressCompany");
-        return orderService.ship(orderNo, trackingNumber, expressCompany);
+        Integer shippingCost = params.get("shippingCost") != null
+                ? Integer.valueOf(params.get("shippingCost").toString()) : null;
+        return orderService.ship(orderNo, trackingNumber, expressCompany, shippingCost);
+    }
+
+    @Operation(summary = "更新订单快递信息")
+    @PostMapping("/update-express")
+    public Result<String> updateExpress(@RequestBody Map<String, Object> params) {
+        String orderNo = (String) params.get("orderNo");
+        String trackingNumber = (String) params.get("trackingNumber");
+        String expressCompany = (String) params.get("expressCompany");
+        Integer shippingCost = params.get("shippingCost") != null
+                ? Integer.valueOf(params.get("shippingCost").toString()) : null;
+        return orderService.updateExpress(orderNo, trackingNumber, expressCompany, shippingCost);
     }
     }
 
 
     @Operation(summary = "关闭订单")
     @Operation(summary = "关闭订单")

+ 1 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/product/ProductOrderController.java

@@ -98,7 +98,7 @@ private QrCodeService qrCodeService;
         String orderNo = (String) params.get("orderNo");
         String orderNo = (String) params.get("orderNo");
         String trackingNumber = (String) params.get("trackingNumber");
         String trackingNumber = (String) params.get("trackingNumber");
         String expressCompany = (String) params.get("expressCompany");
         String expressCompany = (String) params.get("expressCompany");
-        return orderService.ship(orderNo, trackingNumber, expressCompany);
+        return orderService.ship(orderNo, trackingNumber, expressCompany, null);
     }
     }
 
 
     // ========== 退款流程 ==========
     // ========== 退款流程 ==========

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/InventoryInboundOrder.java

@@ -18,6 +18,8 @@ public class InventoryInboundOrder implements Serializable {
     private String supplierName;
     private String supplierName;
     private String status;
     private String status;
     private Integer totalItems;
     private Integer totalItems;
+    private Integer totalAmount;
+    private Date inboundDate;
     private Long operatorId;
     private Long operatorId;
     private String remark;
     private String remark;
     private Date createdAt;
     private Date createdAt;

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java

@@ -63,6 +63,7 @@ public class ProductOrder implements Serializable {
     private Long auditorId;
     private Long auditorId;
     private Date auditedAt;
     private Date auditedAt;
     private String auditReason;
     private String auditReason;
+    private Integer shippingCost;
 
 
     private Date createdAt;
     private Date createdAt;
     private Date updatedAt;
     private Date updatedAt;

+ 72 - 1
cfc-backend/src/main/java/com/etotem/cfc/service/InventoryService.java

@@ -34,7 +34,8 @@ public class InventoryService {
 
 
     @Transactional
     @Transactional
     public Result<Map<String, Object>> createInboundOrder(Long supplierId, String supplierName,
     public Result<Map<String, Object>> createInboundOrder(Long supplierId, String supplierName,
-                                                           List<Map<String, Object>> items, Long operatorId, String remark) {
+                                                           List<Map<String, Object>> items, Long operatorId,
+                                                           String remark, Date inboundDate, Integer totalAmount) {
         if (items == null || items.isEmpty()) {
         if (items == null || items.isEmpty()) {
             return Result.error("入库明细不能为空");
             return Result.error("入库明细不能为空");
         }
         }
@@ -47,6 +48,8 @@ public class InventoryService {
         order.setSupplierName(supplierName);
         order.setSupplierName(supplierName);
         order.setStatus("pending");
         order.setStatus("pending");
         order.setTotalItems(items.size());
         order.setTotalItems(items.size());
+        order.setTotalAmount(totalAmount != null ? totalAmount : 0);
+        order.setInboundDate(inboundDate);
         order.setOperatorId(operatorId);
         order.setOperatorId(operatorId);
         order.setRemark(remark);
         order.setRemark(remark);
         order.setCreatedAt(new Date());
         order.setCreatedAt(new Date());
@@ -470,4 +473,72 @@ public class InventoryService {
         }
         }
         return Result.success("库存充足");
         return Result.success("库存充足");
     }
     }
+
+    // ========== 手动出库 ==========
+
+    @Transactional
+    public Result<String> createManualOutbound(Long productId, Long skuId, Integer quantity,
+                                                String reason, Long operatorId, Date outboundDate, String remark) {
+        Product product = productMapper.selectById(productId);
+        if (product == null) return Result.error("商品不存在");
+
+        if (skuId != null) {
+            ProductSku sku = productSkuMapper.selectById(skuId);
+            if (sku == null || sku.getStock() == null || sku.getStock() < quantity) {
+                return Result.error("SKU库存不足");
+            }
+            sku.setStock(sku.getStock() - quantity);
+            sku.setUpdateTime(new Date());
+            productSkuMapper.updateById(sku);
+        } else {
+            if (product.getStock() == null || product.getStock() < quantity) {
+                return Result.error("库存不足");
+            }
+            product.setStock(product.getStock() - quantity);
+            product.setUpdatedAt(new Date());
+            productMapper.updateById(product);
+        }
+
+        InventoryTransaction tx = new InventoryTransaction();
+        tx.setProductId(productId);
+        tx.setSkuId(skuId);
+        tx.setType("outbound");
+        tx.setDirection(-1);
+        tx.setQuantity(quantity);
+        tx.setBeforeStock(product.getStock() != null ? product.getStock() + quantity : quantity);
+        tx.setAfterStock(product.getStock() != null ? product.getStock() : 0);
+        tx.setReferenceType("manual_outbound");
+        tx.setOperatorId(operatorId);
+        tx.setRemark("手动出库: " + (reason != null ? reason : "") + (remark != null ? " - " + remark : ""));
+        tx.setCreatedAt(new Date());
+        transactionMapper.insert(tx);
+        return Result.success("出库成功");
+    }
+
+    public Result<Map<String, Object>> listManualOutboundTransactions(int page, int size, String startDate, String endDate) {
+        Page<InventoryTransaction> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<InventoryTransaction> wrapper = new LambdaQueryWrapper<InventoryTransaction>()
+                .eq(InventoryTransaction::getReferenceType, "manual_outbound")
+                .orderByDesc(InventoryTransaction::getCreatedAt);
+        if (startDate != null && !startDate.isEmpty()) {
+            try {
+                wrapper.ge(InventoryTransaction::getCreatedAt, new SimpleDateFormat("yyyy-MM-dd").parse(startDate));
+            } catch (Exception ignored) {}
+        }
+        if (endDate != null && !endDate.isEmpty()) {
+            try {
+                Calendar cal = Calendar.getInstance();
+                cal.setTime(new SimpleDateFormat("yyyy-MM-dd").parse(endDate));
+                cal.add(Calendar.DAY_OF_MONTH, 1);
+                wrapper.lt(InventoryTransaction::getCreatedAt, cal.getTime());
+            } catch (Exception ignored) {}
+        }
+        Page<InventoryTransaction> result = transactionMapper.selectPage(pageParam, wrapper);
+        Map<String, Object> data = new HashMap<>();
+        data.put("records", result.getRecords());
+        data.put("total", result.getTotal());
+        data.put("page", result.getCurrent());
+        data.put("size", result.getSize());
+        return Result.success(data);
+    }
 }
 }

+ 21 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -468,6 +468,10 @@ public class ProductOrderService {
     }
     }
 
 
     public Result<String> ship(String orderNo, String trackingNumber, String expressCompany) {
     public Result<String> ship(String orderNo, String trackingNumber, String expressCompany) {
+        return ship(orderNo, trackingNumber, expressCompany, null);
+    }
+
+    public Result<String> ship(String orderNo, String trackingNumber, String expressCompany, Integer shippingCost) {
         ProductOrder order = orderMapper.selectOne(
         ProductOrder order = orderMapper.selectOne(
             new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
             new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
         if (order == null) {
         if (order == null) {
@@ -484,11 +488,28 @@ public class ProductOrderService {
             order.setLogisticsCompany(expressCompany);
             order.setLogisticsCompany(expressCompany);
             order.setStatus("pending_receipt");
             order.setStatus("pending_receipt");
         }
         }
+        if (shippingCost != null) {
+            order.setShippingCost(shippingCost);
+        }
         order.setUpdatedAt(new Date());
         order.setUpdatedAt(new Date());
         orderMapper.updateById(order);
         orderMapper.updateById(order);
         return Result.success("已发货");
         return Result.success("已发货");
     }
     }
 
 
+    public Result<String> updateExpress(String orderNo, String trackingNumber, String expressCompany, Integer shippingCost) {
+        ProductOrder order = orderMapper.selectOne(
+            new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
+        if (order == null) {
+            return Result.error("订单不存在");
+        }
+        if (trackingNumber != null) order.setTrackingNumber(trackingNumber);
+        if (expressCompany != null) order.setExpressCompany(expressCompany);
+        if (shippingCost != null) order.setShippingCost(shippingCost);
+        order.setUpdatedAt(new Date());
+        orderMapper.updateById(order);
+        return Result.success("快递信息已更新");
+    }
+
     public Result<String> confirmReceive(String orderNo, Long userId) {
     public Result<String> confirmReceive(String orderNo, Long userId) {
         ProductOrder order = orderMapper.selectOne(
         ProductOrder order = orderMapper.selectOne(
             new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));
             new LambdaQueryWrapper<ProductOrder>().eq(ProductOrder::getOrderNo, orderNo));

+ 4 - 1
cfc-backend/src/main/resources/schema.sql

@@ -1141,6 +1141,8 @@ CREATE TABLE IF NOT EXISTS inventory_inbound_orders (
     supplier_name VARCHAR(100) COMMENT '供应商名称',
     supplier_name VARCHAR(100) COMMENT '供应商名称',
     status VARCHAR(16) DEFAULT 'pending' COMMENT '状态',
     status VARCHAR(16) DEFAULT 'pending' COMMENT '状态',
     total_items INT DEFAULT 0 COMMENT '商品种类数',
     total_items INT DEFAULT 0 COMMENT '商品种类数',
+    total_amount INT DEFAULT 0 COMMENT '入库总金额(分)',
+    inbound_date DATE COMMENT '入库日期',
     operator_id BIGINT COMMENT '操作人ID',
     operator_id BIGINT COMMENT '操作人ID',
     remark TEXT COMMENT '备注',
     remark TEXT COMMENT '备注',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
@@ -1204,7 +1206,8 @@ provision_status VARCHAR(32) COMMENT '活动provision状态: unprovisioned(待
 auditor_id BIGINT COMMENT '审核人ID',
 auditor_id BIGINT COMMENT '审核人ID',
 audited_at DATETIME COMMENT '审核时间',
 audited_at DATETIME COMMENT '审核时间',
 audit_reason TEXT COMMENT '审核拒绝原因',
 audit_reason TEXT COMMENT '审核拒绝原因',
-created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    shipping_cost INT DEFAULT 0 COMMENT '快递费(分)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
     INDEX idx_buyer_id (buyer_id),
     INDEX idx_buyer_id (buyer_id),
     INDEX idx_family_id (family_id),
     INDEX idx_family_id (family_id),