Bläddra i källkod

feat: 库存管理 - InventoryService

Xiaogang Liao 1 månad sedan
förälder
incheckning
7a5f2fee80
1 ändrade filer med 473 tillägg och 0 borttagningar
  1. 473 0
      cfc-backend/src/main/java/com/etotem/cfc/service/InventoryService.java

+ 473 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/InventoryService.java

@@ -0,0 +1,473 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+@Service
+public class InventoryService {
+
+    @Resource
+    private InventoryTransactionMapper transactionMapper;
+    @Resource
+    private InventoryInboundOrderMapper inboundOrderMapper;
+    @Resource
+    private InventoryInboundItemMapper inboundItemMapper;
+    @Resource
+    private InventoryCountingRecordMapper countingRecordMapper;
+    @Resource
+    private ProductBundleItemMapper bundleItemMapper;
+    @Resource
+    private ProductMapper productMapper;
+    @Resource
+    private ProductSkuMapper productSkuMapper;
+
+    // ========== 入库单 ==========
+
+    @Transactional
+    public Result<Map<String, Object>> createInboundOrder(Long supplierId, String supplierName,
+                                                           List<Map<String, Object>> items, Long operatorId, String remark) {
+        if (items == null || items.isEmpty()) {
+            return Result.error("入库明细不能为空");
+        }
+        String orderNo = "IN-" + new SimpleDateFormat("yyyyMMdd").format(new Date())
+                + "-" + System.currentTimeMillis() % 10000;
+
+        InventoryInboundOrder order = new InventoryInboundOrder();
+        order.setOrderNo(orderNo);
+        order.setSupplierId(supplierId);
+        order.setSupplierName(supplierName);
+        order.setStatus("pending");
+        order.setTotalItems(items.size());
+        order.setOperatorId(operatorId);
+        order.setRemark(remark);
+        order.setCreatedAt(new Date());
+        order.setUpdatedAt(new Date());
+        inboundOrderMapper.insert(order);
+
+        for (Map<String, Object> item : items) {
+            InventoryInboundItem inboundItem = new InventoryInboundItem();
+            inboundItem.setInboundOrderId(order.getId());
+            inboundItem.setProductId(Long.valueOf(item.get("productId").toString()));
+            if (item.get("skuId") != null) {
+                inboundItem.setSkuId(Long.valueOf(item.get("skuId").toString()));
+            }
+            inboundItem.setQuantity(Integer.valueOf(item.get("quantity").toString()));
+            inboundItem.setCreatedAt(new Date());
+            inboundItemMapper.insert(inboundItem);
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("id", order.getId());
+        result.put("orderNo", orderNo);
+        return Result.success(result);
+    }
+
+    @Transactional
+    public Result<String> confirmInbound(Long orderId, Long operatorId) {
+        InventoryInboundOrder order = inboundOrderMapper.selectById(orderId);
+        if (order == null) return Result.error("入库单不存在");
+        if (!"pending".equals(order.getStatus())) return Result.error("入库单状态异常");
+
+        List<InventoryInboundItem> items = inboundItemMapper.selectList(
+                new LambdaQueryWrapper<InventoryInboundItem>().eq(InventoryInboundItem::getInboundOrderId, orderId));
+
+        for (InventoryInboundItem item : items) {
+            Product product = productMapper.selectById(item.getProductId());
+            if (product == null) continue;
+
+            int beforeStock = product.getStock() != null ? product.getStock() : 0;
+            int afterStock = beforeStock + item.getQuantity();
+
+            if (item.getSkuId() != null) {
+                ProductSku sku = productSkuMapper.selectById(item.getSkuId());
+                if (sku != null) {
+                    int skuBefore = sku.getStock() != null ? sku.getStock() : 0;
+                    sku.setStock(skuBefore + item.getQuantity());
+                    sku.setUpdateTime(new Date());
+                    productSkuMapper.updateById(sku);
+                }
+            } else {
+                product.setStock(afterStock);
+                product.setUpdatedAt(new Date());
+                productMapper.updateById(product);
+            }
+
+            InventoryTransaction tx = new InventoryTransaction();
+            tx.setProductId(item.getProductId());
+            tx.setSkuId(item.getSkuId());
+            tx.setType("inbound");
+            tx.setDirection(1);
+            tx.setQuantity(item.getQuantity());
+            tx.setBeforeStock(beforeStock);
+            tx.setAfterStock(afterStock);
+            tx.setReferenceType("inbound_order");
+            tx.setReferenceId(orderId);
+            tx.setOperatorId(operatorId);
+            tx.setCreatedAt(new Date());
+            transactionMapper.insert(tx);
+        }
+
+        order.setStatus("completed");
+        order.setUpdatedAt(new Date());
+        inboundOrderMapper.updateById(order);
+        return Result.success("入库成功");
+    }
+
+    @Transactional
+    public Result<String> cancelInbound(Long orderId) {
+        InventoryInboundOrder order = inboundOrderMapper.selectById(orderId);
+        if (order == null) return Result.error("入库单不存在");
+        if (!"pending".equals(order.getStatus())) return Result.error("仅待确认入库单可取消");
+        order.setStatus("cancelled");
+        order.setUpdatedAt(new Date());
+        inboundOrderMapper.updateById(order);
+        return Result.success("已取消");
+    }
+
+    public Result<Map<String, Object>> listInboundOrders(int page, int size, String status, String keyword) {
+        Page<InventoryInboundOrder> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<InventoryInboundOrder> wrapper = new LambdaQueryWrapper<InventoryInboundOrder>()
+                .orderByDesc(InventoryInboundOrder::getCreatedAt);
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(InventoryInboundOrder::getStatus, status);
+        }
+        Page<InventoryInboundOrder> result = inboundOrderMapper.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);
+    }
+
+    public Result<Map<String, Object>> detailInboundOrder(Long id) {
+        InventoryInboundOrder order = inboundOrderMapper.selectById(id);
+        if (order == null) return Result.error("入库单不存在");
+        List<InventoryInboundItem> items = inboundItemMapper.selectList(
+                new LambdaQueryWrapper<InventoryInboundItem>().eq(InventoryInboundItem::getInboundOrderId, id));
+        Map<String, Object> data = new HashMap<>();
+        data.put("order", order);
+        data.put("items", items);
+        return Result.success(data);
+    }
+
+    // ========== 库存流水 ==========
+
+    public Result<Map<String, Object>> listTransactions(int page, int size, Long productId, Long skuId,
+                                                         String type, String startDate, String endDate) {
+        Page<InventoryTransaction> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<InventoryTransaction> wrapper = new LambdaQueryWrapper<InventoryTransaction>()
+                .orderByDesc(InventoryTransaction::getCreatedAt);
+        if (productId != null) wrapper.eq(InventoryTransaction::getProductId, productId);
+        if (skuId != null) wrapper.eq(InventoryTransaction::getSkuId, skuId);
+        if (type != null && !type.isEmpty()) wrapper.eq(InventoryTransaction::getType, type);
+        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);
+    }
+
+    // ========== 记录出库 ==========
+
+    @Transactional
+    public void recordOutbound(Long productId, Long skuId, Integer quantity, String referenceType,
+                                Long referenceId, Long operatorId, String remark) {
+        Product product = productMapper.selectById(productId);
+        if (product == null) return;
+
+        int beforeStock = product.getStock() != null ? product.getStock() : 0;
+        int afterStock = beforeStock - quantity;
+
+        InventoryTransaction tx = new InventoryTransaction();
+        tx.setProductId(productId);
+        tx.setSkuId(skuId);
+        tx.setType("outbound");
+        tx.setDirection(-1);
+        tx.setQuantity(quantity);
+        tx.setBeforeStock(beforeStock);
+        tx.setAfterStock(afterStock);
+        tx.setReferenceType(referenceType);
+        tx.setReferenceId(referenceId);
+        tx.setOperatorId(operatorId);
+        tx.setRemark(remark);
+        tx.setCreatedAt(new Date());
+        transactionMapper.insert(tx);
+    }
+
+    @Transactional
+    public void recordRefundInbound(Long productId, Long skuId, Integer quantity, String referenceType,
+                                     Long referenceId, Long operatorId, String remark) {
+        Product product = productMapper.selectById(productId);
+        if (product == null) return;
+
+        int beforeStock = product.getStock() != null ? product.getStock() : 0;
+        int afterStock = beforeStock + quantity;
+
+        InventoryTransaction tx = new InventoryTransaction();
+        tx.setProductId(productId);
+        tx.setSkuId(skuId);
+        tx.setType("refund");
+        tx.setDirection(1);
+        tx.setQuantity(quantity);
+        tx.setBeforeStock(beforeStock);
+        tx.setAfterStock(afterStock);
+        tx.setReferenceType(referenceType);
+        tx.setReferenceId(referenceId);
+        tx.setOperatorId(operatorId);
+        tx.setRemark(remark);
+        tx.setCreatedAt(new Date());
+        transactionMapper.insert(tx);
+    }
+
+    // ========== 盘点 ==========
+
+    @Transactional
+    public Result<String> createCounting(Long productId, Long skuId, Integer actualStock, Long operatorId, String remark) {
+        Product product = productMapper.selectById(productId);
+        if (product == null) return Result.error("商品不存在");
+
+        int expectedStock = 0;
+        if (skuId != null) {
+            ProductSku sku = productSkuMapper.selectById(skuId);
+            if (sku != null) expectedStock = sku.getStock() != null ? sku.getStock() : 0;
+        } else {
+            expectedStock = product.getStock() != null ? product.getStock() : 0;
+        }
+
+        InventoryCountingRecord record = new InventoryCountingRecord();
+        record.setProductId(productId);
+        record.setSkuId(skuId);
+        record.setExpectedStock(expectedStock);
+        record.setActualStock(actualStock);
+        record.setDifference(actualStock - expectedStock);
+        record.setStatus("pending");
+        record.setOperatorId(operatorId);
+        record.setRemark(remark);
+        record.setCreatedAt(new Date());
+        countingRecordMapper.insert(record);
+        return Result.success("盘点记录已创建");
+    }
+
+    @Transactional
+    public Result<String> confirmCounting(Long recordId, Long operatorId) {
+        InventoryCountingRecord record = countingRecordMapper.selectById(recordId);
+        if (record == null) return Result.error("盘点记录不存在");
+        if (!"pending".equals(record.getStatus())) return Result.error("盘点记录已确认");
+
+        if (record.getDifference() != 0) {
+            if (record.getSkuId() != null) {
+                ProductSku sku = productSkuMapper.selectById(record.getSkuId());
+                if (sku != null) {
+                    sku.setStock(record.getActualStock());
+                    sku.setUpdateTime(new Date());
+                    productSkuMapper.updateById(sku);
+                }
+            } else {
+                Product product = productMapper.selectById(record.getProductId());
+                if (product != null) {
+                    product.setStock(record.getActualStock());
+                    product.setUpdatedAt(new Date());
+                    productMapper.updateById(product);
+                }
+            }
+
+            InventoryTransaction tx = new InventoryTransaction();
+            tx.setProductId(record.getProductId());
+            tx.setSkuId(record.getSkuId());
+            tx.setType("adjustment");
+            tx.setDirection(record.getDifference() > 0 ? 1 : -1);
+            tx.setQuantity(Math.abs(record.getDifference()));
+            tx.setBeforeStock(record.getExpectedStock());
+            tx.setAfterStock(record.getActualStock());
+            tx.setReferenceType("counting");
+            tx.setReferenceId(recordId);
+            tx.setOperatorId(operatorId);
+            tx.setRemark("盘点调账: " + record.getRemark());
+            tx.setCreatedAt(new Date());
+            transactionMapper.insert(tx);
+        }
+
+        record.setStatus("confirmed");
+        countingRecordMapper.updateById(record);
+        return Result.success("盘点已确认");
+    }
+
+    public Result<Map<String, Object>> listCountingRecords(int page, int size, String status) {
+        Page<InventoryCountingRecord> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<InventoryCountingRecord> wrapper = new LambdaQueryWrapper<InventoryCountingRecord>()
+                .orderByDesc(InventoryCountingRecord::getCreatedAt);
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(InventoryCountingRecord::getStatus, status);
+        }
+        Page<InventoryCountingRecord> result = countingRecordMapper.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);
+    }
+
+    // ========== 库存预警 ==========
+
+    public Result<List<Map<String, Object>>> listStockAlerts() {
+        List<Map<String, Object>> alerts = new ArrayList<>();
+
+        List<Product> products = productMapper.selectList(
+                new LambdaQueryWrapper<Product>()
+                        .eq(Product::getProductType, "physical")
+                        .apply("min_stock_alert IS NOT NULL AND stock < min_stock_alert"));
+        for (Product p : products) {
+            Map<String, Object> alert = new HashMap<>();
+            alert.put("productId", p.getId());
+            alert.put("productName", p.getName());
+            alert.put("skuId", null);
+            alert.put("specs", null);
+            alert.put("currentStock", p.getStock());
+            alert.put("minStockAlert", p.getMinStockAlert());
+            alerts.add(alert);
+        }
+
+        List<ProductSku> skus = productSkuMapper.selectList(
+                new LambdaQueryWrapper<ProductSku>()
+                        .apply("min_stock_alert IS NOT NULL AND stock < min_stock_alert AND enabled = 1"));
+        for (ProductSku sku : skus) {
+            Product p = productMapper.selectById(sku.getProductId());
+            Map<String, Object> alert = new HashMap<>();
+            alert.put("productId", sku.getProductId());
+            alert.put("productName", p != null ? p.getName() : "未知");
+            alert.put("skuId", sku.getId());
+            alert.put("specs", sku.getSpecs());
+            alert.put("currentStock", sku.getStock());
+            alert.put("minStockAlert", sku.getMinStockAlert());
+            alerts.add(alert);
+        }
+
+        return Result.success(alerts);
+    }
+
+    // ========== 商品库存详情 ==========
+
+    public Result<Map<String, Object>> getProductStock(Long productId) {
+        Product product = productMapper.selectById(productId);
+        if (product == null) return Result.error("商品不存在");
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("productId", product.getId());
+        data.put("productName", product.getName());
+        data.put("productType", product.getProductType());
+        data.put("stock", product.getStock());
+        data.put("minStockAlert", product.getMinStockAlert());
+
+        List<ProductSku> skus = productSkuMapper.findByProductIdAll(productId);
+        data.put("skus", skus);
+
+        List<InventoryTransaction> recentTx = transactionMapper.selectList(
+                new LambdaQueryWrapper<InventoryTransaction>()
+                        .eq(InventoryTransaction::getProductId, productId)
+                        .orderByDesc(InventoryTransaction::getCreatedAt)
+                        .last("LIMIT 10"));
+        data.put("recentTransactions", recentTx);
+
+        if ("bundle".equals(product.getProductType())) {
+            List<ProductBundleItem> bundleItems = bundleItemMapper.selectList(
+                    new LambdaQueryWrapper<ProductBundleItem>()
+                            .eq(ProductBundleItem::getBundleProductId, productId));
+            List<Map<String, Object>> childProducts = new ArrayList<>();
+            for (ProductBundleItem bi : bundleItems) {
+                Product child = productMapper.selectById(bi.getChildProductId());
+                if (child != null) {
+                    Map<String, Object> cp = new HashMap<>();
+                    cp.put("productId", child.getId());
+                    cp.put("productName", child.getName());
+                    cp.put("stock", child.getStock());
+                    cp.put("quantity", bi.getQuantity());
+                    cp.put("skuId", bi.getChildSkuId());
+                    childProducts.add(cp);
+                }
+            }
+            data.put("bundleItems", childProducts);
+        }
+
+        return Result.success(data);
+    }
+
+    // ========== 套餐商品组成 ==========
+
+    @Transactional
+    public Result<String> saveBundleItems(Long bundleProductId, List<Map<String, Object>> items) {
+        bundleItemMapper.delete(new LambdaQueryWrapper<ProductBundleItem>()
+                .eq(ProductBundleItem::getBundleProductId, bundleProductId));
+
+        for (Map<String, Object> item : items) {
+            ProductBundleItem bi = new ProductBundleItem();
+            bi.setBundleProductId(bundleProductId);
+            bi.setChildProductId(Long.valueOf(item.get("childProductId").toString()));
+            if (item.get("childSkuId") != null) {
+                bi.setChildSkuId(Long.valueOf(item.get("childSkuId").toString()));
+            }
+            bi.setQuantity(item.get("quantity") != null ? Integer.valueOf(item.get("quantity").toString()) : 1);
+            bi.setCreatedAt(new Date());
+            bundleItemMapper.insert(bi);
+        }
+        return Result.success("套餐商品配置已保存");
+    }
+
+    public Result<List<ProductBundleItem>> listBundleItems(Long bundleProductId) {
+        List<ProductBundleItem> items = bundleItemMapper.selectList(
+                new LambdaQueryWrapper<ProductBundleItem>()
+                        .eq(ProductBundleItem::getBundleProductId, bundleProductId));
+        return Result.success(items);
+    }
+
+    // ========== 套餐库存检查 ==========
+
+    public Result<String> checkBundleStock(Long bundleProductId, Integer quantity) {
+        List<ProductBundleItem> items = bundleItemMapper.selectList(
+                new LambdaQueryWrapper<ProductBundleItem>()
+                        .eq(ProductBundleItem::getBundleProductId, bundleProductId));
+        if (items.isEmpty()) return Result.error("套餐未配置子商品");
+
+        for (ProductBundleItem bi : items) {
+            int need = bi.getQuantity() * quantity;
+            if (bi.getChildSkuId() != null) {
+                ProductSku sku = productSkuMapper.selectById(bi.getChildSkuId());
+                if (sku == null || sku.getStock() == null || sku.getStock() < need) {
+                    Product p = productMapper.selectById(bi.getChildProductId());
+                    return Result.error("子商品库存不足: " + (p != null ? p.getName() : "未知"));
+                }
+            } else {
+                Product p = productMapper.selectById(bi.getChildProductId());
+                if (p == null || p.getStock() == null || p.getStock() < need) {
+                    return Result.error("子商品库存不足: " + (p != null ? p.getName() : "未知"));
+                }
+            }
+        }
+        return Result.success("库存充足");
+    }
+}