# Data Model: 商品规格

**Feature**: 013-food-spec | **Date**: 2026-07-15

## 实体关系

```text
pos_store (门店)
  │ 1
  │
  │ N
food_specs (规格组) ──< food_specs_value (规格值)
  │ N
  │
  │ N   (多对多，通过关联表)
pos_food (商品) ──< food_spec_relation ──> food_specs
```

## 新增表

### food_specs（规格组）

门店级可复用规格维度（如"甜度""加料"）。

| 列 | 类型 | 说明 |
|----|------|------|
| id | bigint PK auto | 主键 |
| title | varchar(64) | 规格名（如"甜度"） |
| type | char(1) | 1=单选 2=多选 |
| state | char(1) | 1=必选 2=可选 |
| language | varchar(8) | 语言（沿用现有 language 取值约定） |
| remark | varchar(255) | 备注 |
| md_id | bigint | 所属门店 id |
| sort | int | 排序 |
| is_open | tinyint(1) | 是否启用 |
| is_delete | tinyint(1) | 软删除标记 |

**transient 字段**（不入库）：`foodSpecsItems: List<FoodSpecsValue>` —— 承载级联规格值，保存时作为入参、查询时作为出参。

### food_specs_value（规格值）

规格组下的可选项（如"无糖""珍珠"）。

| 列 | 类型 | 说明 |
|----|------|------|
| id | bigint PK auto | 主键 |
| parent_id | bigint | 所属规格组 id（→ food_specs.id） |
| name | varchar(64) | 规格值名 |
| price | decimal(10,2) | 加价金额（与 pos_food.price 同口径） |
| note | varchar(255) | 备注 |
| state | char(1) | 状态 |
| is_open | tinyint(1) | 是否启用 |

### food_spec_relation（商品-规格关联）

商品与规格组的多对多关联。

| 列 | 类型 | 说明 |
|----|------|------|
| id | bigint PK auto | 主键 |
| food_id | bigint | 商品 id（→ pos_food.id） |
| specs_id | bigint | 规格组 id（→ food_specs.id） |

## 既有表扩展

### pos_food

- 保留现有 `food_sku`（text/json）列：冗余存规格 JSON，与结构化关联双写。
- **transient 字段**（不入库）：
  - `foodSpecs: List<FoodSpecs>` —— 保存商品时作为入参（写关联表），查询时作为出参（带规格结构）。
  - `sku: JSONArray` —— 保存时作为入参（toString 写入 `food_sku` 列）；foodie 已有此字段，复用。

## 校验规则（来自需求）

- 规格组 `type ∈ {1,2}`，`state ∈ {1,2}`。
- 删除规格组为软删（`is_delete=1`）；关联表记录保留（已生成订单不受影响）。
- 商品可用规格 = `is_open=1` 且 `is_delete=0` 的规格组，及其下 `is_open=1` 的规格值。
- 门店隔离：查询规格组强制按 `md_id` 过滤。

## 索引建议

- `food_specs`: `(md_id, language, is_delete, is_open)`
- `food_specs_value`: `(parent_id, is_open)`
- `food_spec_relation`: `(food_id)`, `(specs_id)`

## 外键

不建物理外键（与项目现有约定一致），靠应用层维护引用完整性。
