forked from erp-dev/erp
10 KiB
10 KiB
外部 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
同步主规则
- 按
BianHaoID对本次拉到的 records 分组 - 每个分组 upsert 一条
PrintingOrder - 分组内每条 record 按
BianHaoID + YanSe覆盖 upsert 一条PrintingJob - 仅当本地全部写入成功后,才推进外部 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 |
原样保存 |
HpName |
布料名 | PrintingOrder.fabric |
原样保存到订单面料字段 |
area |
地区/区域 | PrintingOrder.area |
原样保存;取不到时回退为空字符串 "" |
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 |
原样保存 |
FidJ |
电脑位置 | PrintingOrder.position |
取分组首条 record 的原始值 |
BeiZhu |
滚筒注意事项 | PrintingOrder.rolling_warn |
原样保存 |
KdRiQi |
外部开单日期时间 | PrintingOrder.outgoing_date / PrintingOrder.kd_riqi |
按中国时间解析并保存;即使外部字符串带 Z,也按本地业务时间解释;kd_riqi 用于 API 展示、筛选和排序 |
| 分组首条原始数据 | 外部来源快照 | PrintingOrder.external_raw |
保存 order 级别的原始数据,便于追溯 |
补充说明:
- 外部
HpName已作为布料名使用,直接写入PrintingOrder.fabric - 外部
area直接写入PrintingOrder.area;若外部未返回或为空,则统一写入空字符串 BianHaoKD、RiQi等当前未单独属性化的字段,保留在external_raw中BianHaoKD当前样例值类似"1.27",不按日期强解析- 外部
KdRiQi样例可能形如2026-07-08T17:22:22Z,但业务语义为中国时间2026-07-08 17:22:22,不能按 UTC 解释,否则 API 展示会偏移 8 小时
历史 kd_riqi 回填命令
新增字段 PrintingOrder.kd_riqi 后,新同步数据会自动写入该字段。历史数据的 KdRiQi 已保存在 external_raw.first_record.KdRiQi,可用以下 management command 批量回填。
命令文件:
printing/management/commands/backfill_printing_order_kd_riqi.py
默认处理范围:
- 只处理
PrintingOrder.kd_riqi IS NULL的订单 - 从
external_raw.first_record.KdRiQi提取并解析时间 - 解析成功则批量写入
kd_riqi - 缺失或解析失败的数据跳过并计数
- 不覆盖已有
kd_riqi - 使用
bulk_update(['kd_riqi']),不会更新updated_at
历史错误值修正模式:
- 传
--overwrite后,不再限定kd_riqi IS NULL - 所有存在
external_raw且能读取external_raw.first_record.KdRiQi的订单都会重新解析并覆盖kd_riqi - 传
--update-outgoing-date后,会同时覆盖outgoing_date - 该模式用于修正早期把外部
KdRiQi误按 UTC 解析导致的 8 小时偏移
推荐先 dry-run:
docker compose exec -T -e DB_HOST=postgres -e DB_PORT=5432 web uv run python manage.py backfill_printing_order_kd_riqi --dry-run
确认统计后批量执行:
docker compose exec -T -e DB_HOST=postgres -e DB_PORT=5432 web uv run python manage.py backfill_printing_order_kd_riqi
常用参数:
--dry-run:只统计,不写入--batch-size 1000:批量读取和批量写入大小,默认1000--limit 5000:最多处理多少条候选记录--merchant-id 1:限定商户--external-order-id KD20453713:限定外部订单编号,适合单条验证--overwrite:覆盖已有kd_riqi,用于修正历史按 UTC 解析导致的偏移数据--update-outgoing-date:同时用KdRiQi修正outgoing_date
输出为 JSON,典型字段:
{
"dry_run": true,
"matched": 18000,
"updated": 0,
"would_update": 17800,
"skipped_missing": 150,
"skipped_invalid": 50,
"overwrite": false,
"update_outgoing_date": false
}
历史偏移数据修正建议先 dry-run:
docker compose exec -T -e DB_HOST=postgres -e DB_PORT=5432 web uv run python manage.py backfill_printing_order_kd_riqi --overwrite --update-outgoing-date --dry-run
确认后执行:
docker compose exec -T -e DB_HOST=postgres -e DB_PORT=5432 web uv run python manage.py backfill_printing_order_kd_riqi --overwrite --update-outgoing-date
二、单条 record -> PrintingJob
| 外部字段 | 含义 | 本系统字段 | 规则 |
|---|---|---|---|
ID |
明细顺序 ID | PrintingJob.original_id |
保存最近一次覆盖该任务的外部明细 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 |
补充说明:
PrintingJob的覆盖唯一性按PrintingOrder.external_order_id + YanSe处理- 若同一外部订单下再次出现相同
YanSe,则更新已有PrintingJob,不再新增 - 更新时
original_id会覆盖为最新外部 record 的ID
员工绑定规则
- 读取外部
CaoZY - 在同步用户所属商户下按
Employee.name精确匹配 - 若匹配到的
Employee.sys_user存在:PrintingOrder.created_by = employee.sys_userPrintingOrder.external_employee_name = ""
- 若未匹配到,或员工未绑定系统用户:
PrintingOrder.created_by = settings.PRINTING_EXTERNAL_SYNC_USER_IDPrintingOrder.external_employee_name = 外部原文
客户绑定规则
- 读取
customer.KhName - 在同步用户所属商户下按
Customer.name精确匹配 - 取第一条结果作为
PrintingOrder.customer - 同时保留:
external_customer_idexternal_customer_name
保护规则:
- 若本地找不到客户,则本次同步失败,不推进外部 cursor
- 原因:
PrintingOrder.customer在业务上不可空,且当前未授权把客户兜底到固定客户
产品绑定与图片规则
- 读取
YanSe - 在同步用户所属商户下按
Product.name精确匹配 - 匹配成功:
PrintingJob.product = 匹配到的产品- 不请求外部图片 API
PrintingJob.external_product_name = ""
- 匹配失败:
- 先创建本地
Product - 再调用外部图片 API:
GET /api/v1/image?name_b64=<base64url(YanSe)>
- 将返回的 base64 图片内容写入
Product.image - 由 Django storage 自动上传到七牛
- 成功后
PrintingJob.product = 新建产品 PrintingJob.external_product_name = ""
- 先创建本地
- 若产品创建或图片拉取/上传失败:
- 该条外部
record.ID记为失败 - 该 record 不创建
PrintingJob - 本批次其它成功 records 继续处理
- cursor 仍然推进
- 该条外部
外部接口与游标策略
外部配置写入 settings.py,不经 .env:
PRINTING_EXTERNAL_RECORDS_BASE_URLPRINTING_EXTERNAL_RECORDS_AUTHORIZATIONPRINTING_EXTERNAL_SYNC_USER_IDPRINTING_EXTERNAL_SYNC_PRODUCT_CATEGORY_ID
游标策略:
- 拉取 records 时显式使用
update_cursor=false - 本地成功 records / 失败 records 都处理完成后,调用
/api/v1/cursor/set推进到last_record_id - 失败 records 需单独落库,便于后续按外部
record.ID补偿
调度策略
Celery Beat 每 5 分钟触发一次。
单次请求固定:
limit=100- 不传范围参数
即依赖外部 cursor 做增量拉取。
本次新增字段计划
PrintingOrder
external_order_idexternal_customer_idexternal_customer_nameexternal_employee_nameexternal_raw
PrintingJob
external_product_nameexternal_raw
API v1 失败记录
PrintingExternalSyncFailureexternal_record_idexternal_order_idproduct_nameerrorraw
已接受的业务风险
BianHaoID是否跨外部系统唯一,不由本系统负责纠偏- 客户按名称取第一条,存在误绑风险,但这是本期接受的业务策略
- 产品若本地不存在,则依赖“创建产品 + 拉图上传”流程;该流程失败时只影响对应
record.ID