forked from erp-dev/erp
feat: haobuye
This commit is contained in:
170
docs/printing_external_records_sync_2026-03-20.md
Normal file
170
docs/printing_external_records_sync_2026-03-20.md
Normal 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`
|
||||
Reference in New Issue
Block a user