1
0
forked from erp-dev/erp
Files
erpnew/docs/printing_external_records_sync_2026-03-20.md

10 KiB
Raw Blame History

外部 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 按 BianHaoID + YanSe 覆盖 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 原样保存
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;若外部未返回或为空,则统一写入空字符串
  • BianHaoKDRiQi 等当前未单独属性化的字段,保留在 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.nameexternal_product_name / external_raw 表达
单条原始数据 外部来源快照 PrintingJob.external_raw 保存 job 级别完整原始 record

补充说明:

  • PrintingJob 的覆盖唯一性按 PrintingOrder.external_order_id + YanSe 处理
  • 若同一外部订单下再次出现相同 YanSe,则更新已有 PrintingJob,不再新增
  • 更新时 original_id 会覆盖为最新外部 record 的 ID

员工绑定规则

  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