# 商品入库 — 技术方案

> **依据：** 《商品入库功能需求.md》v1.0.1  
> **关联：** 《关联需求分析.md》v1.6；平台《订单管理功能需求》v1.0.1、《商品管理功能需求》v1.3.3；商家《商品列表功能需求》v1.4、《店铺商品列表技术方案》、《商品出库功能需求-草稿》、《库存调整功能需求-草稿》、《库存查询功能需求-草稿》、《库存日志功能需求-草稿》、《售后管理功能需求》v1.0、《商家端店铺上下文接口说明》  
> **范围：** 商家端 **库存管理 · 商品入库** — 入库单列表/检索/详情、**手工入库**、**商品保存触发的自动入库（留痕）**；**不含** 出库/调整/库存查询页（兄弟模块复用本方案 **`biz_stock_log`**）。  
> **原则：** `X-Shop-Id` 店铺隔离；**手工入库改库存**；**自动入库仅留痕**（库存已在 `GoodsServiceImpl` 写入）；同次保存 **一张入库单多行明细**；单据 **确认后不可改**。

---

## 1. 技术架构

| 项 | 选型 |
|----|------|
| 基础框架 | RuoYi **v3.9.2**（`springboot2` 分支） |
| 数据库 | **MySQL 5.7.39** |
| ORM / 权限 / 响应 | MyBatis；`@PreAuthorize`；`AjaxResult` / `TableDataInfo` |
| 店铺上下文 | `SellerShopContextInterceptor` + **`X-Shop-Id`** |
| 入库单号 | **IN** + yyyyMMdd + 6 位序号（DB `MAX` 前缀，同 `OrderAppSupport.nextAftersaleNo`） |
| 事务 | 手工入库：**单事务**（主表 + 明细 + 加库存 + 日志）；自动入库：**与商品保存同事务** |
| 日志 | 手工入库 `@Log`；全链路写 **`biz_stock_log`** |

### 1.1 与兄弟模块 / 平台边界

| 维度 | 本模块 | 协作模块 |
|------|--------|----------|
| 路径 | **`/agri/seller/stock/inbound`** | 出库 `/agri/seller/stock/outbound`（待建） |
| 改库存 | **手工入库** `stock += qty` | 支付扣减 `deductStock`（已有）；调整模块（待建） |
| 自动入库 | **INSERT 入库单 + log**；**不加 stock** | **商品保存** 写 `biz_goods.stock` |
| 库存流水 | 写 **`biz_stock_log`** | 出库/调整/订单 **同表** |
| 平台端 | **无菜单** | — |

### 1.2 协作链

```text
【商品保存】GoodsServiceImpl.saveSeller
    → INSERT/UPDATE biz_goods.stock（唯一写入口，自动场景）
    → 若新建商品且 stock>0（首期单规格）
          StockInboundFacade.createAutoInbound（一张单、多行预留）
          INSERT biz_stock_inbound + item + biz_stock_log（不改 stock）
            ↓
【本模块】GET  list / GET detail / POST create（手工）
    → 校验 → IN 单号 → inbound + item
    → incrementStock（每条明细）
    → biz_stock_log
            ↓
【库存查询 / 库存日志】读 biz_goods.stock、biz_stock_log（待建页面）
```

| 关联 | 协作 |
|------|------|
| **商品列表** | `IGoodsService.saveSeller` 末尾调 **`IStockInboundFacade`**；**编辑改 stock 不触发** |
| **订单支付** | `BizGoodsMapper.deductStock` + 将来写 log（reason=下单扣减） |
| **售后** | **不自动** 退货入库；商家 **POST 本模块** |
| **库存调整** | 写 log（reason=库存编辑），**不产生** inbound 单 |

### 1.3 模块落位

**菜单：** 库存管理 → 商品入库  
**包路径：** `com.ruoyi.web.modules.stock`

```text
baqing-shop/src/main/java/com/ruoyi/web/modules/stock/
├── controller/SellerStockInboundController.java
├── service/
│   ├── IStockInboundService.java
│   ├── IStockLogService.java
│   ├── IStockInboundFacade.java          # 供 goods 模块调自动入库
│   └── impl/
│       ├── StockInboundServiceImpl.java
│       ├── StockLogServiceImpl.java
│       └── StockInboundFacadeImpl.java
├── domain/
│   ├── BizStockInbound.java
│   ├── BizStockInboundItem.java
│   └── BizStockLog.java
├── dto/
│   ├── StockInboundQuery.java
│   ├── StockInboundCreateDTO.java
│   └── StockInboundImportRowDTO.java
├── vo/
│   ├── StockInboundListRowVO.java
│   ├── StockInboundDetailVO.java
│   ├── StockInboundItemVO.java
│   └── StockGoodsPickVO.java
├── mapper/
│   ├── BizStockInboundMapper.java
│   ├── BizStockInboundItemMapper.java
│   └── BizStockLogMapper.java
├── constant/StockConstants.java
└── support/StockSupport.java             # nextInboundNo / reasonText

sql/biz_stock_inbound.sql                 # 权威 DDL（含 biz_stock_log）

# 商品侧改动（协作）
goods/service/impl/GoodsServiceImpl.java  # saveSeller 末尾调 facade
goods/mapper/BizGoodsMapper.java          # + incrementStock / selectSpecText
```

**前端（建议）：**

| 项 | 路径 |
|----|------|
| 列表 | `ruoyi-ui/src/views/agri/seller/stock/inbound/index.vue` |
| 入库弹窗 | 同上或 `inbound/form.vue` |
| API | `ruoyi-ui/src/api/agri/seller/stock/inbound.js` |

---

## 2. 数据库设计

**新建表：** `biz_stock_inbound`、`biz_stock_inbound_item`；**共享** `biz_stock_log`（出库/调整/订单后续复用）。  
**权威 DDL：** [`sql/biz_stock_inbound.sql`](../../../../sql/biz_stock_inbound.sql)

### 2.1 表 `biz_stock_inbound`（入库单主表）

| 字段 | 类型 | 说明 |
|------|------|------|
| inbound_id | bigint PK | 主键 |
| inbound_no | varchar(32) UK | 入库单号；**IN**+yyyyMMdd+6 位 |
| shop_id | bigint | 店铺；**= X-Shop-Id** |
| inbound_type | char(1) | `0` 新增商品规格 `1` 采购 `2` 退货 `3` 其他 |
| remark | varchar(200) | 备注；自动单可空 |
| operator_id | bigint | 经办人 user_id |
| operator_name | varchar(64) | 经办人登录名快照 |
| inbound_time | datetime | 入库时间（确认时刻） |
| create_by / create_time | | 审计 |

**索引：** `uk_inbound_no`；`idx_shop_time (shop_id, inbound_time DESC)`；`idx_shop_type (shop_id, inbound_type, inbound_time)`。

> **无 del_flag：** 单据不可删；**无 update** 业务字段（确认后只读）。

### 2.2 表 `biz_stock_inbound_item`（入库明细）

| 字段 | 类型 | 说明 |
|------|------|------|
| item_id | bigint PK | |
| inbound_id | bigint | FK → inbound |
| goods_id | bigint | 商品 ID |
| sku_id | bigint NULL | **预留**多规格；首期 **NULL** |
| goods_name | varchar(200) | 快照 |
| main_pic | varchar(512) | 快照 |
| spec_text | varchar(256) | 规格展示；单规格 **空或「—」** |
| quantity | int | 入库数量（正整数） |
| stock_before | int | 该行确认前可用库存 |
| stock_after | int | 该行确认后可用库存 |

**索引：** `idx_inbound_id`；`idx_goods_id (goods_id, inbound_id)`（供「按商品名搜单」JOIN）。

### 2.3 表 `biz_stock_log`（库存流水 · 共享）

| 字段 | 类型 | 说明 |
|------|------|------|
| log_id | bigint PK | |
| shop_id | bigint | |
| goods_id | bigint | |
| sku_id | bigint NULL | 预留 |
| goods_name | varchar(200) | 快照 |
| spec_text | varchar(256) | |
| direction | char(1) | `1` 入库 `2` 出库 |
| change_reason | varchar(32) | 见 §2.4 |
| change_qty | int | 变化量（**正数**） |
| stock_before | int | |
| stock_after | int | |
| ref_type | varchar(16) | `INBOUND` / `OUTBOUND` / `ADJUST` / `ORDER` / `GOODS` |
| ref_id | bigint | 关联主键（如 inbound_id） |
| ref_no | varchar(32) | 关联单号（如 inbound_no） |
| operator_id / operator_name | | |
| create_time | datetime | 操作时间 |

**索引：** `idx_shop_time (shop_id, create_time DESC)`；`idx_goods_time (shop_id, goods_id, create_time DESC)`。

### 2.4 枚举

**inbound_type（主表 & 列表展示）：**

| 值 | 含义 | 来源 |
|----|------|------|
| `0` | 新增商品规格 | 商品保存自动 |
| `1` | 采购入库 | 手工 |
| `2` | 退货入库 | 手工 |
| `3` | 其他入库 | 手工 |

**change_reason（`biz_stock_log`，与需求 §3.2 对齐）：**

| code | direction | 说明 |
|------|-----------|------|
| `NEW_SPEC` | 1 | 新增商品规格 |
| `PURCHASE_IN` | 1 | 采购入库 |
| `RETURN_IN` | 1 | 退货入库 |
| `OTHER_IN` | 1 | 其他入库 |
| `STOCK_EDIT` | 1/2 | 库存编辑（调整模块） |
| `ORDER_DEDUCT` | 2 | 下单扣减（订单模块） |
| `ORDER_CANCEL` | 1 | 取消订单返库存（若实现） |
| … | | 出库模块扩展 |

**手工创建 DTO 入库类型（前端 radio → 后端）：** `purchase`→`1`，`return`→`2`，`other`→`3`。

### 2.5 与现有 `biz_goods` 关系

| 项 | 定稿 |
|----|------|
| 库存字段 | **`biz_goods.stock`**（首期 **单规格**，无 SKU 表） |
| 加库存 SQL | `incrementStock`：`stock = stock + #{qty}`，`del_flag='0'` |
| 扣库存 | 已有 `deductStock`（订单） |
| 条码 | **首期无 barcode 列**；扫码接口按 **`goods_sn` 精确匹配**；后续可加 `barcode` 列扩展 |
| 规格文案 | `selectSpecText(goodsId)`：聚合 `biz_goods_attr` 中 `attr_type='2'`，无则 `null` |

### 2.6 首期「新增规格」触发口径（实现）

需求为多规格预留；**当前代码仅 `biz_goods.stock`**：

| 场景 | 是否自动入库 |
|------|--------------|
| **新建商品**（INSERT）且 `stock > 0` | **是**（type=`0`，**仅留痕**） |
| **编辑商品** 仅改 `stock` | **否** → 将来走 **库存调整** |
| **编辑商品** 新增 SKU 行（多规格上线后） | **是**（同次保存 **一张单、多行**） |

---

## 3. 接口设计

**基路径：** `/agri/seller/stock/inbound`  
**上下文：** **`X-Shop-Id`** 必填（`SellerShopContext.getShopId()`）

### 3.1 接口一览

| 方法 | 路径 | 权限 | 说明 |
|------|------|------|------|
| GET | `/list` | `agri:seller:stock:inbound:list` | 分页列表 |
| GET | `/{inboundId}` | `agri:seller:stock:inbound:query` | 详情 |
| POST | `/` | `agri:seller:stock:inbound:add` | **手工入库** |
| GET | `/operators` | `agri:seller:stock:inbound:list` | 经办人下拉 |
| GET | `/goods/options` | `agri:seller:stock:inbound:add` | 商品选择器 |
| GET | `/goods/byBarcode` | `agri:seller:stock:inbound:add` | 条码/编号录入 |
| POST | `/import/preview` | `agri:seller:stock:inbound:add` | 导入预览（可选） |

> **说明：** 自动入库 **无 HTTP 接口**，由 **`IStockInboundFacade`** 内部调用。

### 3.2 列表 `GET /list`

**Query（StockInboundQuery）：**

| 参数 | 说明 |
|------|------|
| pageNum, pageSize | 分页 |
| keyword | 入库单号 **或** 明细商品名称（模糊） |
| inboundType | 空=全部；`0`～`3` |
| operatorId | 经办人 user_id |
| beginTime, endTime | 入库时间区间 `yyyy-MM-dd` |

**固定条件：** `shop_id = requireShopId()`。  
**排序：** `inbound_time DESC, inbound_id DESC`。

**Row（StockInboundListRowVO）：**

| 字段 | 来源 |
|------|------|
| inboundId, inboundNo, inboundType, inboundTypeText | 主表 |
| inboundTime | `yyyy-MM-dd HH:mm:ss` |
| remark | |
| operatorName | |

### 3.3 详情 `GET /{inboundId}`

**条件：** `shop_id` 匹配，否则 404。

**StockInboundDetailVO：** 主表字段 + `items[]`（StockInboundItemVO：goodsId、goodsName、mainPic、specText、quantity）。

### 3.4 手工入库 `POST /`

**Body（StockInboundCreateDTO）：**

```json
{
  "inboundType": "1",
  "remark": "optional",
  "items": [
    { "goodsId": 1001, "skuId": null, "quantity": 10 }
  ]
}
```

| 字段 | 校验 |
|------|------|
| inboundType | 必填；`1`/`2`/`3` |
| remark | ≤200 |
| items | 至少 1 行；**goodsId 不可重复**（重复 **合并 quantity**，前端合并后提交亦可） |
| items[].quantity | 整数 ≥1 |

**处理流程（单事务）：**

```text
1. requireShopId()
2. 校验 IN1～IN6（商品存在、属当前店、未删除）
3. inboundNo = StockSupport.nextInboundNo()
4. INSERT biz_stock_inbound
5. FOR each item（按 goodsId 顺序，避免死锁）:
     SELECT stock FROM biz_goods FOR UPDATE
     stock_before = stock
     incrementStock(goodsId, qty)          -- 手工入库才执行
     stock_after = stock_before + qty
     INSERT biz_stock_inbound_item（含快照）
     INSERT biz_stock_log（direction=1, reason=PURCHASE_IN/…, ref_type=INBOUND）
6. 返回 { inboundId, inboundNo }
```

**失败：** 任一行商品无效 → **整单回滚**（IN-A2）。

### 3.5 经办人 `GET /operators`

返回当前 `shop_id` 下 **历史入库单** 出现过的 `operator_id + operator_name`（去重），供筛选下拉。

### 3.6 商品选择 `GET /goods/options`

| 参数 | 说明 |
|------|------|
| keyword | 商品名称 / 编号模糊 |
| pageNum, pageSize | 分页 |

**条件：** `shop_id`、`del_flag='0'`（**不限** goods_status）。  
**返回 StockGoodsPickVO：** goodsId、goodsName、mainPic、specText、stock（当前可用库存）、goodsSn。

### 3.7 条码录入 `GET /goods/byBarcode`

| 参数 | 说明 |
|------|------|
| barcode | 必填 |

**逻辑：** `shop_id` 下 `goods_sn = barcode`（首期）；0 条 → 业务错误「未找到该条码对应的商品」；多条 → 返回 **候选列表** 由前端选择（多规格上线后扩展）。

### 3.8 导入预览 `POST /import/preview`（可选）

上传 Excel → 解析为 `StockInboundImportRowDTO`（goodsSn/specText/quantity）→ 校验 → 返回 `{ validRows[], errorRows[] }`。  
**定稿：** 存在 **errorRows** 时 **禁止提交** 正式入库（前端拦截）；正式入库仍走 **`POST /`**。

---

## 4. 自动入库（Facade）

**接口：** `IStockInboundFacade.createAutoInbound(AutoInboundCommand cmd)`

**调用点：** `GoodsServiceImpl.saveSeller` **事务内**、商品 **INSERT 成功** 且 `stock > 0` 之后。

**AutoInboundCommand 字段：**

| 字段 | 说明 |
|------|------|
| shopId | |
| operatorId / operatorName | 当前登录用户 |
| lines | `[{ goodsId, specText, quantity, stockBefore, stockAfter }]` |

**规则（v1.0.1）：**

- **一次 save 一张单**；lines 多条 → 多行 `biz_stock_inbound_item`。
- **不调用** `incrementStock`（stock 已由商品 INSERT 写入）。
- `stock_before=0`，`stock_after=quantity`（新建商品）。
- 写 `biz_stock_log`（reason=`NEW_SPEC`，direction=`1`）。
- 若 lines 为空 → **不建单**。

**多规格扩展：** 商品保存识别「新增 SKU 行」组装 lines；仍 **一张单**。

---

## 5. 核心 SQL 片段

**列表 keyword：**

```sql
WHERE b.shop_id = #{shopId}
  AND (#{keyword} IS NULL OR b.inbound_no LIKE CONCAT('%',#{keyword},'%')
       OR EXISTS (
         SELECT 1 FROM biz_stock_inbound_item i
         WHERE i.inbound_id = b.inbound_id
           AND i.goods_name LIKE CONCAT('%',#{keyword},'%')
       ))
```

**incrementStock：**

```sql
UPDATE biz_goods
SET stock = stock + #{quantity}, update_time = sysdate()
WHERE goods_id = #{goodsId} AND shop_id = #{shopId} AND del_flag = '0'
```

---

## 6. 权限与菜单（SQL 种子建议）

| 权限标识 | 说明 |
|----------|------|
| `agri:seller:stock:inbound:list` | 列表、经办人下拉 |
| `agri:seller:stock:inbound:query` | 详情 |
| `agri:seller:stock:inbound:add` | 手工入库、选品、条码、导入 |

菜单：**库存管理 → 商品入库**（`parent_id` 指向库存管理目录，`path=inbound`）。

---

## 7. 实现清单

| # | 项 | 状态 |
|---|-----|------|
| 1 | DDL `sql/biz_stock_inbound.sql` | 待建 |
| 2 | Domain / Mapper / XML | 待建 |
| 3 | `StockSupport.nextInboundNo` | 待建 |
| 4 | `StockInboundServiceImpl` 手工入库 | 待建 |
| 5 | `StockInboundFacadeImpl` 自动入库 | 待建 |
| 6 | `GoodsServiceImpl.saveSeller` 挂钩 | 待改 |
| 7 | `BizGoodsMapper.incrementStock` | 待建 |
| 8 | `SellerStockInboundController` | 待建 |
| 9 | 单元测试 Service + Controller | 待建 |
| 10 | ruoyi-ui 列表/入库弹窗 | 待建 |

---

## 8. 版本记录

| 版本 | 说明 |
|------|------|
| **v1.0** | 首版：DDL、接口、手工/自动入库分工、与 goods/order/兄弟模块协作；对齐需求 v1.0.1 **留痕不加库存**、**一次保存一张单** |

---

*文档版本：v1.0 · 依据《商品入库功能需求.md》v1.0.1*
