Przeglądaj źródła

feat: 供应商管理(CRUD + 侧边栏 + 入库单选择供应商)

Xiaogang Liao 3 miesięcy temu
rodzic
commit
862a8d41f2

+ 55 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/EcomSupplierController.java

@@ -0,0 +1,55 @@
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.EcomSupplier;
+import com.etotem.cfc.service.EcomSupplierService;
+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/supplier")
+public class EcomSupplierController {
+
+    @Resource
+    private EcomSupplierService supplierService;
+
+    @Operation(summary = "供应商列表")
+    @PostMapping("/list")
+    public Result<Map<String, Object>> list(@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 keyword = (String) params.get("keyword");
+        return supplierService.list(page, size, keyword);
+    }
+
+    @Operation(summary = "所有供应商(下拉选择用)")
+    @PostMapping("/all")
+    public Result<List<EcomSupplier>> all() {
+        return supplierService.all();
+    }
+
+    @Operation(summary = "创建供应商")
+    @PostMapping("/save")
+    public Result<String> save(@RequestBody EcomSupplier supplier) {
+        return supplierService.save(supplier);
+    }
+
+    @Operation(summary = "更新供应商")
+    @PostMapping("/update")
+    public Result<String> update(@RequestBody EcomSupplier supplier) {
+        return supplierService.update(supplier);
+    }
+
+    @Operation(summary = "删除供应商")
+    @PostMapping("/delete")
+    public Result<String> delete(@RequestBody Map<String, Object> params) {
+        Long id = params.get("id") != null ? Long.valueOf(params.get("id").toString()) : null;
+        return supplierService.delete(id);
+    }
+}

+ 73 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/EcomSupplierService.java

@@ -0,0 +1,73 @@
+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.EcomSupplier;
+import com.etotem.cfc.mapper.EcomSupplierMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class EcomSupplierService {
+
+    @Resource
+    private EcomSupplierMapper supplierMapper;
+
+    public Result<Map<String, Object>> list(int page, int size, String keyword) {
+        Page<EcomSupplier> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<EcomSupplier> wrapper = new LambdaQueryWrapper<EcomSupplier>()
+                .orderByDesc(EcomSupplier::getCreatedAt);
+        if (keyword != null && !keyword.trim().isEmpty()) {
+            wrapper.like(EcomSupplier::getName, keyword.trim());
+        }
+        Page<EcomSupplier> result = supplierMapper.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<EcomSupplier>> all() {
+        List<EcomSupplier> list = supplierMapper.selectList(
+                new LambdaQueryWrapper<EcomSupplier>()
+                        .eq(EcomSupplier::getStatus, 1)
+                        .orderByAsc(EcomSupplier::getName));
+        return Result.success(list);
+    }
+
+    public Result<String> save(EcomSupplier supplier) {
+        if (supplier.getName() == null || supplier.getName().trim().isEmpty()) {
+            return Result.error("供应商名称不能为空");
+        }
+        supplier.setCreatedAt(new Date());
+        supplier.setUpdatedAt(new Date());
+        if (supplier.getStatus() == null) {
+            supplier.setStatus(1);
+        }
+        supplierMapper.insert(supplier);
+        return Result.success("创建成功");
+    }
+
+    public Result<String> update(EcomSupplier supplier) {
+        if (supplier.getId() == null) {
+            return Result.error("id不能为空");
+        }
+        supplier.setUpdatedAt(new Date());
+        supplierMapper.updateById(supplier);
+        return Result.success("更新成功");
+    }
+
+    public Result<String> delete(Long id) {
+        if (id == null) return Result.error("id不能为空");
+        supplierMapper.deleteById(id);
+        return Result.success("删除成功");
+    }
+}

BIN
cfc-langgraph/.venv-packages.tar.gz


+ 7 - 0
cfc-web/src/api/admin.js

@@ -1732,6 +1732,13 @@ export function deleteEcomSupplier(id) {
   })
 }
 
+export function getAllSuppliers() {
+  return request({
+    url: '/api/admin/supplier/all',
+    method: 'post'
+  })
+}
+
 // ===== 家庭收益管理 =====
 export function getFamilyEarningsList() {
   return request({

+ 1 - 0
cfc-web/src/views/Layout.vue

@@ -195,6 +195,7 @@ export default {
           children: [
             { path: '/product-manage', label: '商品管理', icon: 'el-icon-s-goods', perm: 'commerce:products' },
             { path: '/inventory', label: '库存管理', icon: 'el-icon-document', perm: 'commerce:products' },
+            { path: '/ecom-supplier', label: '供应商管理', icon: 'el-icon-s-shop', perm: 'commerce:products' },
             { path: '/order-manage', label: '订单管理', icon: 'el-icon-s-order', perm: 'commerce:orders' },
             { path: '/pending-refund', label: '待退款管理', icon: 'el-icon-warning', perm: 'commerce:orders' },
             { path: '/product-profit-rate', label: '产品利润率', icon: 'el-icon-data-line', perm: 'commerce:profit' },

+ 15 - 2
cfc-web/src/views/admin/InventoryInboundCreate.vue

@@ -16,7 +16,9 @@
         </el-form-item>
 
         <el-form-item label="供应商">
-          <el-input v-model="form.supplierName" placeholder="供应商名称" style="width:300px;" />
+          <el-select v-model="form.supplierId" filterable placeholder="搜索选择供应商" style="width:300px;" @change="(v) => { const s = supplierOptions.find(x => x.id === v); form.supplierName = s ? s.name : ''; }">
+            <el-option v-for="s in supplierOptions" :key="s.id" :label="s.name" :value="s.id" />
+          </el-select>
         </el-form-item>
 
         <el-form-item label="总金额(元)">
@@ -50,16 +52,18 @@
 </template>
 
 <script>
-import { createInboundOrder, getProductList, getProductSkuList } from '@/api/admin'
+import { createInboundOrder, getProductList, getProductSkuList, getAllSuppliers } from '@/api/admin'
 
 export default {
   name: 'InventoryInboundCreate',
   data() {
     return {
       submitting: false,
+      supplierOptions: [],
       form: {
         inboundDate: '',
         operatorName: '',
+        supplierId: '',
         supplierName: '',
         totalAmount: '',
         remark: '',
@@ -67,7 +71,16 @@ export default {
       }
     }
   },
+  mounted() {
+    this.loadSuppliers()
+  },
   methods: {
+    async loadSuppliers() {
+      try {
+        const res = await getAllSuppliers()
+        this.supplierOptions = res.data || []
+      } catch (e) { /* ignore */ }
+    },
     addItem() {
       this.form.items.push({ productId: '', skuId: '', quantity: 1, productOptions: [], skuOptions: [] })
     },

+ 1964 - 0
docs/superpowers/plans/2026-08-05-inventory-management.md

@@ -0,0 +1,1964 @@
+# 库存管理 实现计划
+
+> **面向 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<T>`,使用 `@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<InventoryTransaction> {}
+```
+
+对其他 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<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);
+        }
+
+        // 查询SKU级预警
+        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());
+
+        // 查询SKU库存
+        List<ProductSku> skus = productSkuMapper.findByProductIdAll(productId);
+        data.put("skus", skus);
+
+        // 最近10条流水
+        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("库存充足");
+    }
+}
+```
+
+- [ ] **步骤 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<Map<String, Object>> createInbound(@RequestBody Map<String, Object> 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<Map<String, Object>> items = (List<Map<String, Object>>) params.get("items");
+        return inventoryService.createInboundOrder(supplierId, supplierName, items, adminId, remark);
+    }
+
+    @Operation(summary = "入库单列表")
+    @PostMapping("/inbound/list")
+    public Result<Map<String, Object>> listInbound(@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 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<Map<String, Object>> detailInbound(@RequestBody Map<String, Object> 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<String> confirmInbound(@RequestBody Map<String, Object> 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<String> cancelInbound(@RequestBody Map<String, Object> 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<Map<String, Object>> listTransactions(@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;
+        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<String> createCounting(@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 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<String> confirmCounting(@RequestBody Map<String, Object> 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<Map<String, Object>> listCounting(@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 status = (String) params.get("status");
+        return inventoryService.listCountingRecords(page, size, status);
+    }
+
+    // ========== 库存预警 ==========
+
+    @Operation(summary = "库存预警列表")
+    @PostMapping("/alert/list")
+    public Result<List<Map<String, Object>>> listAlerts() {
+        return inventoryService.listStockAlerts();
+    }
+
+    // ========== 商品库存详情 ==========
+
+    @Operation(summary = "商品库存详情")
+    @PostMapping("/product/stock")
+    public Result<Map<String, Object>> productStock(@RequestBody Map<String, Object> 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<String> saveBundleItems(@RequestBody Map<String, Object> params) {
+        Long bundleProductId = params.get("bundleProductId") != null
+                ? Long.valueOf(params.get("bundleProductId").toString()) : null;
+        if (bundleProductId == null) return Result.error("bundleProductId不能为空");
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> items = (List<Map<String, Object>>) params.get("items");
+        return inventoryService.saveBundleItems(bundleProductId, items);
+    }
+
+    @Operation(summary = "查询套餐商品组成")
+    @PostMapping("/bundle/items/list")
+    public Result<List<ProductBundleItem>> listBundleItems(@RequestBody Map<String, Object> 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<String> checkBundleStock(@RequestBody Map<String, Object> 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<ProductBundleItem> bundleItems = bundleItemMapper.selectList(
+            new LambdaQueryWrapper<ProductBundleItem>()
+                    .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<String> 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
+<template>
+  <div class="inventory-manage admin-page">
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">库存管理</span>
+        <el-button type="success" size="mini" icon="el-icon-plus" @click="$router.push('/inventory/inbound-create')">创建入库单</el-button>
+      </div>
+
+      <el-tabs v-model="activeTab">
+        <!-- 库存流水 -->
+        <el-tab-pane label="库存流水" name="transactions">
+          <div class="filter-bar" style="margin-bottom:12px;">
+            <el-input v-model="txFilters.productName" placeholder="商品名称" clearable style="width:160px;margin-right:8px;" />
+            <el-select v-model="txFilters.type" placeholder="类型" clearable style="width:120px;margin-right:8px;">
+              <el-option label="全部" value="" />
+              <el-option label="入库" value="inbound" />
+              <el-option label="出库" value="outbound" />
+              <el-option label="调账" value="adjustment" />
+              <el-option label="盘点" value="counting" />
+              <el-option label="退款入库" value="refund" />
+            </el-select>
+            <el-date-picker v-model="txFilters.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" style="width:260px;margin-right:8px;" />
+            <el-button type="primary" size="mini" @click="loadTransactions">查询</el-button>
+          </div>
+          <el-table :data="txList" v-loading="txLoading" border stripe max-height="500">
+            <el-table-column prop="id" label="ID" width="60" />
+            <el-table-column label="商品" min-width="150">
+              <template slot-scope="{ row }">{{ row.productName || 'ID:' + row.productId }}</template>
+            </el-table-column>
+            <el-table-column label="类型" width="80">
+              <template slot-scope="{ row }">
+                <el-tag :type="row.type === 'inbound' || row.type === 'refund' ? 'success' : row.type === 'outbound' ? 'danger' : 'warning'" size="mini">
+                  {{ {'inbound':'入库','outbound':'出库','adjustment':'调账','counting':'盘点','refund':'退款入库'}[row.type] || row.type }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="数量" width="80">
+              <template slot-scope="{ row }">
+                <span :style="{ color: row.direction === 1 ? '#67c23a' : '#f56c6c' }">
+                  {{ row.direction === 1 ? '+' : '-' }}{{ row.quantity }}
+                </span>
+              </template>
+            </el-table-column>
+            <el-table-column prop="beforeStock" label="变动前" width="70" />
+            <el-table-column prop="afterStock" label="变动后" width="70" />
+            <el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip />
+            <el-table-column prop="createdAt" label="时间" width="160" />
+          </el-table>
+          <el-pagination @size-change="txPageSize=$event;loadTransactions()" @current-change="txPage=$event;loadTransactions()" :current-page="txPage" :page-sizes="[10,20,50]" :page-size="txPageSize" :total="txTotal" layout="total, sizes, prev, pager, next, jumper" class="pagination-wrap" />
+        </el-tab-pane>
+
+        <!-- 入库单 -->
+        <el-tab-pane label="入库单" name="inbound">
+          <el-table :data="inboundList" v-loading="inboundLoading" border stripe max-height="500">
+            <el-table-column prop="orderNo" label="入库单号" width="180" />
+            <el-table-column prop="supplierName" label="供应商" width="150" />
+            <el-table-column prop="totalItems" label="商品项数" width="80" />
+            <el-table-column label="状态" width="90">
+              <template slot-scope="{ row }">
+                <el-tag :type="row.status === 'completed' ? 'success' : row.status === 'cancelled' ? 'info' : 'warning'" size="mini">
+                  {{ {'pending':'待确认','completed':'已完成','cancelled':'已取消'}[row.status] || row.status }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="createdAt" label="创建时间" width="160" />
+            <el-table-column label="操作" width="200" fixed="right">
+              <template slot-scope="{ row }">
+                <el-button size="mini" @click="showInboundDetail(row)">详情</el-button>
+                <el-button v-if="row.status === 'pending'" size="mini" type="success" @click="handleConfirmInbound(row)">确认入库</el-button>
+                <el-button v-if="row.status === 'pending'" size="mini" type="danger" @click="handleCancelInbound(row)">取消</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <el-pagination @size-change="inboundPageSize=$event;loadInboundOrders()" @current-change="inboundPage=$event;loadInboundOrders()" :current-page="inboundPage" :page-sizes="[10,20,50]" :page-size="inboundPageSize" :total="inboundTotal" layout="total, sizes, prev, pager, next, jumper" class="pagination-wrap" />
+        </el-tab-pane>
+
+        <!-- 库存预警 -->
+        <el-tab-pane label="库存预警" name="alerts">
+          <el-button type="primary" size="mini" @click="loadAlerts" style="margin-bottom:12px;">刷新</el-button>
+          <el-table :data="alertList" v-loading="alertLoading" border stripe max-height="500">
+            <el-table-column prop="productName" label="商品名称" min-width="200" />
+            <el-table-column prop="specs" label="SKU规格" width="150">
+              <template slot-scope="{ row }">{{ row.specs || '-' }}</template>
+            </el-table-column>
+            <el-table-column prop="currentStock" label="当前库存" width="80">
+              <template slot-scope="{ row }"><span style="color:#f56c6c;font-weight:bold;">{{ row.currentStock }}</span></template>
+            </el-table-column>
+            <el-table-column prop="minStockAlert" label="预警下限" width="80" />
+          </el-table>
+        </el-tab-pane>
+
+        <!-- 盘点记录 -->
+        <el-tab-pane label="盘点记录" name="counting">
+          <el-button type="primary" size="mini" @click="showCreateCounting = true" style="margin-bottom:12px;">新建盘点</el-button>
+          <el-table :data="countingList" v-loading="countingLoading" border stripe max-height="500">
+            <el-table-column prop="productId" label="商品ID" width="70" />
+            <el-table-column label="系统库存" width="80">
+              <template slot-scope="{ row }">{{ row.expectedStock }}</template>
+            </el-table-column>
+            <el-table-column label="实际库存" width="80">
+              <template slot-scope="{ row }">{{ row.actualStock }}</template>
+            </el-table-column>
+            <el-table-column label="差异" width="80">
+              <template slot-scope="{ row }">
+                <span :style="{ color: row.difference > 0 ? '#67c23a' : row.difference < 0 ? '#f56c6c' : '#999' }">
+                  {{ row.difference > 0 ? '+' : '' }}{{ row.difference }}
+                </span>
+              </template>
+            </el-table-column>
+            <el-table-column label="状态" width="80">
+              <template slot-scope="{ row }">
+                <el-tag :type="row.status === 'confirmed' ? 'success' : 'warning'" size="mini">
+                  {{ row.status === 'confirmed' ? '已确认' : '待确认' }}
+                </el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column prop="remark" label="备注" min-width="150" />
+            <el-table-column prop="createdAt" label="时间" width="160" />
+            <el-table-column label="操作" width="100" fixed="right">
+              <template slot-scope="{ row }">
+                <el-button v-if="row.status === 'pending'" size="mini" type="success" @click="handleConfirmCounting(row)">确认</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+          <el-pagination @size-change="countingPageSize=$event;loadCountingRecords()" @current-change="countingPage=$event;loadCountingRecords()" :current-page="countingPage" :page-sizes="[10,20,50]" :page-size="countingPageSize" :total="countingTotal" layout="total, sizes, prev, pager, next, jumper" class="pagination-wrap" />
+        </el-tab-pane>
+      </el-tabs>
+    </el-card>
+
+    <!-- 入库单详情弹窗 -->
+    <el-dialog title="入库单详情" :visible.sync="detailDialogVisible" width="700px">
+      <el-descriptions :column="2" border v-if="inboundDetail">
+        <el-descriptions-item label="入库单号">{{ inboundDetail.order.orderNo }}</el-descriptions-item>
+        <el-descriptions-item label="供应商">{{ inboundDetail.order.supplierName || '-' }}</el-descriptions-item>
+        <el-descriptions-item label="状态">{{ inboundDetail.order.status }}</el-descriptions-item>
+        <el-descriptions-item label="备注">{{ inboundDetail.order.remark || '-' }}</el-descriptions-item>
+      </el-descriptions>
+      <el-table :data="inboundDetail.items" border stripe style="margin-top:12px;" v-if="inboundDetail">
+        <el-table-column prop="productId" label="商品ID" width="70" />
+        <el-table-column prop="skuId" label="SKU ID" width="70" />
+        <el-table-column prop="quantity" label="数量" width="70" />
+      </el-table>
+    </el-dialog>
+
+    <!-- 新建盘点弹窗 -->
+    <el-dialog title="新建盘点" :visible.sync="showCreateCounting" width="450px">
+      <el-form label-width="100px">
+        <el-form-item label="商品ID">
+          <el-input v-model="countingForm.productId" type="number" />
+        </el-form-item>
+        <el-form-item label="SKU ID">
+          <el-input v-model="countingForm.skuId" type="number" placeholder="选填" />
+        </el-form-item>
+        <el-form-item label="实际库存">
+          <el-input v-model="countingForm.actualStock" type="number" />
+        </el-form-item>
+        <el-form-item label="备注">
+          <el-input v-model="countingForm.remark" type="textarea" :rows="2" />
+        </el-form-item>
+      </el-form>
+      <span slot="footer">
+        <el-button @click="showCreateCounting = false">取消</el-button>
+        <el-button type="primary" @click="handleCreateCounting">提交</el-button>
+      </span>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getInventoryTransactions, getInboundOrderList, getInboundOrderDetail, confirmInbound, cancelInbound, getStockAlerts, getCountingList, createCountingRecord, confirmCounting } from '@/api/admin'
+
+export default {
+  name: 'InventoryManage',
+  data() {
+    return {
+      activeTab: 'transactions',
+      // 流水
+      txList: [], txLoading: false, txPage: 1, txPageSize: 20, txTotal: 0,
+      txFilters: { productName: '', type: '', dateRange: null },
+      // 入库单
+      inboundList: [], inboundLoading: false, inboundPage: 1, inboundPageSize: 20, inboundTotal: 0,
+      inboundDetail: null, detailDialogVisible: false,
+      // 预警
+      alertList: [], alertLoading: false,
+      // 盘点
+      countingList: [], countingLoading: false, countingPage: 1, countingPageSize: 20, countingTotal: 0,
+      showCreateCounting: false, countingForm: { productId: '', skuId: '', actualStock: '', remark: '' }
+    }
+  },
+  mounted() {
+    this.loadTransactions()
+    this.loadInboundOrders()
+    this.loadAlerts()
+    this.loadCountingRecords()
+  },
+  methods: {
+    async loadTransactions() {
+      this.txLoading = true
+      try {
+        const params = {
+          page: this.txPage, size: this.txPageSize,
+          type: this.txFilters.type || null,
+          startDate: this.txFilters.dateRange ? this.txFilters.dateRange[0] : null,
+          endDate: this.txFilters.dateRange ? this.txFilters.dateRange[1] : null
+        }
+        const res = await getInventoryTransactions(params)
+        this.txList = res.data.records || []
+        this.txTotal = res.data.total || 0
+      } catch (e) { this.$message.error('加载失败') }
+      finally { this.txLoading = false }
+    },
+    async loadInboundOrders() {
+      this.inboundLoading = true
+      try {
+        const res = await getInboundOrderList({ page: this.inboundPage, size: this.inboundPageSize })
+        this.inboundList = res.data.records || []
+        this.inboundTotal = res.data.total || 0
+      } catch (e) { this.$message.error('加载失败') }
+      finally { this.inboundLoading = false }
+    },
+    async showInboundDetail(row) {
+      try {
+        const res = await getInboundOrderDetail(row.id)
+        this.inboundDetail = res.data
+        this.detailDialogVisible = true
+      } catch (e) { this.$message.error('加载失败') }
+    },
+    async handleConfirmInbound(row) {
+      try {
+        await this.$confirm('确认入库?库存将被增加,不可撤销。', '提示')
+        await confirmInbound(row.id)
+        this.$message.success('入库成功')
+        this.loadInboundOrders()
+      } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
+    },
+    async handleCancelInbound(row) {
+      try {
+        await this.$confirm('确认取消该入库单?', '提示')
+        await cancelInbound(row.id)
+        this.$message.success('已取消')
+        this.loadInboundOrders()
+      } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
+    },
+    async loadAlerts() {
+      this.alertLoading = true
+      try {
+        const res = await getStockAlerts()
+        this.alertList = res.data || []
+      } catch (e) { this.$message.error('加载失败') }
+      finally { this.alertLoading = false }
+    },
+    async loadCountingRecords() {
+      this.countingLoading = true
+      try {
+        const res = await getCountingList({ page: this.countingPage, size: this.countingPageSize })
+        this.countingList = res.data.records || []
+        this.countingTotal = res.data.total || 0
+      } catch (e) { this.$message.error('加载失败') }
+      finally { this.countingLoading = false }
+    },
+    async handleCreateCounting() {
+      if (!this.countingForm.productId || !this.countingForm.actualStock) {
+        return this.$message.warning('请填写必填项')
+      }
+      try {
+        await createCountingRecord({
+          productId: parseInt(this.countingForm.productId),
+          skuId: this.countingForm.skuId ? parseInt(this.countingForm.skuId) : null,
+          actualStock: parseInt(this.countingForm.actualStock),
+          remark: this.countingForm.remark
+        })
+        this.$message.success('盘点记录已创建')
+        this.showCreateCounting = false
+        this.countingForm = { productId: '', skuId: '', actualStock: '', remark: '' }
+        this.loadCountingRecords()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    async handleConfirmCounting(row) {
+      try {
+        await this.$confirm('确认盘点结果?差异将自动调账。', '提示')
+        await confirmCounting(row.id)
+        this.$message.success('盘点已确认')
+        this.loadCountingRecords()
+      } catch (e) { if (e !== 'cancel') this.$message.error('操作失败') }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.inventory-manage { padding: 20px; }
+.filter-bar { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
+.pagination-wrap { margin-top: 12px; text-align: right; }
+</style>
+```
+
+- [ ] **步骤 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
+<template>
+  <div class="inbound-create admin-page">
+    <el-card>
+      <div slot="header" class="admin-page-header">
+        <span class="admin-page-title">创建入库单</span>
+        <el-button @click="$router.push('/inventory')">返回</el-button>
+      </div>
+
+      <el-form label-width="100px">
+        <el-form-item label="供应商">
+          <el-input v-model="form.supplierName" placeholder="供应商名称" style="width:300px;" />
+        </el-form-item>
+
+        <el-form-item label="入库明细">
+          <div v-for="(item, idx) in form.items" :key="idx" style="display:flex;gap:8px;margin-bottom:8px;align-items:center;">
+            <el-input v-model="item.productId" placeholder="商品ID" type="number" style="width:120px;" />
+            <el-input v-model="item.skuId" placeholder="SKU ID(选填)" type="number" style="width:120px;" />
+            <el-input v-model="item.quantity" placeholder="数量" type="number" style="width:100px;" />
+            <el-button type="danger" size="mini" @click="form.items.splice(idx,1)">删除</el-button>
+          </div>
+          <el-button type="primary" size="mini" @click="form.items.push({productId:'',skuId:'',quantity:1})">添加行</el-button>
+        </el-form-item>
+
+        <el-form-item label="备注">
+          <el-input v-model="form.remark" type="textarea" :rows="2" style="width:400px;" />
+        </el-form-item>
+
+        <el-form-item>
+          <el-button type="success" @click="handleSubmit" :loading="submitting">提交入库单</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { createInboundOrder } from '@/api/admin'
+
+export default {
+  name: 'InventoryInboundCreate',
+  data() {
+    return {
+      submitting: false,
+      form: {
+        supplierName: '',
+        remark: '',
+        items: [{ productId: '', skuId: '', quantity: 1 }]
+      }
+    }
+  },
+  methods: {
+    async handleSubmit() {
+      const validItems = this.form.items.filter(i => i.productId && i.quantity > 0)
+      if (validItems.length === 0) {
+        return this.$message.warning('请添加入库明细')
+      }
+      this.submitting = true
+      try {
+        const items = validItems.map(i => ({
+          productId: parseInt(i.productId),
+          skuId: i.skuId ? parseInt(i.skuId) : null,
+          quantity: parseInt(i.quantity)
+        }))
+        await createInboundOrder({
+          supplierName: this.form.supplierName || null,
+          remark: this.form.remark || null,
+          items
+        })
+        this.$message.success('入库单已创建')
+        this.$router.push('/inventory')
+      } catch (e) {
+        this.$message.error('创建失败')
+      } finally {
+        this.submitting = false
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.inbound-create { padding: 20px; }
+</style>
+```
+
+- [ ] **步骤 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:在操作列增加「库存」按钮**
+
+在 `<el-dropdown>` 之前添加:
+```html
+<el-button size="mini" type="info" @click="showInventoryDialog(row)">库存</el-button>
+```
+
+- [ ] **步骤 2:添加库存弹窗对话框**
+
+在 `</el-card>` 之前添加:
+```html
+<el-dialog title="库存管理" :visible.sync="inventoryDialogVisible" width="800px">
+  <div v-loading="inventoryLoading">
+    <el-descriptions :column="2" border v-if="inventoryData">
+      <el-descriptions-item label="商品名称">{{ inventoryData.productName }}</el-descriptions-item>
+      <el-descriptions-item label="当前库存">{{ inventoryData.stock }}</el-descriptions-item>
+      <el-descriptions-item label="预警下限">{{ inventoryData.minStockAlert ?? 10 }}</el-descriptions-item>
+    </el-descriptions>
+
+    <h4 style="margin:16px 0 8px;">SKU 库存</h4>
+    <el-table :data="inventoryData.skus" border stripe v-if="inventoryData && inventoryData.skus && inventoryData.skus.length">
+      <el-table-column prop="id" label="SKU ID" width="70" />
+      <el-table-column prop="specs" label="规格" min-width="150" />
+      <el-table-column prop="stock" label="库存" width="70" />
+      <el-table-column prop="minStockAlert" label="预警下限" width="70" />
+    </el-table>
+    <div v-else-if="inventoryData" style="color:#999;padding:8px;">无SKU数据</div>
+
+    <h4 style="margin:16px 0 8px;">最近 10 条流水</h4>
+    <el-table :data="inventoryData.recentTransactions" border stripe v-if="inventoryData && inventoryData.recentTransactions && inventoryData.recentTransactions.length">
+      <el-table-column label="类型" width="80">
+        <template slot-scope="{ row }">
+          <el-tag :type="row.type === 'inbound' ? 'success' : row.type === 'outbound' ? 'danger' : 'warning'" size="mini">
+            {{ {'inbound':'入库','outbound':'出库','adjustment':'调账','counting':'盘点','refund':'退款入库'}[row.type] || row.type }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="数量" width="70">
+        <template slot-scope="{ row }">
+          <span :style="{ color: row.direction === 1 ? '#67c23a' : '#f56c6c' }">{{ row.direction === 1 ? '+' : '-' }}{{ row.quantity }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="beforeStock" label="变动前" width="70" />
+      <el-table-column prop="afterStock" label="变动后" width="70" />
+      <el-table-column prop="remark" label="备注" min-width="150" />
+      <el-table-column prop="createdAt" label="时间" width="160" />
+    </el-table>
+    <div v-else style="color:#999;padding:8px;">暂无流水记录</div>
+  </div>
+  <span slot="footer">
+    <el-button @click="inventoryDialogVisible = false">关闭</el-button>
+    <el-button type="primary" @click="$router.push('/inventory')">进入库存管理</el-button>
+  </span>
+</el-dialog>
+```
+
+- [ ] **步骤 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 代码一致