# 镜框资料与供应状态实施方案

## 一、实施目标

基于 `镜框资料与供应状态需求分析.md`，本方案落地以下能力：

1. 在产品资料中维护镜框尺寸、厂家信息、销售状态、供应状态和 SKU 等级。
2. 定制履约创建订单快照时补齐镜片厂家所需加工参数。
3. 镜片委外单向镜片厂家展示镜片高度、中梁宽度、镜片最大宽度。
4. 镜框采购单推送厂家时只包含厂家货号、厂家颜色编号、镜框成本。
5. 支持供应商回传镜框供应状态，并保存历史记录。
6. 支持根据销售数据自动计算镜框 SKU A/B/C/D 等级。
7. 支持按 SKU 等级定期生成供应商状态确认任务。

## 二、总体技术路线

镜框主资料直接扩展现有产品表：

```text
product              # SKU、产品名、成本、重量、镜框尺寸、厂家信息、销售状态、供应状态、SKU等级
```

过程型数据新增专用表：

```text
frame_supply_status_log          # 供应状态回传历史
frame_sku_grade_log              # SKU 等级计算历史
frame_supply_check_task          # 供应状态确认任务
frame_supply_check_task_detail   # 供应状态确认任务明细
```

这样可以避免为镜框主资料新增独立主表，同时减少定制字段读写复杂度，并保留供应状态和等级计算的过程追溯。

## 三、数据改造

### 3.1 产品表字段新增

在 `product` 表新增镜框定制字段。字段全部加 `frame_` 前缀，避免污染原通用产品字段语义。

镜框尺寸字段：

| 字段 | 标题 | 类型 | 必填 |
| --- | --- | --- | --- |
| `frame_lens_width` | 镜片宽度 | number | 是 |
| `frame_lens_height` | 镜片高度 | number | 是 |
| `frame_width` | 镜框宽度 | number | 是 |
| `frame_bridge_width` | 中梁宽度 | number | 是 |
| `frame_temple_length` | 镜腿长度 | number | 是 |
| `frame_lens_diagonal` | 镜片斜对角线 | number | 否 |
| `frame_size_display` | 框架上的尺寸 | string | 是 |
| `frame_size_unit` | 尺寸单位 | string | 否，默认 `mm` |

厂家和运营字段：

| 字段 | 标题 | 类型 | 必填 |
| --- | --- | --- | --- |
| `frame_manufacturer` | 厂家 | string | 是 |
| `frame_factory_item_no` | 厂家货号 | string | 是 |
| `frame_factory_color_no` | 厂家颜色编号 | string | 是 |
| `frame_color` | 颜色 | string | 是 |
| `frame_original_price` | 原始定价 | number | 是 |
| `packaging_cost` | 包材费用 | number | 否 |
| `frame_spring_hinge` | 弹簧铰链 | select | 是 |
| `frame_adjustable_nose_pad` | 可调节鼻托 | select | 是 |
| `frame_sales_status` | 镜框销售状态 | select | 是 |
| `frame_supply_status` | 供应状态 | select | 是 |
| `frame_sku_grade` | SKU 等级 | select | 系统自动 |
| `frame_supply_status_updated_at` | 最近供应状态更新时间 | datetime/string | 系统自动 |
| `frame_supply_status_updated_by` | 最近供应状态回传方 | string | 系统自动 |

枚举建议：

```text
frame_spring_hinge: 0 否, 1 是
frame_adjustable_nose_pad: 0 否, 1 是

frame_sales_status:
0 未上架
10 正常销售
20 暂时隐藏
30 停止销售

frame_supply_status:
0 未知
10 供应正常
20 需要生产
30 停产

frame_sku_grade:
A / B / C / D
```

### 3.2 新增 SQL

建议在下一个月度 SQL 文件中增加以下产品字段和过程表。

```sql
ALTER TABLE `hg_product`
  ADD COLUMN `packaging_cost` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '包材费用',
  ADD COLUMN `frame_lens_width` decimal(8,2) DEFAULT NULL COMMENT '镜片宽度',
  ADD COLUMN `frame_lens_height` decimal(8,2) DEFAULT NULL COMMENT '镜片高度',
  ADD COLUMN `frame_width` decimal(8,2) DEFAULT NULL COMMENT '镜框宽度',
  ADD COLUMN `frame_bridge_width` decimal(8,2) DEFAULT NULL COMMENT '中梁宽度',
  ADD COLUMN `frame_temple_length` decimal(8,2) DEFAULT NULL COMMENT '镜腿长度',
  ADD COLUMN `frame_lens_diagonal` decimal(8,2) DEFAULT NULL COMMENT '镜片斜对角线',
  ADD COLUMN `frame_size_display` varchar(64) DEFAULT NULL COMMENT '框架上的尺寸，如 53-19-147',
  ADD COLUMN `frame_size_unit` varchar(16) NOT NULL DEFAULT 'mm' COMMENT '镜框尺寸单位',
  ADD COLUMN `frame_manufacturer` varchar(128) DEFAULT NULL COMMENT '镜框厂家',
  ADD COLUMN `frame_factory_item_no` varchar(128) DEFAULT NULL COMMENT '厂家货号',
  ADD COLUMN `frame_factory_color_no` varchar(128) DEFAULT NULL COMMENT '厂家颜色编号',
  ADD COLUMN `frame_color` varchar(128) DEFAULT NULL COMMENT '镜框颜色',
  ADD COLUMN `frame_original_price` decimal(10,2) unsigned NOT NULL DEFAULT '0.00' COMMENT '原始定价，仅上架参考',
  ADD COLUMN `frame_spring_hinge` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '弹簧铰链 0否 1是',
  ADD COLUMN `frame_adjustable_nose_pad` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '可调节鼻托 0否 1是',
  ADD COLUMN `frame_sales_status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '镜框销售状态: 0未上架 10正常销售 20暂时隐藏 30停止销售',
  ADD COLUMN `frame_supply_status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '供应状态: 0未知 10供应正常 20需要生产 30停产',
  ADD COLUMN `frame_sku_grade` char(1) DEFAULT NULL COMMENT 'SKU等级 A/B/C/D',
  ADD COLUMN `frame_supply_status_updated_at` datetime DEFAULT NULL COMMENT '最近供应状态更新时间',
  ADD COLUMN `frame_supply_status_updated_by` varchar(64) DEFAULT NULL COMMENT '最近供应状态回传方',
  ADD KEY `idx_frame_sales_status` (`frame_sales_status`),
  ADD KEY `idx_frame_supply_status` (`frame_supply_status`),
  ADD KEY `idx_frame_sku_grade` (`frame_sku_grade`),
  ADD KEY `idx_frame_factory_item` (`frame_factory_item_no`, `frame_factory_color_no`);
```

```sql
CREATE TABLE IF NOT EXISTS `hg_frame_supply_status_log` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `company_id` char(18) NOT NULL COMMENT '公司ID',
  `product_id` int(11) unsigned NOT NULL COMMENT '镜框产品ID',
  `supplier_id` int(11) unsigned DEFAULT NULL COMMENT '供应商ID',
  `source_type` tinyint(2) unsigned NOT NULL DEFAULT '1' COMMENT '来源: 1采购单 2定期回传 3人工维护',
  `source_id` int(11) unsigned DEFAULT NULL COMMENT '来源单据ID',
  `supply_status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '供应状态: 0未知 10供应正常 20需要生产 30停产',
  `expected_delivery_days` int(11) unsigned DEFAULT NULL COMMENT '预计交期天数',
  `supplier_remark` varchar(500) DEFAULT NULL COMMENT '供应商备注',
  `create_admin_id` int(11) unsigned DEFAULT NULL COMMENT '创建人',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_product` (`company_id`, `product_id`),
  KEY `idx_supplier` (`supplier_id`),
  KEY `idx_status_time` (`supply_status`, `create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='镜框供应状态回传记录';
```

```sql
CREATE TABLE IF NOT EXISTS `hg_frame_sku_grade_log` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `company_id` char(18) NOT NULL COMMENT '公司ID',
  `product_id` int(11) unsigned NOT NULL COMMENT '镜框产品ID',
  `grade` char(1) NOT NULL COMMENT '等级 A/B/C/D',
  `stat_start_date` date NOT NULL COMMENT '统计开始日期',
  `stat_end_date` date NOT NULL COMMENT '统计结束日期',
  `sale_qty` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '销量',
  `sale_amount` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '销售金额',
  `rank_no` int(11) unsigned DEFAULT NULL COMMENT '排名',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_product_date` (`product_id`, `stat_end_date`),
  KEY `idx_grade` (`company_id`, `grade`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='镜框SKU等级计算记录';
```

```sql
CREATE TABLE IF NOT EXISTS `hg_frame_supply_check_task` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `company_id` char(18) NOT NULL COMMENT '公司ID',
  `supplier_id` int(11) unsigned NOT NULL COMMENT '供应商ID',
  `period` varchar(32) NOT NULL COMMENT '任务周期',
  `grade_scope` varchar(32) DEFAULT NULL COMMENT '等级范围，如 A/B/C',
  `sku_count` int(11) unsigned NOT NULL DEFAULT '0' COMMENT 'SKU数量',
  `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态: 0待发送 10已发送 20部分回传 30已完成 80已过期',
  `send_time` datetime DEFAULT NULL COMMENT '发送时间',
  `finish_time` datetime DEFAULT NULL COMMENT '完成时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_supplier_period` (`supplier_id`, `period`),
  KEY `idx_status` (`company_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='镜框供应状态回传任务';
```

```sql
CREATE TABLE IF NOT EXISTS `hg_frame_supply_check_task_detail` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `company_id` char(18) NOT NULL COMMENT '公司ID',
  `task_id` int(11) unsigned NOT NULL COMMENT '任务ID',
  `product_id` int(11) unsigned NOT NULL COMMENT '镜框产品ID',
  `sku_grade` char(1) NOT NULL COMMENT 'SKU等级',
  `factory_item_no` varchar(128) DEFAULT NULL COMMENT '厂家货号',
  `factory_color_no` varchar(128) DEFAULT NULL COMMENT '厂家颜色编号',
  `supply_status` tinyint(2) unsigned DEFAULT NULL COMMENT '供应状态',
  `expected_delivery_days` int(11) unsigned DEFAULT NULL COMMENT '预计交期天数',
  `supplier_remark` varchar(500) DEFAULT NULL COMMENT '供应商备注',
  `status` tinyint(2) unsigned NOT NULL DEFAULT '0' COMMENT '状态: 0待填写 10已填写',
  `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_task` (`task_id`),
  KEY `idx_product` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='镜框供应状态回传任务明细';
```

### 3.3 新增 Model

新增：

```text
App\Model\FrameSupplyStatusLog
App\Model\FrameSkuGradeLog
App\Model\FrameSupplyCheckTask
App\Model\FrameSupplyCheckTaskDetail
```

Model 要点：

- 使用现有 `Model` 基类。
- 增加 `BelongsToCompany`。
- `FrameSupplyCheckTask` 关联 `detailList`。
- `FrameSupplyCheckTaskDetail` 关联 `productInfo`。
- `FrameSupplyStatusLog` 关联 `productInfo`、`supplierInfo`。

## 四、后端服务设计

### 4.1 镜框产品资料服务

新增服务：

```text
App\Service\Common\FrameProductProfileService
```

职责：

1. 读取产品镜框字段。
2. 校验镜框必填字段。
3. 计算镜片最大宽度。
4. 生成订单快照中的 `frame.size`。
5. 为采购单、镜片委外单、供应状态任务提供统一字段输出。

核心方法建议：

```php
getProfile(int $productId): array
validateRequired(array $profile): void
buildFrameSnapshot(int $productId, array $baseSnapshot = []): array
buildLensFactoryParams(array $frameSnapshot): array
buildPurchasePayload(int $productId): array
updateSupplyStatus(int $productId, int $status, array $context = []): void
```

### 4.2 订单快照改造

改造：

```text
App\Service\Common\OpticalCustomFulfillmentBuildService::buildFrameSnapshot()
```

当前代码曾从产品扩展字段读取：

```text
frame_lens_width
frame_bridge_width
frame_temple_length
frame_size_display
frame_size_unit
frame_color
```

需要补充读取：

```text
frame_lens_height
frame_width
frame_lens_diagonal
frame_factory_item_no
frame_factory_color_no
frame_manufacturer
```

并在快照中写入：

```json
{
  "frame": {
    "size": {
      "lens_width": 53,
      "lens_height": 40,
      "frame_width": 138,
      "bridge_width": 19,
      "temple_length": 147,
      "lens_diagonal": 55,
      "lens_max_width": 55,
      "display": "53-19-147",
      "unit": "mm"
    },
    "factory": {
      "manufacturer": "xxx",
      "item_no": "F12345",
      "color_no": "C07"
    }
  }
}
```

缺少 `frame_lens_height`、`frame_lens_width`、`frame_bridge_width` 时，应将镜框部件标记异常，或阻断镜片委外生成。

推荐第一阶段：阻断镜片委外生成，提示补齐镜框尺寸。

### 4.3 镜片厂家参数输出

在供应商委外单详情中增加：

```json
{
  "lens_factory_params": {
    "lens_height": 40,
    "bridge_width": 19,
    "lens_max_width": 55,
    "unit": "mm"
  }
}
```

改造点：

```text
App\Controller\Admin\SupplierEntrustController@detail
App\Controller\Admin\SupplierEntrustController@componentList
```

如果供应商端后续独立接口展示委外单，也同步增加。

取值优先级：

```text
custom_item_component.params_snapshot.frame.size
  -> product
  -> 报错/提示补录
```

### 4.4 镜框采购单推送厂家

新增服务方法：

```text
FrameProductProfileService::buildPurchasePayload(int $productId)
```

输出只允许：

```json
{
  "factory_item_no": "F12345",
  "factory_color_no": "C07",
  "frame_cost": 12.5
}
```

采购单推送厂家前校验：

1. 产品是镜框 SKU。
2. 有厂家或供应商。
3. 有厂家货号。
4. 有厂家颜色编号。
5. 有镜框成本。

如果现有采购单还没有“推送供应商/厂家”动作，第一阶段先在采购单详情增加“厂家推送数据预览”，实际推送后续接供应商门户或外部接口。

### 4.5 供应状态回传

新增服务：

```text
App\Service\Common\FrameSupplyStatusService
```

职责：

1. 供应商回传供应状态。
2. 写入 `frame_supply_status_log`。
3. 更新 `product.frame_supply_status`。
4. 更新 `product.frame_supply_status_updated_at`。
5. 更新回传任务明细状态。
6. 根据供应状态生成销售状态建议。

核心方法：

```php
updateStatus(int $productId, int $supplierId, int $status, array $context = []): void
batchSubmitTask(int $taskId, array $rows, int $supplierId): array
suggestSalesStatus(int $productId, int $supplyStatus): ?int
```

销售状态建议只返回建议，不自动覆盖 `frame_sales_status`。

### 4.6 SKU 等级计算

新增服务：

```text
App\Service\Common\FrameSkuGradeService
```

职责：

1. 按销售数据统计镜框 SKU 销量。
2. 计算 A/B/C/D 等级。
3. 写入 `frame_sku_grade_log`。
4. 更新 `product.frame_sku_grade`。

默认规则：

```text
统计窗口：近 90 天
A：近 90 天销量排名前 20%
B：近 90 天销量排名 20%-50%
C：近 90 天有销量但不属于 A/B
D：近 90 天无销量
```

统计范围：

- 普通销售订单。
- 排除取消订单。
- 排除红人寄样订单。
- 排除全额退货冲销订单。

后续可用公司配置覆盖阈值。

### 4.7 供应状态确认任务

新增服务：

```text
App\Service\Common\FrameSupplyCheckTaskService
```

职责：

1. 根据 SKU 等级和周期生成供应商回传任务。
2. 生成任务明细。
3. 供应商提交状态。
4. 任务完成度统计。

任务生成规则：

| 等级 | 生成频率 |
| --- | --- |
| A | 每周 |
| B | 每两周 |
| C | 每月 |
| D | 不生成 |

周期建议：

```text
A: YYYY-WW
B: YYYY-BIWEEK-序号
C: YYYY-MM
```

第一阶段可以手工点击生成任务；第二阶段再加定时任务自动生成。

## 五、接口实施

### 5.1 管理端供应状态接口

新增控制器：

```text
App\Controller\Admin\FrameSupplyStatusController
```

路由：

```text
GET  /admin/frame-supply-status/log
POST /admin/frame-supply-status/update
```

`POST /admin/frame-supply-status/update` 请求：

```json
{
  "product_id": 123,
  "supplier_id": 88,
  "supply_status": 10,
  "expected_delivery_days": 7,
  "supplier_remark": "有现货",
  "source_type": 3
}
```

### 5.2 管理端 SKU 等级接口

新增控制器：

```text
App\Controller\Admin\FrameSkuGradeController
```

路由：

```text
POST /admin/frame-sku-grade/recalculate
GET  /admin/frame-sku-grade/list
GET  /admin/frame-sku-grade/log
```

`POST /admin/frame-sku-grade/recalculate` 请求：

```json
{
  "stat_start_date": "2026-05-01",
  "stat_end_date": "2026-08-01"
}
```

不传日期时默认近 90 天。

### 5.3 管理端供应确认任务接口

新增控制器：

```text
App\Controller\Admin\FrameSupplyCheckTaskController
```

路由：

```text
GET  /admin/frame-supply-check-task/index
GET  /admin/frame-supply-check-task/detail
POST /admin/frame-supply-check-task/generate
POST /admin/frame-supply-check-task/send
POST /admin/frame-supply-check-task/close
```

`POST /admin/frame-supply-check-task/generate` 请求：

```json
{
  "supplier_id": 88,
  "grade_scope": ["A", "B"],
  "period": "2026-W31"
}
```

### 5.4 供应商端回传接口

新增控制器：

```text
App\Controller\Supplier\FrameSupplyCheckTaskController
```

路由：

```text
GET  /supplier/frame-supply-check-task/index
GET  /supplier/frame-supply-check-task/detail
POST /supplier/frame-supply-check-task/submit
POST /supplier/frame-supply-status/update
```

供应商提交任务：

```json
{
  "task_id": 1001,
  "items": [
    {
      "detail_id": 1,
      "supply_status": 10,
      "expected_delivery_days": 3,
      "supplier_remark": "有货"
    },
    {
      "detail_id": 2,
      "supply_status": 20,
      "expected_delivery_days": 15,
      "supplier_remark": "需要生产"
    }
  ]
}
```

权限要求：

- 供应商只能查看和提交 `supplier_id = 当前登录供应商` 的任务。
- 供应商端不返回 ERP SKU、产品名、销售状态、原始定价、销售数据。

## 六、前端实施

### 6.1 镜框产品编辑页

在产品编辑页增加“镜框资料”分组。

分组字段：

1. 尺寸参数：镜片宽度、镜片高度、镜框宽度、中梁宽度、镜腿长度、镜片斜对角线、框架上的尺寸。
2. 厂家信息：厂家、厂家货号、厂家颜色编号。
3. 销售信息：颜色、原始定价、镜框成本、重量、弹簧铰链、可调节鼻托。
4. 状态信息：销售状态、供应状态、SKU 等级、最近供应状态更新时间。

前端校验：

- 必填字段不能为空。
- 数值字段必须大于等于 0。
- `frame_size_display` 格式建议提示为 `53-19-147`，不强制只允许该格式。

### 6.2 镜片委外单详情

在供应商可见参数中增加：

- 镜片高度。
- 中梁宽度。
- 镜片最大宽度。

内部端可以同时显示来源字段；供应商端只显示加工所需字段。

### 6.3 采购单厂家推送预览

镜框采购单增加“厂家推送数据”区块：

| 字段 | 值 |
| --- | --- |
| 厂家货号 | `frame_factory_item_no` |
| 厂家颜色编号 | `frame_factory_color_no` |
| 镜框成本 | `product.cost` |

包材费用 `product.packaging_cost` 仅用于 ERP 内部成本核算和后续利润统计，不推给镜框厂家。

缺字段时显示红色提示，并阻断推送。

### 6.4 SKU 等级和供应状态页面

管理端新增“镜框供应状态”页面：

- SKU 等级。
- 供应状态。
- 销售状态。
- 厂家。
- 厂家货号。
- 厂家颜色编号。
- 最近回传时间。
- 操作：手动更新、查看历史、生成任务。

### 6.5 供应商填写页

供应商端任务详情只展示：

- 厂家货号。
- 厂家颜色编号。
- 当前等级。
- 供应状态选择。
- 预计交期。
- 备注。

## 七、定时任务

### 7.1 SKU 等级计算任务

建议命令：

```text
php bin/hyperf.php frame:sku-grade:refresh
```

默认每日凌晨执行一次。

职责：

1. 统计近 90 天销售数据。
2. 计算等级。
3. 更新产品表 `frame_sku_grade`。
4. 写入等级日志。

### 7.2 供应确认任务生成

建议命令：

```text
php bin/hyperf.php frame:supply-check:generate
```

默认每周一执行。

生成规则：

- A 级：每周生成。
- B 级：隔周生成。
- C 级：每月第一个周一生成。
- D 级：不生成。

第一阶段可不启用自动命令，只做管理端手工生成。

## 八、与现有流程的集成点

### 8.1 定制履约

改造 `OpticalCustomFulfillmentBuildService`：

- 生成镜框部件快照时补齐新字段。
- 计算 `lens_max_width`。
- 缺必填尺寸时阻断镜片委外或标记异常。

### 8.2 镜片委外

改造委外单详情展示：

- 从镜框部件快照读取加工参数。
- 展示给镜片厂家。
- 不从当前产品资料实时读取历史订单参数。

### 8.3 镜框采购

采购单推送厂家前调用：

```text
FrameProductProfileService::buildPurchasePayload()
```

只输出允许字段。

### 8.4 Shopify 同步

第一阶段不自动处理 Shopify 上下架。

后续可按以下映射做同步：

| 镜框销售状态 | Shopify 建议 |
| --- | --- |
| 未上架 | 不发布 |
| 正常销售 | 发布 |
| 暂时隐藏 | 下架或隐藏 |
| 停止销售 | 下架 |

该部分需要单独评审，避免供应商状态变化直接影响线上销售。

## 九、权限和数据边界

### 9.1 管理端

- 产品负责人、采购、运营可维护镜框资料。
- 采购可查看厂家推送字段。
- 运营可维护销售状态。
- 供应状态可由采购、运营、供应商回传更新。
- SKU 等级由系统自动计算，管理员可手工重算，不建议人工直接改。

### 9.2 供应商端

供应商只允许：

- 查看分配给自己的供应确认任务。
- 填写供应状态、预计交期、备注。

供应商不允许查看：

- ERP SKU。
- 产品名。
- 原始定价。
- 销售状态。
- 销售数量。
- 销售金额。
- 内部利润和成本结构。

镜框成本是否推给供应商只限采购单场景，供应状态确认任务不展示镜框成本。

## 十、测试方案

### 10.1 单元测试

覆盖：

- 镜片最大宽度计算。
- 镜框必填字段校验。
- 采购推送字段过滤。
- 供应状态更新写日志。
- SKU 等级计算。

### 10.2 接口测试

覆盖：

1. 产品保存镜框字段。
2. 定制订单生成镜框快照。
3. 镜片委外单展示加工参数。
4. 管理端更新供应状态。
5. 供应商提交供应确认任务。
6. SKU 等级手动重算。
7. 任务生成和任务明细提交。

### 10.3 业务回归

重点验证：

- 原产品尺寸 `l/w/h`、包装尺寸不被镜框尺寸覆盖。
- 历史订单快照不受产品资料修改影响。
- 采购推送不会泄露产品名、SKU、原始定价、销售状态。
- 供应商端不能越权查看其他供应商任务。
- D 级 SKU 不生成供应确认任务。

## 十一、实施阶段

### 第一阶段：镜框资料和定制履约参数

交付内容：

1. 新增并维护镜框产品表字段。
2. 产品页支持镜框资料维护。
3. `OpticalCustomFulfillmentBuildService` 写入新快照字段。
4. 镜片委外详情展示镜片高度、中梁宽度、镜片最大宽度。

验收标准：

- 镜框产品可维护所有必填字段。
- 定制订单快照中存在 `lens_height`、`bridge_width`、`lens_max_width`。
- 镜片厂家可看到加工参数。

### 第二阶段：采购推送和供应状态

交付内容：

1. 新增供应状态日志表和 Model。
2. 管理端供应状态更新接口。
3. 供应商端供应状态更新接口。
4. 镜框采购单厂家推送字段预览和校验。

验收标准：

- 采购推送只包含厂家货号、厂家颜色编号、镜框成本。
- 供应状态更新后，产品当前供应状态和历史记录同步更新。

### 第三阶段：SKU 等级计算

交付内容：

1. 新增 SKU 等级日志表和 Model。
2. 新增 `FrameSkuGradeService`。
3. 管理端重算接口。
4. SKU 信息页展示等级。

验收标准：

- 系统能根据近 90 天销量生成 A/B/C/D。
- 当前等级写入 `product.frame_sku_grade`。
- 等级历史可查询。

### 第四阶段：供应商定期回传任务

交付内容：

1. 新增任务表和任务明细表。
2. 管理端生成任务。
3. 供应商端查看和提交任务。
4. 任务完成度统计。
5. 可选定时任务。

验收标准：

- A/B/C 按频率生成任务，D 不生成。
- 供应商提交后更新明细、写供应状态日志、更新产品当前供应状态。

## 十二、风险和处理

### 12.1 镜片高度未确认

风险：没有镜片高度无法稳定推送镜片厂家。

处理：实施前确认镜片高度为必填；如业务不确认，则镜片委外参数允许为空并在供应商端醒目标识。

### 12.2 镜框成本口径不统一

风险：`product.cost` 可能被其他业务当通用产品成本使用。

处理：第一阶段使用 `product.cost`；如发现镜框成本需要与通用成本拆分，再新增 `product.frame_cost` 字段。

### 12.3 SKU 等级规则争议

风险：按销量排名和按阈值会产生不同等级。

处理：第一阶段采用近 90 天销量百分位；同时预留公司配置，后续支持阈值。

### 12.4 供应状态影响线上销售

风险：供应商误填停产后直接下架会影响销售。

处理：供应状态只生成运营建议，不自动覆盖销售状态，不自动同步 Shopify。

## 十三、最终落地口径

镜框主资料直接放在 `product` 表新增字段中。

订单加工参数以 `custom_item_component.params_snapshot` 为准。

供应状态和 SKU 等级属于过程数据，使用专用表留痕。

采购推送厂家只发厂家货号、厂家颜色编号和镜框成本。

供应商回传状态只影响供应状态，不直接修改销售状态。
