1
0
forked from erp-dev/erp

feat: haobuye

This commit is contained in:
2026-03-20 20:00:12 +08:00
parent 84dbb2ee10
commit 96cc67706a
10 changed files with 1162 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
# 外部 records 同步到 PrintingOrder / PrintingJob 设计
日期2026-03-20
## 目标
将外部服务 `GET http://43.139.183.222:18080/api/v1/records` 返回的增量 records
按每 5 分钟一次的频率同步到本系统:
- 一组相同 `BianHaoID` 的 records -> 1 条 `printing.PrintingOrder`
- 每条 record -> 1 条 `printing.PrintingJob`
本设计文档用于固化本次实现的字段映射和兜底规则,后续若业务修正,以此为追溯基线。
## 外部 records 样例判断
外部 `records` 不是“订单主表”,而是“印花明细表”。
判断依据:
- 同一个 `BianHaoID` 下会出现多条 record
- 每条 record 含数量、尺寸、单位、产品名等明细信息
- 因此单条 record 更接近本系统的 `PrintingJob`
## 同步主规则
1.`BianHaoID` 对本次拉到的 records 分组
2. 每个分组 upsert 一条 `PrintingOrder`
3. 分组内每条 record 按 `ID` upsert 一条 `PrintingJob`
4. 仅当本地全部写入成功后,才推进外部 cursor
## 字段映射
### 一、外部分组 -> PrintingOrder
| 外部字段 | 含义 | 本系统字段 | 规则 |
| --- | --- | --- | --- |
| `BianHaoID` | 订单编号 | `PrintingOrder.external_order_id` | 分组键;原样保存 |
| `customer.KhName` | 客户名 | `PrintingOrder.customer` | 按本商户 `Customer.name` 精确匹配,取第一条 |
| `KhID` | 外部客户 ID | `PrintingOrder.external_customer_id` | 原样保存 |
| `customer.KhName` | 外部客户名 | `PrintingOrder.external_customer_name` | 原样保存 |
| `CaoZY` | 外部操作员/业务员 | `PrintingOrder.created_by` / `PrintingOrder.external_employee_name` | 若能匹配到内部员工且员工绑定了系统用户,则 `created_by` 取该用户,`external_employee_name` 置空;否则 `created_by` 取固定同步用户,`external_employee_name` 保留原文 |
| `SHDZ` | 布料来源 + 工艺 | `PrintingOrder.fabric_source` / `PrintingOrder.craft` | 以空白字符 split第 1 段为 `fabric_source`,剩余重新拼接为 `craft` |
| `SeHao` | 幅宽 | `PrintingOrder.width` | 原样保存 |
| `MeoA` | 曲线 | `PrintingOrder.curve` | 原样保存 |
| `BeiZhu` | 滚筒注意事项 | `PrintingOrder.rolling_warn` | 原样保存 |
| `KdRiQi` | 下单日期 | `PrintingOrder.outgoing_date` | 按 ISO 时间解析并保存 |
| 分组首条原始数据 | 外部来源快照 | `PrintingOrder.external_raw` | 保存 order 级别的原始数据,便于追溯 |
补充说明:
- 外部没有可靠的 “面料” 字段,因此 `PrintingOrder.fabric` 本次同步固定写空字符串 `""`
- `BianHaoKD``RiQi``FidJ` 等当前未单独属性化的字段,保留在 `external_raw`
- `BianHaoKD` 当前样例值类似 `"1.27"`,不按日期强解析
### 二、单条 record -> PrintingJob
| 外部字段 | 含义 | 本系统字段 | 规则 |
| --- | --- | --- | --- |
| `ID` | 明细顺序 ID | `PrintingJob.original_id` | 幂等键;原样保存 |
| `YanSe` | 外部产品名 | `PrintingJob.product` / `PrintingJob.external_product_name` | 若能按本商户 `Product.name` 精确匹配,直接绑定该产品且不再拉图;若本地不存在,则先创建产品、拉图并上传到 storage成功后绑定该产品 |
| `ShuLiang` | 下单数量 | `PrintingJob.quantity` | 按整数解析;当前预期外部值为整数字符串或 `10.00` 这类可安全转整数的值 |
| `JiJiaDW` | 单位 | `PrintingJob.unit` | 去掉前导 `/` 后保存,例如 `/段` -> `段` |
| `ShuLiangZ` | 一段尺寸 | `PrintingJob.size` | 原样保存 |
| `BeiZhuC` | 件数 | `PrintingJob.pieces` | 提取其中的整数,例如 `10件` -> `10` |
| `YanSe` | 外部产品名 | `PrintingJob.description` | 本次不单独映射到 description原始值由 `product.name``external_product_name` / `external_raw` 表达 |
| 单条原始数据 | 外部来源快照 | `PrintingJob.external_raw` | 保存 job 级别完整原始 record |
## 员工绑定规则
1. 读取外部 `CaoZY`
2. 在同步用户所属商户下按 `Employee.name` 精确匹配
3. 若匹配到的 `Employee.sys_user` 存在:
- `PrintingOrder.created_by = employee.sys_user`
- `PrintingOrder.external_employee_name = ""`
4. 若未匹配到,或员工未绑定系统用户:
- `PrintingOrder.created_by = settings.PRINTING_EXTERNAL_SYNC_USER_ID`
- `PrintingOrder.external_employee_name = 外部原文`
## 客户绑定规则
1. 读取 `customer.KhName`
2. 在同步用户所属商户下按 `Customer.name` 精确匹配
3. 取第一条结果作为 `PrintingOrder.customer`
4. 同时保留:
- `external_customer_id`
- `external_customer_name`
保护规则:
- 若本地找不到客户,则本次同步失败,不推进外部 cursor
- 原因:`PrintingOrder.customer` 在业务上不可空,且当前未授权把客户兜底到固定客户
## 产品绑定与图片规则
1. 读取 `YanSe`
2. 在同步用户所属商户下按 `Product.name` 精确匹配
3. 匹配成功:
- `PrintingJob.product = 匹配到的产品`
- 不请求外部图片 API
- `PrintingJob.external_product_name = ""`
4. 匹配失败:
- 先创建本地 `Product`
- 再调用外部图片 API
- `GET /api/v1/image?name_b64=<base64url(YanSe)>`
- 将返回的 base64 图片内容写入 `Product.image`
- 由 Django storage 自动上传到七牛
- 成功后 `PrintingJob.product = 新建产品`
- `PrintingJob.external_product_name = ""`
5. 若产品创建或图片拉取/上传失败:
- 该条外部 `record.ID` 记为失败
- 该 record 不创建 `PrintingJob`
- 本批次其它成功 records 继续处理
- cursor 仍然推进
## 外部接口与游标策略
外部配置写入 `settings.py`,不经 `.env`
- `PRINTING_EXTERNAL_RECORDS_BASE_URL`
- `PRINTING_EXTERNAL_RECORDS_AUTHORIZATION`
- `PRINTING_EXTERNAL_SYNC_USER_ID`
- `PRINTING_EXTERNAL_SYNC_PRODUCT_CATEGORY_ID`
游标策略:
1. 拉取 records 时显式使用 `update_cursor=false`
2. 本地成功 records / 失败 records 都处理完成后,调用 `/api/v1/cursor/set` 推进到 `last_record_id`
3. 失败 records 需单独落库,便于后续按外部 `record.ID` 补偿
## 调度策略
Celery Beat 每 5 分钟触发一次。
单次请求固定:
- `limit=100`
- 不传范围参数
即依赖外部 cursor 做增量拉取。
## 本次新增字段计划
### PrintingOrder
- `external_order_id`
- `external_customer_id`
- `external_customer_name`
- `external_employee_name`
- `external_raw`
### PrintingJob
- `external_product_name`
- `external_raw`
### API v1 失败记录
- `PrintingExternalSyncFailure`
- `external_record_id`
- `external_order_id`
- `product_name`
- `error`
- `raw`
## 已接受的业务风险
1. `BianHaoID` 是否跨外部系统唯一,不由本系统负责纠偏
2. 客户按名称取第一条,存在误绑风险,但这是本期接受的业务策略
3. 产品若本地不存在,则依赖“创建产品 + 拉图上传”流程;该流程失败时只影响对应 `record.ID`