# 库存管理 实现计划 > **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。 **目标:** 为商品管理模块增加完整的库存管理(入库单+流水+盘点+预警+套餐拆库存),支持商品及 SKU 级别。 **架构:** 后端新增 5 张表 + InventoryService + AdminInventoryController,订单支付/取消/退款时自动记录库存流水;管理端新增 InventoryManage.vue(4 标签页)+ InventoryInboundCreate.vue **技术栈:** Spring Boot 2.7.18 + MyBatis-Plus + Vue 2 + Element UI --- ## 文件结构 ### 后端新增 - `entity/InventoryTransaction.java` — 库存流水实体 - `entity/InventoryInboundOrder.java` — 入库单实体 - `entity/InventoryInboundItem.java` — 入库单明细实体 - `entity/InventoryCountingRecord.java` — 盘点记录实体 - `entity/ProductBundleItem.java` — 套餐商品组成实体 - `mapper/InventoryTransactionMapper.java` — 库存流水 Mapper - `mapper/InventoryInboundOrderMapper.java` — 入库单 Mapper - `mapper/InventoryInboundItemMapper.java` — 入库单明细 Mapper - `mapper/InventoryCountingRecordMapper.java` — 盘点记录 Mapper - `mapper/ProductBundleItemMapper.java` — 套餐组成 Mapper - `service/InventoryService.java` — 库存管理核心服务 - `controller/admin/AdminInventoryController.java` — 管理端库存 API ### 后端修改 - `entity/Product.java` — 加 `minStockAlert` 字段 - `entity/ProductSku.java` — 加 `minStockAlert` 字段 - `config/DatabaseInitializer.java` — 迁移 168-172 建表加列 - `resources/schema.sql` — 同步新表定义 - `service/ProductOrderService.java` — 支付/取消/退款时记录库存流水 + bundle 商品穿透 - `service/ProductService.java` — 增加 bundle 库存检查支持 ### 管理端新增 - `views/admin/InventoryManage.vue` — 库存管理主页(4 标签页) - `views/admin/InventoryInboundCreate.vue` — 创建入库单 ### 管理端修改 - `api/admin.js` — 新增库存管理 API 调用 - `router/index.js` — 新增库存管理路由 - `views/admin/ProductManage.vue` — 每行加「库存」按钮 --- ### 任务 1:数据库迁移 — 建表 + 加列 **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` - 修改:`cfc-backend/src/main/resources/schema.sql` - [ ] **步骤 1:在 DatabaseInitializer.runMigrations() 末尾添加迁移 168** ```java // 迁移168: 创建 inventory_transactions 表(库存流水) try { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS inventory_transactions (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "product_id BIGINT NOT NULL COMMENT '商品ID', " + "sku_id BIGINT COMMENT 'SKU ID,NULL=商品级库存', " + "type VARCHAR(20) NOT NULL COMMENT '类型: inbound/outbound/adjustment/counting/refund', " + "direction TINYINT NOT NULL COMMENT '方向: 1=入库 -1=出库', " + "quantity INT NOT NULL COMMENT '变动数量(正数)', " + "before_stock INT NOT NULL COMMENT '变动前库存', " + "after_stock INT NOT NULL COMMENT '变动后库存', " + "reference_type VARCHAR(32) COMMENT '关联类型: inbound_order/order/counting/adjustment', " + "reference_id BIGINT COMMENT '关联ID', " + "operator_id BIGINT COMMENT '操作人ID,关联users.id', " + "remark VARCHAR(500) COMMENT '备注', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "INDEX idx_product (product_id), " + "INDEX idx_sku (sku_id), " + "INDEX idx_type (type), " + "INDEX idx_reference (reference_type, reference_id), " + "INDEX idx_created_at (created_at)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存流水表'"); log.info("已创建 inventory_transactions 表"); } catch (Exception e) { log.warn("创建 inventory_transactions 表可能已存在: {}", e.getMessage()); } ``` - [ ] **步骤 2:添加迁移 169 — 创建入库单表** ```java // 迁移169: 创建 inventory_inbound_orders 表(入库单) try { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS inventory_inbound_orders (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "order_no VARCHAR(64) NOT NULL UNIQUE COMMENT '入库单号', " + "supplier_id BIGINT COMMENT '供应商ID,关联ecom_supplier.id', " + "supplier_name VARCHAR(200) COMMENT '供应商名称', " + "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/completed/cancelled', " + "total_items INT NOT NULL DEFAULT 0 COMMENT '商品总数', " + "operator_id BIGINT COMMENT '操作人ID,关联users.id', " + "remark VARCHAR(500) COMMENT '备注', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " + "INDEX idx_status (status), " + "INDEX idx_operator (operator_id)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入库单表'"); log.info("已创建 inventory_inbound_orders 表"); } catch (Exception e) { log.warn("创建 inventory_inbound_orders 表可能已存在: {}", e.getMessage()); } ``` - [ ] **步骤 3:添加迁移 170 — 创建入库单明细表 + 盘点表 + 套餐组成表** ```java // 迁移170: 创建 inventory_inbound_items 表(入库单明细) try { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS inventory_inbound_items (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "inbound_order_id BIGINT NOT NULL COMMENT '入库单ID', " + "product_id BIGINT NOT NULL COMMENT '商品ID', " + "sku_id BIGINT COMMENT 'SKU ID', " + "quantity INT NOT NULL COMMENT '入库数量', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "INDEX idx_inbound_order (inbound_order_id), " + "INDEX idx_product (product_id)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='入库单明细表'"); log.info("已创建 inventory_inbound_items 表"); } catch (Exception e) { log.warn("创建 inventory_inbound_items 表可能已存在: {}", e.getMessage()); } // 迁移171: 创建 inventory_counting_records 表(盘点记录) try { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS inventory_counting_records (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "product_id BIGINT NOT NULL COMMENT '商品ID', " + "sku_id BIGINT COMMENT 'SKU ID', " + "expected_stock INT NOT NULL COMMENT '系统库存', " + "actual_stock INT NOT NULL COMMENT '实际库存', " + "difference INT NOT NULL COMMENT '差异(actual - expected)', " + "status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT '状态: pending/confirmed', " + "operator_id BIGINT COMMENT '操作人ID', " + "remark VARCHAR(500) COMMENT '备注', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "INDEX idx_product (product_id), " + "INDEX idx_status (status)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='盘点记录表'"); log.info("已创建 inventory_counting_records 表"); } catch (Exception e) { log.warn("创建 inventory_counting_records 表可能已存在: {}", e.getMessage()); } // 迁移172: 创建 product_bundle_items 表(套餐商品组成) try { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS product_bundle_items (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "bundle_product_id BIGINT NOT NULL COMMENT '套餐商品ID,关联products.id', " + "child_product_id BIGINT NOT NULL COMMENT '子商品ID,关联products.id', " + "child_sku_id BIGINT COMMENT '子商品SKU ID,NULL=商品级', " + "quantity INT NOT NULL DEFAULT 1 COMMENT '子商品数量', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "INDEX idx_bundle (bundle_product_id), " + "INDEX idx_child (child_product_id)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='套餐商品组成表'"); log.info("已创建 product_bundle_items 表"); } catch (Exception e) { log.warn("创建 product_bundle_items 表可能已存在: {}", e.getMessage()); } ``` - [ ] **步骤 4:添加迁移 173 — 商品表加库存预警列** ```java // 迁移173: products表添加min_stock_alert列(库存预警下限) ensureColumn("products", "min_stock_alert", "INT DEFAULT 10 COMMENT '库存预警下限'"); // 迁移173: product_skus表添加min_stock_alert列(库存预警下限) ensureColumn("product_skus", "min_stock_alert", "INT DEFAULT 10 COMMENT '库存预警下限'"); ``` - [ ] **步骤 5:同步 schema.sql** 在 schema.sql 末尾追加 5 张新表的 `CREATE TABLE IF NOT EXISTS` 语句,以及 products/product_skus 的 `min_stock_alert` 列定义。 - [ ] **步骤 6:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 7:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java git add cfc-backend/src/main/resources/schema.sql git commit -m "feat: 库存管理 - 数据库迁移(5张新表+预警列)" ``` --- ### 任务 2:后端实体类 **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/InventoryTransaction.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/InventoryInboundOrder.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/InventoryInboundItem.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/InventoryCountingRecord.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/entity/ProductBundleItem.java` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/Product.java` - 修改:`cfc-backend/src/main/java/com/etotem/cfc/entity/ProductSku.java` - [ ] **步骤 1:创建 InventoryTransaction.java** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("inventory_transactions") public class InventoryTransaction implements Serializable { @TableId(type = IdType.AUTO) private Long id; private Long productId; private Long skuId; private String type; // inbound/outbound/adjustment/counting/refund private Integer direction; // 1=入库 -1=出库 private Integer quantity; private Integer beforeStock; private Integer afterStock; private String referenceType; // inbound_order/order/counting/adjustment private Long referenceId; private Long operatorId; private String remark; private Date createdAt; } ``` - [ ] **步骤 2:创建 InventoryInboundOrder.java** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("inventory_inbound_orders") public class InventoryInboundOrder implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String orderNo; private Long supplierId; private String supplierName; private String status; // pending/completed/cancelled private Integer totalItems; private Long operatorId; private String remark; private Date createdAt; private Date updatedAt; } ``` - [ ] **步骤 3:创建 InventoryInboundItem.java** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("inventory_inbound_items") public class InventoryInboundItem implements Serializable { @TableId(type = IdType.AUTO) private Long id; private Long inboundOrderId; private Long productId; private Long skuId; private Integer quantity; private Date createdAt; } ``` - [ ] **步骤 4:创建 InventoryCountingRecord.java** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("inventory_counting_records") public class InventoryCountingRecord implements Serializable { @TableId(type = IdType.AUTO) private Long id; private Long productId; private Long skuId; private Integer expectedStock; private Integer actualStock; private Integer difference; private String status; // pending/confirmed private Long operatorId; private String remark; private Date createdAt; } ``` - [ ] **步骤 5:创建 ProductBundleItem.java** ```java package com.etotem.cfc.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.io.Serializable; import java.util.Date; @Data @TableName("product_bundle_items") public class ProductBundleItem implements Serializable { @TableId(type = IdType.AUTO) private Long id; private Long bundleProductId; private Long childProductId; private Long childSkuId; private Integer quantity; private Date createdAt; } ``` - [ ] **步骤 6:更新 Product.java 和 ProductSku.java** Product.java 添加字段: ```java private Integer minStockAlert; // 库存预警下限 ``` 并添加 getter 方法: ```java public Integer getMinStockAlert() { return minStockAlert; } ``` ProductSku.java 添加字段: ```java private Integer minStockAlert; // 库存预警下限 ``` - [ ] **步骤 7:编译验证** 运行:`cd cfc-backend && mvn clean compile` 预期:BUILD SUCCESS - [ ] **步骤 8:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/entity/ git commit -m "feat: 库存管理 - 实体类(5个新实体 + Product/ProductSku扩展)" ``` --- ### 任务 3:后端 Mapper 接口 **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/InventoryTransactionMapper.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/InventoryInboundOrderMapper.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/InventoryInboundItemMapper.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/InventoryCountingRecordMapper.java` - 创建:`cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductBundleItemMapper.java` - [ ] **步骤 1:创建 5 个 Mapper 接口** 每个 Mapper 继承 `BaseMapper`,使用 `@Mapper` 注解: ```java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.InventoryTransaction; import org.apache.ibatis.annotations.Mapper; @Mapper public interface InventoryTransactionMapper extends BaseMapper {} ``` 对其他 4 个 Mapper 同样操作,仅替换实体类型和文件名。 - [ ] **步骤 2:编译验证** ```bash cd cfc-backend && mvn clean compile ``` - [ ] **步骤 3:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/mapper/Inventory*.java git add cfc-backend/src/main/java/com/etotem/cfc/mapper/ProductBundleItemMapper.java git commit -m "feat: 库存管理 - Mapper接口" ``` --- ### 任务 4:InventoryService — 核心服务 **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/service/InventoryService.java` - [ ] **步骤 1:创建 InventoryService.java** ```java 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.*; import java.util.stream.Collectors; @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> createInboundOrder(Long supplierId, String supplierName, List> 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 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 result = new HashMap<>(); result.put("id", order.getId()); result.put("orderNo", orderNo); return Result.success(result); } @Transactional public Result 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 items = inboundItemMapper.selectList( new LambdaQueryWrapper().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 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> listInboundOrders(int page, int size, String status, String keyword) { Page pageParam = new Page<>(page, size); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .orderByDesc(InventoryInboundOrder::getCreatedAt); if (status != null && !status.isEmpty()) { wrapper.eq(InventoryInboundOrder::getStatus, status); } Page result = inboundOrderMapper.selectPage(pageParam, wrapper); Map 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> detailInboundOrder(Long id) { InventoryInboundOrder order = inboundOrderMapper.selectById(id); if (order == null) return Result.error("入库单不存在"); List items = inboundItemMapper.selectList( new LambdaQueryWrapper().eq(InventoryInboundItem::getInboundOrderId, id)); Map data = new HashMap<>(); data.put("order", order); data.put("items", items); return Result.success(data); } // ========== 库存流水 ========== public Result> listTransactions(int page, int size, Long productId, Long skuId, String type, String startDate, String endDate) { Page pageParam = new Page<>(page, size); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .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 result = transactionMapper.selectPage(pageParam, wrapper); Map 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 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 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> listCountingRecords(int page, int size, String status) { Page pageParam = new Page<>(page, size); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .orderByDesc(InventoryCountingRecord::getCreatedAt); if (status != null && !status.isEmpty()) { wrapper.eq(InventoryCountingRecord::getStatus, status); } Page result = countingRecordMapper.selectPage(pageParam, wrapper); Map 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>> listStockAlerts() { List> alerts = new ArrayList<>(); // 查询商品级预警 List products = productMapper.selectList( new LambdaQueryWrapper() .eq(Product::getProductType, "physical") .apply("min_stock_alert IS NOT NULL AND stock < min_stock_alert")); for (Product p : products) { Map 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); } // 查询SKU级预警 List skus = productSkuMapper.selectList( new LambdaQueryWrapper() .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 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> getProductStock(Long productId) { Product product = productMapper.selectById(productId); if (product == null) return Result.error("商品不存在"); Map 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()); // 查询SKU库存 List skus = productSkuMapper.findByProductIdAll(productId); data.put("skus", skus); // 最近10条流水 List recentTx = transactionMapper.selectList( new LambdaQueryWrapper() .eq(InventoryTransaction::getProductId, productId) .orderByDesc(InventoryTransaction::getCreatedAt) .last("LIMIT 10")); data.put("recentTransactions", recentTx); // 如果是套餐商品,查询子商品 if ("bundle".equals(product.getProductType())) { List bundleItems = bundleItemMapper.selectList( new LambdaQueryWrapper() .eq(ProductBundleItem::getBundleProductId, productId)); List> childProducts = new ArrayList<>(); for (ProductBundleItem bi : bundleItems) { Product child = productMapper.selectById(bi.getChildProductId()); if (child != null) { Map 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 saveBundleItems(Long bundleProductId, List> items) { // 删除旧配置 bundleItemMapper.delete(new LambdaQueryWrapper() .eq(ProductBundleItem::getBundleProductId, bundleProductId)); // 插入新配置 for (Map 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> listBundleItems(Long bundleProductId) { List items = bundleItemMapper.selectList( new LambdaQueryWrapper() .eq(ProductBundleItem::getBundleProductId, bundleProductId)); return Result.success(items); } // ========== 套餐库存检查 ========== public Result checkBundleStock(Long bundleProductId, Integer quantity) { List items = bundleItemMapper.selectList( new LambdaQueryWrapper() .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("库存充足"); } } ``` - [ ] **步骤 2:编译验证** ```bash cd cfc-backend && mvn clean compile ``` - [ ] **步骤 3:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/InventoryService.java git commit -m "feat: 库存管理 - InventoryService(入库单/流水/盘点/预警/套餐)" ``` --- ### 任务 5:AdminInventoryController — 管理端 API **文件:** - 创建:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminInventoryController.java` - [ ] **步骤 1:创建 AdminInventoryController.java** ```java package com.etotem.cfc.controller.admin; import com.etotem.cfc.common.Result; import com.etotem.cfc.entity.ProductBundleItem; import com.etotem.cfc.service.InventoryService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.util.List; import java.util.Map; @Tag(name = "管理端-库存管理") @RestController @RequestMapping("/api/admin/inventory") public class AdminInventoryController { @Resource private InventoryService inventoryService; // ========== 入库单 ========== @Operation(summary = "创建入库单") @PostMapping("/inbound/create") public Result> createInbound(@RequestBody Map params, @RequestAttribute("userId") Long adminId) { Long supplierId = params.get("supplierId") != null ? Long.valueOf(params.get("supplierId").toString()) : null; String supplierName = (String) params.get("supplierName"); String remark = (String) params.get("remark"); @SuppressWarnings("unchecked") List> items = (List>) params.get("items"); return inventoryService.createInboundOrder(supplierId, supplierName, items, adminId, remark); } @Operation(summary = "入库单列表") @PostMapping("/inbound/list") public Result> listInbound(@RequestBody Map 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 status = (String) params.get("status"); String keyword = (String) params.get("keyword"); return inventoryService.listInboundOrders(page, size, status, keyword); } @Operation(summary = "入库单详情") @PostMapping("/inbound/detail") public Result> detailInbound(@RequestBody Map params) { Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null; if (id == null) return Result.error("id不能为空"); return inventoryService.detailInboundOrder(id); } @Operation(summary = "确认入库") @PostMapping("/inbound/confirm") public Result confirmInbound(@RequestBody Map params, @RequestAttribute("userId") Long adminId) { Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null; if (id == null) return Result.error("id不能为空"); return inventoryService.confirmInbound(id, adminId); } @Operation(summary = "取消入库单") @PostMapping("/inbound/cancel") public Result cancelInbound(@RequestBody Map params) { Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null; if (id == null) return Result.error("id不能为空"); return inventoryService.cancelInbound(id); } // ========== 库存流水 ========== @Operation(summary = "库存流水列表") @PostMapping("/transactions/list") public Result> listTransactions(@RequestBody Map 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; 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; String type = (String) params.get("type"); String startDate = (String) params.get("startDate"); String endDate = (String) params.get("endDate"); return inventoryService.listTransactions(page, size, productId, skuId, type, startDate, endDate); } // ========== 盘点 ========== @Operation(summary = "创建盘点") @PostMapping("/counting/create") public Result createCounting(@RequestBody Map 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 actualStock = params.get("actualStock") != null ? Integer.valueOf(params.get("actualStock").toString()) : null; String remark = (String) params.get("remark"); if (productId == null) return Result.error("productId不能为空"); if (actualStock == null) return Result.error("actualStock不能为空"); return inventoryService.createCounting(productId, skuId, actualStock, adminId, remark); } @Operation(summary = "确认盘点") @PostMapping("/counting/confirm") public Result confirmCounting(@RequestBody Map params, @RequestAttribute("userId") Long adminId) { Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null; if (id == null) return Result.error("id不能为空"); return inventoryService.confirmCounting(id, adminId); } @Operation(summary = "盘点列表") @PostMapping("/counting/list") public Result> listCounting(@RequestBody Map 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 status = (String) params.get("status"); return inventoryService.listCountingRecords(page, size, status); } // ========== 库存预警 ========== @Operation(summary = "库存预警列表") @PostMapping("/alert/list") public Result>> listAlerts() { return inventoryService.listStockAlerts(); } // ========== 商品库存详情 ========== @Operation(summary = "商品库存详情") @PostMapping("/product/stock") public Result> productStock(@RequestBody Map params) { Long productId = params.get("productId") != null ? Long.valueOf(params.get("productId").toString()) : null; if (productId == null) return Result.error("productId不能为空"); return inventoryService.getProductStock(productId); } // ========== 套餐商品 ========== @Operation(summary = "配置套餐商品组成") @PostMapping("/bundle/items") public Result saveBundleItems(@RequestBody Map params) { Long bundleProductId = params.get("bundleProductId") != null ? Long.valueOf(params.get("bundleProductId").toString()) : null; if (bundleProductId == null) return Result.error("bundleProductId不能为空"); @SuppressWarnings("unchecked") List> items = (List>) params.get("items"); return inventoryService.saveBundleItems(bundleProductId, items); } @Operation(summary = "查询套餐商品组成") @PostMapping("/bundle/items/list") public Result> listBundleItems(@RequestBody Map params) { Long bundleProductId = params.get("bundleProductId") != null ? Long.valueOf(params.get("bundleProductId").toString()) : null; if (bundleProductId == null) return Result.error("bundleProductId不能为空"); return inventoryService.listBundleItems(bundleProductId); } @Operation(summary = "检查套餐库存") @PostMapping("/bundle/check-stock") public Result checkBundleStock(@RequestBody Map params) { Long bundleProductId = params.get("bundleProductId") != null ? Long.valueOf(params.get("bundleProductId").toString()) : null; Integer quantity = params.get("quantity") != null ? Integer.valueOf(params.get("quantity").toString()) : 1; if (bundleProductId == null) return Result.error("bundleProductId不能为空"); return inventoryService.checkBundleStock(bundleProductId, quantity); } } ``` - [ ] **步骤 2:编译验证** ```bash cd cfc-backend && mvn clean compile ``` - [ ] **步骤 3:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminInventoryController.java git commit -m "feat: 库存管理 - AdminInventoryController" ``` --- ### 任务 6:订单集成 — 修改 ProductOrderService **文件:** - 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java` - [ ] **步骤 1:注入 InventoryService** 在 ProductOrderService 的 `@Resource` 区域添加: ```java @Resource private InventoryService inventoryService; ``` - [ ] **步骤 2:修改 handlePaymentSuccess() — 支付成功后记录出库流水** 在 `handlePaymentSuccess()` 方法中,找到 `productService.decreaseStock(...)` 调用之后,添加流水记录。同时处理 bundle 商品穿透扣减。 将原本的库存扣减逻辑: ```java // 仅对实物商品扣减库存 Product paidProduct = productMapper.selectById(order.getProductId()); if (paidProduct != null && "physical".equals(paidProduct.getProductType())) { boolean stockOk = productService.decreaseStock(order.getProductId(), order.getQuantity()); if (!stockOk) { log.error("库存扣减失败: orderNo={}, productId={}, quantity={}", orderNo, order.getProductId(), order.getQuantity()); order.setRemark("库存扣减失败"); order.setUpdatedAt(new Date()); orderMapper.updateById(order); throw new RuntimeException("库存不足"); } } ``` 替换为: ```java // 仅对实物商品扣减库存 Product paidProduct = productMapper.selectById(order.getProductId()); if (paidProduct != null && "physical".equals(paidProduct.getProductType())) { boolean stockOk = productService.decreaseStock(order.getProductId(), order.getQuantity()); if (!stockOk) { log.error("库存扣减失败: orderNo={}, productId={}, quantity={}", orderNo, order.getProductId(), order.getQuantity()); order.setRemark("库存扣减失败"); order.setUpdatedAt(new Date()); orderMapper.updateById(order); throw new RuntimeException("库存不足"); } // 记录出库流水 inventoryService.recordOutbound(order.getProductId(), null, order.getQuantity(), "order", order.getId(), null, "订单支付:" + orderNo); } else if (paidProduct != null && "bundle".equals(paidProduct.getProductType())) { // 套餐商品:穿透到子商品扣减库存 List bundleItems = bundleItemMapper.selectList( new LambdaQueryWrapper() .eq(ProductBundleItem::getBundleProductId, paidProduct.getId())); for (ProductBundleItem bi : bundleItems) { int need = bi.getQuantity() * order.getQuantity(); if (bi.getChildSkuId() != null) { boolean skuOk = productSkuService.decreaseStock(bi.getChildSkuId(), need); if (!skuOk) { throw new RuntimeException("套餐子商品库存不足: bundleItemId=" + bi.getId()); } inventoryService.recordOutbound(bi.getChildProductId(), bi.getChildSkuId(), need, "order", order.getId(), null, "套餐出库:" + orderNo); } else { boolean stockOk = productService.decreaseStock(bi.getChildProductId(), need); if (!stockOk) { throw new RuntimeException("套餐子商品库存不足: bundleItemId=" + bi.getId()); } inventoryService.recordOutbound(bi.getChildProductId(), null, need, "order", order.getId(), null, "套餐出库:" + orderNo); } } } ``` 注意:需要添加 `ProductBundleItemMapper` 的注入: ```java @Resource private ProductBundleItemMapper bundleItemMapper; ``` - [ ] **步骤 3:修改 cancel() — 取消订单后记录退款流水** 在 `cancel()` 方法中,找到 `productService.increaseStock(...)` 调用之后,添加: ```java // 记录退款入库流水 inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(), "order", order.getId(), null, "取消订单:" + orderNo); ``` - [ ] **步骤 4:修改 approveRefund() — 退款后记录退款流水** 在 `approveRefund()` 方法中,找到 `productService.increaseStock(...)` 调用之后,添加: ```java inventoryService.recordRefundInbound(order.getProductId(), null, order.getQuantity(), "order", order.getId(), adminId, "退款:" + orderNo); ``` - [ ] **步骤 5:修改 create() — 库存检查时支持 bundle** 在 `create()` 方法中,找到库存检查逻辑: ```java if ("physical".equals(product.getProductType()) && product.getStock() != null && product.getStock() < dto.getQuantity()) { return Result.error("库存不足"); } ``` 在之前添加 bundle 检查: ```java // 套餐商品:检查子商品库存 if ("bundle".equals(product.getProductType())) { Result bundleCheck = inventoryService.checkBundleStock(product.getId(), dto.getQuantity()); if (bundleCheck.getCode() != 200) { return Result.error(bundleCheck.getMessage()); } } ``` 类似地,在 `createMultiItem()` 方法中做同样的处理。 - [ ] **步骤 6:编译验证** ```bash cd cfc-backend && mvn clean compile ``` - [ ] **步骤 7:Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java git commit -m "feat: 库存管理 - 订单集成(支付/取消/退款库存流水 + bundle穿透)" ``` --- ### 任务 7:Web Admin — API 调用 **文件:** - 修改:`cfc-web/src/api/admin.js` - [ ] **步骤 1:在 admin.js 末尾添加库存管理 API** ```javascript // ========== 库存管理 API ========== export function createInboundOrder(data) { return request({ url: '/api/admin/inventory/inbound/create', method: 'post', data }) } export function getInboundOrderList(params) { return request({ url: '/api/admin/inventory/inbound/list', method: 'post', data: params }) } export function getInboundOrderDetail(id) { return request({ url: '/api/admin/inventory/inbound/detail', method: 'post', data: { id } }) } export function confirmInbound(id) { return request({ url: '/api/admin/inventory/inbound/confirm', method: 'post', data: { id } }) } export function cancelInbound(id) { return request({ url: '/api/admin/inventory/inbound/cancel', method: 'post', data: { id } }) } export function getInventoryTransactions(params) { return request({ url: '/api/admin/inventory/transactions/list', method: 'post', data: params }) } export function createCountingRecord(data) { return request({ url: '/api/admin/inventory/counting/create', method: 'post', data }) } export function confirmCounting(id) { return request({ url: '/api/admin/inventory/counting/confirm', method: 'post', data: { id } }) } export function getCountingList(params) { return request({ url: '/api/admin/inventory/counting/list', method: 'post', data: params }) } export function getStockAlerts() { return request({ url: '/api/admin/inventory/alert/list', method: 'post' }) } export function getProductStock(productId) { return request({ url: '/api/admin/inventory/product/stock', method: 'post', data: { productId } }) } export function saveBundleItems(bundleProductId, items) { return request({ url: '/api/admin/inventory/bundle/items', method: 'post', data: { bundleProductId, items } }) } export function getBundleItems(bundleProductId) { return request({ url: '/api/admin/inventory/bundle/items/list', method: 'post', data: { bundleProductId } }) } export function checkBundleStock(bundleProductId, quantity) { return request({ url: '/api/admin/inventory/bundle/check-stock', method: 'post', data: { bundleProductId, quantity } }) } ``` - [ ] **步骤 2:Commit** ```bash git add cfc-web/src/api/admin.js git commit -m "feat: 库存管理 - 管理端API调用" ``` --- ### 任务 8:InventoryManage.vue — 库存管理主页 **文件:** - 创建:`cfc-web/src/views/admin/InventoryManage.vue` - [ ] **步骤 1:创建 InventoryManage.vue(4 标签页:库存流水 / 入库单 / 库存预警 / 盘点记录)** ```vue ``` - [ ] **步骤 2:Commit** ```bash git add cfc-web/src/views/admin/InventoryManage.vue git commit -m "feat: 库存管理 - InventoryManage.vue(4标签页)" ``` --- ### 任务 9:InventoryInboundCreate.vue — 创建入库单 **文件:** - 创建:`cfc-web/src/views/admin/InventoryInboundCreate.vue` - [ ] **步骤 1:创建 InventoryInboundCreate.vue** ```vue ``` - [ ] **步骤 2:Commit** ```bash git add cfc-web/src/views/admin/InventoryInboundCreate.vue git commit -m "feat: 库存管理 - InventoryInboundCreate.vue" ``` --- ### 任务 10:ProductManage.vue — 增加库存按钮 **文件:** - 修改:`cfc-web/src/views/admin/ProductManage.vue` - [ ] **步骤 1:在操作列增加「库存」按钮** 在 `` 之前添加: ```html 库存 ``` - [ ] **步骤 2:添加库存弹窗对话框** 在 `` 之前添加: ```html
{{ inventoryData.productName }} {{ inventoryData.stock }} {{ inventoryData.minStockAlert ?? 10 }}

SKU 库存

无SKU数据

最近 10 条流水

暂无流水记录
关闭 进入库存管理
``` - [ ] **步骤 3:添加 data 和方法** 在 data() 中添加: ```javascript inventoryDialogVisible: false, inventoryLoading: false, inventoryData: null, ``` 在 methods 中添加: ```javascript async showInventoryDialog(row) { this.inventoryDialogVisible = true this.inventoryLoading = true try { const { getProductStock } = await import('@/api/admin') const res = await getProductStock(row.id) this.inventoryData = res.data } catch (e) { this.$message.error('加载库存信息失败') } finally { this.inventoryLoading = false } } ``` - [ ] **步骤 4:Commit** ```bash git add cfc-web/src/views/admin/ProductManage.vue git commit -m "feat: 库存管理 - ProductManage.vue增加库存按钮+弹窗" ``` --- ### 任务 11:路由 + 侧边栏 **文件:** - 修改:`cfc-web/src/router/index.js` - [ ] **步骤 1:添加库存管理路由** 在 `product-manage` 路由附近(约第 224 行后)添加: ```javascript { path: 'inventory', name: 'InventoryManage', component: () => import('@/views/admin/InventoryManage.vue'), meta: { title: '库存管理', perm: 'commerce:products' } }, { path: 'inventory/inbound-create', name: 'InventoryInboundCreate', component: () => import('@/views/admin/InventoryInboundCreate.vue'), meta: { title: '创建入库单', perm: 'commerce:products' } }, ``` - [ ] **步骤 2:添加侧边栏菜单** 在 `Layout.vue` 中找到商品管理的菜单项,添加「库存管理」子菜单。 - [ ] **步骤 3:Commit** ```bash git add cfc-web/src/router/index.js git commit -m "feat: 库存管理 - 路由+侧边栏" ``` --- ### 任务 12:编译验证 — 全量编译 - [ ] **步骤 1:后端编译** ```bash cd cfc-backend && mvn clean compile ``` 预期:BUILD SUCCESS - [ ] **步骤 2:前端编译** ```bash cd cfc-web && npm run build ``` 预期:无报错 --- ## 自检 - [ ] 规格覆盖度:所有 5 张表已实现,所有 API 端点已实现,管理端 4 标签页 + 入库单创建 + 商品列表库存按钮 - [ ] 无占位符 - [ ] 类型一致性:所有实体字段名与数据库列名一致,所有 API 参数名与 controller 代码一致