AutoRpa/docs/archive/采购主链RPA一期设计文档.md

608 lines
31 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 采购主链 RPA 一期设计文档(技术设计 / HLD+LLD)
> 用途:把《采购主链RPA一期需求文档_三方会审版》(业务规则)和《采购主链RPA一期开发规划》(排期与阶段)落成**可建库、可对接口、可分工**的技术设计底稿。
> 关系说明:本文取代早期《一期开发任务清单_表结构_API草案.md》的**架构与数据模型部分**——该草案工程框架扎实,但建立在"合同文件入池→OCR→映射"的错误骨架上,缺**订单驱动主线、产品对码、三来源分流**。本文保留其状态机/幂等/证据/待办思想,按订单驱动重排实体与节点。
> 术语:状态中心 = 我们自研的编排与审计中台;执行器 = RPA 平台(影刀/来也等);对码 = 电商药品匹配到 M5 货品ID/编码。
---
## 一、设计目标与非目标
### 1.1 设计目标
1. **订单驱动**:以电商订单(医药城/小药药)导出为入口,线下手工单为 OCR 分支,贯穿到 M5 录入待保存。
2. **分层解耦**:业务规则、状态机、审计、对码口径掌握在状态中心;RPA 平台仅做页面执行,可替换。
3. **对码可迭代**:对码表版本化、累积迭代,未命中一对多转人工,命中率可统计。
4. **不可逆保护**:一期末端"保存/签订/生成收货通知单"由人工执行,自动化做成配置开关。
5. **可追溯**:订单号→合同号→返利总单号→M5单号→收货通知单号全链路可查,每节点留证。
### 1.2 非目标(一期不做)
- 招财平台接入。
- M5 自动保存/签订(阶段三才放开,本期只留开关)。
- 加单/拆单的全自动重做(父订单号先记录,处理转人工,待确认)。
- 返利中心重复订单的全自动重建(规则未讲完,先转人工)。
---
## 二、系统架构
### 2.1 分层架构图
```
┌─────────────────────────────────────────────────────────────┐
│ 人工协同层 │
│ 企业微信应用消息/群机器人 + H5待办页(Vue/React) │
│ 待办推送 / 一对多对码选择 / 人工检查入口 / 修改留痕 │
└───────────────▲─────────────────────────────▲───────────────┘
│ 待办/回执 │ OAuth身份
┌───────────────┴─────────────────────────────┴───────────────┐
│ 状态中心(自研,核心资产) │
│ ┌──────────┬──────────┬──────────┬──────────┬────────────┐ │
│ │ 建单入池 │ 状态机 │ 对码引擎 │ 待办路由 │ 审计/证据 │ │
│ │ 幂等去重 │ 编排调度 │ mapping │ 角色分派 │ 事件流 │ │
│ └──────────┴──────────┴──────────┴──────────┴────────────┘ │
│ 统一节点契约 / 回调接口 / 执行平台路由 / 查询审计 API │
└──────▲───────────────▲──────────────────▲───────────────────┘
│ HTTP回调 │ OCR调用 │ 平台路由
┌──────┴──────┐ ┌──────┴──────┐ ┌───────┴─────────────────┐
│ RPA执行器 │ │ OCR服务 │ │ 对象存储(MinIO/NAS) │
│ 影刀/来也 │ │ PaddleOCR │ │ 合同PDF/截图/OCR JSON │
│ 登录/导出/ │ │ (本地优先) │ │ 证据目录 │
│ 建单/导入/ │ └─────────────┘ └─────────────────────────┘
│ 截图/上传 │
└──────▲──────┘
│ 操作
┌──────┴───────────────────────────────────────────────────────┐
│ 外部业务系统(RPA 通过 UI 操作,非 API 集成) │
│ 医药城 │ 小药药 │ 合同系统 contract.gzmpc.com │ 返利中心 │
│ │ │ │ flgzmpc.com │
│ M5(INCA) 采购订单管理 1055 │
└───────────────────────────────────────────────────────────────┘
```
### 2.2 组件职责边界
| 组件 | 职责 | 明确不做 |
| --- | --- | --- |
| **状态中心** | job 生命周期、状态机流转、对码、幂等、待办路由、证据审计、平台路由、统一查询 | 不直接操作外部系统页面 |
| **RPA 执行器** | 登录/导出Excel/建合同/PDF合并上传/返利导入/M5录入/截图/文件读写/HTTP回调 | 不做流程主状态判断、不做业务规则决策 |
| **OCR 服务** | 线下手工单图片→结构化字段 JSON + 置信度 | 仅线下单,线上走 Excel 不经 OCR |
| **企业微信/H5** | 待办通知、一对多选择、人工确认、修改留痕、身份识别 | 不直接写业务结果,须经状态中心标准动作 |
| **对象存储** | 合同PDF、截图、OCR JSON、日志等证据落盘 | — |
**关键设计原则**:RPA 平台"半节点执行过程中不跨平台接力";支持"按新单切换平台""按节点配置平台";切换前跑统一 POC 用例。
### 2.3 技术选型(推荐,可依公司栈调整)
| 层 | 采用 | 备选 |
| --- | --- | --- |
| 状态中心 | Python 3 + FastAPI(开发快、与 OCR/对码脚本同栈、Pydantic 校验回调契约方便) | Java + Spring Boot |
| 数据库 | MySQL 8(状态中心自有库,只存流程状态+累积别名+证据) | PostgreSQL |
| 状态机 | 轻量自实现(NODE_ORDER 顺序驱动 + 7 状态枚举) | — |
| 对象存储 | MinIO / 公司现有 NAS | 本地文件系统 |
| RPA | 影刀 / 来也UiBot / 艺赛旗 择一 POC | — |
| OCR | PaddleOCR 本地部署(无信息安全顾虑) | 阿里/腾讯云OCR |
| 前端 | Vue/React + 企业微信 OAuth | — |
> 注:状态中心确定采用 **Python 3 + FastAPI + MySQL 8**,骨架代码在 `state_center/`(建库脚本、对码引擎、三回调、整单查询已跑通)。若后续团队要平移到 Java+Spring Boot,数据模型与接口契约保持不变即可。
### 2.4 部署拓扑
```
公司内网(与业务系统同网段,便于 RPA 访问)
├── 状态中心服务(容器/虚机) ── MySQL主 ── 对象存储MinIO
├── OCR服务(本地GPU/CPU) [仅阶段2.2线下单启用]
├── RPA运行机(独立虚机, 保持登录态, 定时/7×24)
└── 企业微信应用(内网可信域名 + H5静态站)
```
安全边界:
- RPA 回调接口用 `X-Agent-Token`;内部服务用 `Bearer service-token`;H5 用企业微信身份换 JWT。
- 合同/OCR 证据按角色可见(`visible_roles`),敏感文件脱敏标记 `is_masked`。
- **网络暴露提醒**:状态中心所有写接口须鉴权,回调接口须校验 token + 幂等键,禁止无认证暴露。
---
## 三、数据模型设计
> 按订单驱动重排。核心实体:`job / job_node / order_item / product_match / user_task / mapping_config / executor_binding_config / job_artifact / job_event`。
> 相比早期草案新增:`order_item`(订单明细行)、`product_match`(对码结果,核心难点落库);`job` 主键由"合同文件"改为"订单批次"。
### 3.1 实体关系
```
job (一次流程实例 = 同供应商同日一批订单)
├─1:N─ order_item (订单明细行, 对码前的原始行)
├─1:N─ product_match (对码结果, 命中/未命中/一对多候选)
├─1:N─ job_node (每节点每次尝试)
├─1:N─ user_task (人工待办)
├─1:N─ job_artifact (证据)
└─1:N─ job_event (事件流)
mapping_config (对码表/供应商对照/承付结算映射, 版本化)
executor_binding_config (节点→平台路由)
```
### 3.2 `job`(流程实例)
一次流程实例 = **同一供应商 + 同一订单日期 + 同一来源** 的一批订单(对应一条合同)。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | bigint PK | |
| job_id | varchar(64) unique | 对外流程单号 |
| business_key | varchar(160) | 业务唯一键:`来源+供应商+订单日期`(见§5.2) |
| parent_job_id | varchar(64) null | 重跑/加单来源 |
| source_type | varchar(16) | `YYC`(医药城)/`XYY`(小药药)/`OFFLINE`(线下单) |
| order_date | date | 订单日期(=合同签约日期) |
| supplier_name | varchar(128) | 供应商/商家 |
| order_no_list_json | json | 该批订单号数组(贯穿追溯) |
| contract_template | varchar(16) null | `FORMAT`(格式)/`NON_FORMAT`(非格式承兑) |
| need_pdf_upload | tinyint | 医药城=1,小药药=0 |
| planner_name | varchar(64) null | 计划员 |
| current_node_code | varchar(64) | 当前节点 |
| status | varchar(24) | 见§4.1 |
| mapping_version | varchar(64) null | 绑定的对码表版本 |
| executor_platform | varchar(16) null | 默认执行平台 |
| contract_system_no | varchar(64) null | 合同系统编号 |
| rebate_order_no | varchar(64) null | 返利中心总单号 |
| m5_order_no | varchar(64) null | M5 采购订单号 |
| receipt_notice_no | varchar(64) null | 收货通知单号 |
| current_role | varchar(16) null | 当前责任角色 |
| current_assignee | varchar(64) null | 当前责任人 |
| auto_save_enabled | tinyint | M5自动保存开关(阶段一=0) |
| created_by | varchar(64) | |
| created_at / updated_at / finished_at | datetime | |
索引:`uniq_job_business_open(business_key, status)`、`idx_supplier_date(supplier_name, order_date)`、`idx_status`、`idx_created_at`。
### 3.3 `order_item`(订单明细行,对码前原始数据)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | bigint PK | |
| job_id | varchar(64) | |
| order_no | varchar(64) | 订单号 |
| parent_order_no | varchar(64) null | 父订单号(加单/拆单) |
| raw_product_name | varchar(255) | 平台品名 |
| platform_product_code | varchar(64) null | 平台产品编码 |
| spec | varchar(128) null | 规格(对码痛点:15×2 vs 30片) |
| manufacturer | varchar(255) null | 厂家(含进口英文) |
| approval_no | varchar(64) null | 国药准字/批准文号 |
| qty | decimal(14,2) | 数量 |
| unit_price | decimal(14,4) null | 单价 |
| source_row_no | int | 原始 Excel 行号(回溯) |
| created_at | datetime | |
索引:`idx_job(job_id)`、`idx_order_no`。
### 3.4 `product_match`(对码结果,核心难点落库)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | bigint PK | |
| job_id | varchar(64) | |
| order_item_id | bigint | 对应订单行 |
| match_key | varchar(512) | 四要素拼接键:通用名+批准文号+规格+厂家 |
| match_status | varchar(16) | `EXACT`(精确命中)/`FUZZY`(模糊)/`MULTI`(一对多)/`NONE`(未命中)/`MANUAL`(人工确认) |
| goods_id | varchar(64) null | 命中的 M5 货品ID |
| goods_code | varchar(64) null | 货品编码 |
| base_unit | varchar(32) null | 基本单位 |
| candidates_json | json null | 一对多候选列表(供H5选择) |
| source_add_needed | tinyint | 是否需 M5 新增货源 |
| confirmed_by | varchar(64) null | 人工确认人 |
| confirmed_at | datetime null | |
| mapping_version | varchar(64) | 使用的对码表版本 |
| created_at | datetime | |
索引:`idx_job(job_id)`、`idx_status(match_status)`。
### 3.5 `job_node`(节点执行)
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | bigint PK | |
| job_id | varchar(64) | |
| node_code | varchar(64) | 见§4.2 |
| attempt_no | int | 第几次尝试(不覆盖历史) |
| status | varchar(24) | 节点状态 |
| executor_platform | varchar(16) null | 实际执行平台 |
| executor_run_id | varchar(64) null | 平台侧任务号 |
| started_at / ended_at | datetime null | |
| duration_ms | bigint null | |
| error_code | varchar(64) null | |
| error_summary | varchar(500) null | |
| role_at_failure | varchar(16) null | |
| output_json | json null | 节点输出(业务编号等) |
| callback_idempotency_key | varchar(128) null | 回调幂等键 |
| created_at | datetime | |
索引:`uniq_job_node_attempt(job_id, node_code, attempt_no)`、`idx_status`。
### 3.6 `user_task`(人工待办)
沿用早期草案结构,`reason_type` 扩展订单驱动场景。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | bigint PK | |
| task_id | varchar(64) unique | |
| job_id | varchar(64) | |
| node_code | varchar(64) | |
| role_code | varchar(16) | `PLANNER`/`PURCHASER`/`QC`/`OPS` |
| assignee_user_id | varchar(64) null | |
| title | varchar(255) | |
| reason_type | varchar(32) | `MATCH_MULTI`/`MATCH_NONE`/`SOURCE_ADD`/`OCR_LOW_CONF`/`REBATE_DUP`/`QC_MISSING`/`CONTRACT_DISABLED`/... |
| reason_user_friendly | varchar(500) | 人话说明 |
| why_me_text | varchar(255) | 为什么派给你 |
| editable_fields_json | json | 白名单字段 |
| action_schema_json | json | 允许动作 |
| comparison_snapshot_json | json null | 原值/建议/当前对比 |
| candidates_json | json null | 一对多对码候选 |
| status | varchar(16) | `OPEN`/`SUBMITTED`/`CANCELLED`/`EXPIRED` |
| submit_token | varchar(64) | 防重复 |
| submitted_action | varchar(24) null | |
| submitted_payload_json | json null | |
| submitted_by / submitted_at | | |
| due_at | datetime null | 超时升级 |
| created_at | datetime | |
### 3.7 `mapping_config`(映射/对码,版本化)
统一映射表,`config_type` 区分用途。对码表是其中 `PRODUCT` 类型的累积数据。
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| id | bigint PK | |
| config_type | varchar(24) | `PRODUCT`(对码)/`SUPPLIER`(供应商对照)/`SETTLEMENT`/`PAYMENT_TERM`/`PLANNER_ROUTE` |
| config_key | varchar(512) | 对码时=四要素拼接键 |
| source_value | varchar(512) | 来源值 |
| target_value | varchar(512) | 目标值(对码=货品ID+编码+单位JSON) |
| extra_json | json null | 扩展 |
| version_no | varchar(64) | 版本号 |
| is_active | tinyint | |
| effective_from / effective_to | datetime | |
| updated_by / updated_at | | |
索引:`idx_type_key(config_type, config_key)`、`idx_active(is_active)`。
### 3.8 `executor_binding_config` / `job_artifact` / `job_event`
沿用早期草案结构(见《一期开发任务清单_表结构_API草案.md》§4.5/4.6/4.7)。要点:
- `executor_binding_config`:`node_code → preferred_platform/fallback_platform + required_capabilities_json + route_policy`。
- `job_artifact`:`artifact_type ∈ {SOURCE_EXCEL, SOURCE_IMAGE, OCR_JSON, CONTRACT_PDF, MERGED_PDF, SCREENSHOT, TEMPLATE, LOG}`;证据目录 `job_id/node_code/attempt_no/`。
- `job_event`:`event_type ∈ {JOB_CREATED, NODE_STARTED, NODE_SUCCESS, NODE_FAILED, TASK_CREATED, TASK_SUBMITTED, MANUAL_HANDOFF, JOB_FINISHED}`;`operator_type ∈ {SYSTEM, ROBOT, USER}`。
### 3.9 关键 DDL 片段(新增表)
```sql
CREATE TABLE job (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
job_id VARCHAR(64) NOT NULL UNIQUE,
business_key VARCHAR(160) NOT NULL,
parent_job_id VARCHAR(64) NULL,
source_type VARCHAR(16) NOT NULL, -- YYC/XYY/OFFLINE
order_date DATE NOT NULL,
supplier_name VARCHAR(128) NOT NULL,
order_no_list_json JSON NULL,
contract_template VARCHAR(16) NULL, -- FORMAT/NON_FORMAT
need_pdf_upload TINYINT NOT NULL DEFAULT 0,
planner_name VARCHAR(64) NULL,
current_node_code VARCHAR(64) NOT NULL,
status VARCHAR(24) NOT NULL,
mapping_version VARCHAR(64) NULL,
executor_platform VARCHAR(16) NULL,
contract_system_no VARCHAR(64) NULL,
rebate_order_no VARCHAR(64) NULL,
m5_order_no VARCHAR(64) NULL,
receipt_notice_no VARCHAR(64) NULL,
current_role VARCHAR(16) NULL,
current_assignee VARCHAR(64) NULL,
auto_save_enabled TINYINT NOT NULL DEFAULT 0,
created_by VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
finished_at DATETIME NULL,
KEY idx_supplier_date (supplier_name, order_date),
KEY idx_status (status),
KEY idx_created_at (created_at)
);
CREATE TABLE order_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
job_id VARCHAR(64) NOT NULL,
order_no VARCHAR(64) NOT NULL,
parent_order_no VARCHAR(64) NULL,
raw_product_name VARCHAR(255) NOT NULL,
platform_product_code VARCHAR(64) NULL,
spec VARCHAR(128) NULL,
manufacturer VARCHAR(255) NULL,
approval_no VARCHAR(64) NULL,
qty DECIMAL(14,2) NOT NULL,
unit_price DECIMAL(14,4) NULL,
source_row_no INT NULL,
created_at DATETIME NOT NULL,
KEY idx_job (job_id),
KEY idx_order_no (order_no)
);
CREATE TABLE product_match (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
job_id VARCHAR(64) NOT NULL,
order_item_id BIGINT NOT NULL,
match_key VARCHAR(512) NOT NULL,
match_status VARCHAR(16) NOT NULL, -- EXACT/FUZZY/MULTI/NONE/MANUAL
goods_id VARCHAR(64) NULL,
goods_code VARCHAR(64) NULL,
base_unit VARCHAR(32) NULL,
candidates_json JSON NULL,
source_add_needed TINYINT NOT NULL DEFAULT 0,
confirmed_by VARCHAR(64) NULL,
confirmed_at DATETIME NULL,
mapping_version VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL,
KEY idx_job (job_id),
KEY idx_status (match_status)
);
```
---
## 四、状态机与节点设计
### 4.1 job 主状态机(7 状态)
```
PENDING ──► RUNNING ──► SUCCESS
│ │ ▲
│ │ └──── (重试 attempt_no+1)
│ ├──► WAITING_USER ──► RUNNING (人工处理后继续)
│ ├──► FAILED_RETRYABLE ──► RUNNING
│ └──► FAILED_MANUAL (转人工手工做,如M5质管缺失)
└──────────────► CANCELLED
```
| 状态 | 含义 | 触发 |
| --- | --- | --- |
| PENDING | 已建单待调度 | 建单成功 |
| RUNNING | 节点执行中 | 调度启动节点 |
| WAITING_USER | 等人工(对码未命中/一对多/货源新增/OCR确认) | 节点转人工 |
| SUCCESS | 全链完成(一期到 M5 待保存+人工完成后) | 末节点成功 |
| FAILED_RETRYABLE | 可自动重试(网页卡顿/登录失败) | 系统类错误 |
| FAILED_MANUAL | 停止转人工手工做(M5质管资料缺失) | 业务决策终止 |
| CANCELLED | 取消 | 人工终止 |
**重试机制**:节点失败不覆盖历史,`attempt_no+1` 新建 `job_node` 行;系统类自动重试 1-2 次,超时升级。
### 4.2 节点编码字典(订单驱动重排)
| 顺序 | 节点编码 | 内容 | 转人工场景 | 能力标签 |
| --- | --- | --- | --- | --- |
| 1 | `ORDER_FETCH` | 登录平台,选前一天,导出订单Excel(状态过滤【待确认】) | 登录失败→OPS | desktop_ui, file_download |
| 1b | `OCR_EXTRACT` | 仅线下单:图片→字段 | 低置信度→PLANNER | ocr, http_callback |
| 2 | `PRODUCT_MATCH` | 四要素对码→货品ID/编码 | 未命中/一对多→PLANNER | (状态中心内算) |
| 2b | `SOURCE_ADD` | 未采购过品种→通知M5新增货源 | 缺货源→业务 | (通知) |
| 3 | `CONTRACT_CREATE` | 合同建单,选模板,签约日=订单日;医药城合并PDF上传 | 合同禁掉失效→采购邮件 | desktop_ui, file_upload, pdf_merge |
| 4 | `CONTRACT_APPROVE_WAIT` | 送审后等半小时查审核 | 未审完→等待 | http_callback, timer |
| 5 | `REBATE_HEADER` | 按合同号查总单,填必填项保存 | — | desktop_ui |
| 6 | `REBATE_IMPORT` | 细单模板导入(备注填订单号,特定补偿=0) | 重复编码→采购【待确认】 | excel_template, file_upload |
| 7 | `REBATE_SUBMIT` | 提交送审(决定M5能否带出协议)⭐ | 未送审→阻断 | desktop_ui |
| 8 | `M5_CREATE_ORDER` | M5-1055新增订单+细单,备注填订单号,选正常保管账,**不点保存** | 质管资料缺失→FAILED_MANUAL | desktop_ui |
| 9 | `M5_MANUAL_CONFIRM` | 人工检查→保存→签订(阶段一人工,开关控制) | — | (人工/可配自动) |
| 10 | `M5_RECEIPT_NOTICE` | 生成收货通知单 | — | (人工/可配自动) |
| 11 | `ARCHIVE_JOB` | 归档业务编号链 | — | (系统) |
**关键卡点**:`REBATE_SUBMIT` 未送审则 M5 带不出协议,是硬阻断点。
### 4.3 节点转移规则(示例)
- `PRODUCT_MATCH`:全 EXACT/已确认 → 进 `CONTRACT_CREATE`;有 MULTI/NONE → `WAITING_USER` 建 `user_task`。
- `CONTRACT_APPROVE_WAIT`:定时器半小时后查;未审完保持等待并重查,超阈值升级 OPS。
- `M5_CREATE_ORDER` 报质管错 → 直接 `FAILED_MANUAL`(不重试、不等质管),通知业务手工做。
---
## 五、幂等、追溯与证据设计
### 5.1 幂等三层
1. **入池幂等**:源文件哈希 `source_file_hash` + `business_key`,同批订单重复导入不生成多条未完成 job。
2. **回调幂等**:`job_id + node_code + attempt_no` 唯一键;重复回调按幂等键去重。
3. **人工提交幂等**:`task_id + submit_token`,防止 H5 重复点击重复推进。
4. **执行前检索**:RPA 在返利/M5 建单前先检索已有单号,避免重复建单。
### 5.2 business_key 生成
`business_key = source_type + "|" + supplier_name + "|" + order_date`
(同供应商同日同来源 = 一条合同 = 一个 job)。加单场景父订单号记录在 order_item,重做逻辑待确认。
### 5.3 订单号贯穿追溯
| 位置 | 填什么 | 给谁看 |
| --- | --- | --- |
| 电商平台 | 原始订单号 ZQD... | 源头 |
| 返利中心细单备注 | 订单号 | 物流部软件 |
| M5 订单备注 | 订单号 | 采购结算(打结算勾对账) |
归档链:订单号→合同编号→返利总单号→M5单号→收货通知单号,写入 `job` 各字段 + `job_event`。
### 5.4 证据存储
目录 `对象存储/{job_id}/{node_code}/{attempt_no}/`,含截图、日志、OCR JSON、合同PDF/合并PDF。`job_artifact` 记录元数据、哈希、可见角色、脱敏标记。
---
## 六、接口契约设计
> 所有写操作经状态中心,RPA/H5/企业微信不得直连数据库。鉴权:内部 `Bearer`,RPA `X-Agent-Token`,H5 企业微信身份换 JWT,写接口带 `X-Idempotency-Key`。
### 6.1 统一节点回调契约
节点开始 `POST /api/jobs/{jobId}/nodes/{nodeCode}/start`
```json
{ "attemptNo": 1, "executorPlatform": "YINGDAO", "operator": "agent-01", "startedAt": "2026-08-07T22:00:00+08:00" }
```
节点成功 `POST /api/jobs/{jobId}/nodes/{nodeCode}/complete`
```json
{
"attemptNo": 1,
"executorRunId": "YD-RUN-001",
"endedAt": "2026-08-07T22:03:00+08:00",
"businessRefs": { "contractSystemNo": "HT...", "rebateOrderNo": null, "m5OrderNo": null, "receiptNoticeNo": null },
"summary": { "importedItemCount": 18, "failedItemCount": 0 },
"artifacts": [ { "artifactType": "SCREENSHOT", "storagePath": "/data/jobs/.../success.png" } ]
}
```
约束:节点要求业务编号却未回传,不得记 SUCCESS。
节点失败 `POST /api/jobs/{jobId}/nodes/{nodeCode}/fail`
```json
{
"attemptNo": 1, "errorCode": "REBATE_DUP_CODE", "errorSummary": "存在重复的编码",
"retryable": false, "routeRole": "PURCHASER",
"artifacts": [ { "artifactType": "SCREENSHOT", "storagePath": "/data/jobs/.../error.png" } ]
}
```
### 6.2 错误码分层
| 层 | 前缀 | 处理 | 例 |
| --- | --- | --- | --- |
| 系统类 | `SYS_` | 自动重试1-2次 | SYS_LOGIN_FAIL, SYS_PAGE_TIMEOUT |
| 业务类 | `BIZ_` | 转对应角色 | BIZ_MATCH_NONE, REBATE_DUP_CODE, M5_QC_MISSING |
| 配置类 | `CFG_` | 转OPS/管理员 | CFG_TEMPLATE_MISSING |
| 页面变化类 | `UI_` | 转OPS | UI_ELEMENT_NOT_FOUND |
| 能力不满足 | `CAP_` | 换平台/转OPS | CAP_PDF_MERGE_UNSUPPORTED |
特例:`M5_QC_MISSING` → 不重试,直接 `FAILED_MANUAL`。
### 6.3 待办与查询接口
- `GET /api/tasks/my?roleCode=PLANNER&status=open`
- `POST /api/tasks/{taskId}/submit`:动作 `CONFIRM_AND_CONTINUE`/`SELECT_CANDIDATE`(对码一对多)/`RETRY_CURRENT_NODE`/`TRANSFER_MANUAL`/`TERMINATE_JOB`;返回即时回执 `receiptMessage`/`nextExpectedAction`。
- `POST /api/jobs/{jobId}/retry`、`POST /api/jobs/{jobId}/handoff`
- `GET /api/jobs/{jobId}`:job基础+节点轨迹+待办+对码摘要+业务编号+证据列表。
- `POST /api/wecom/callback`:只接动作/身份,转标准动作后调内部逻辑。
### 6.4 对码专用接口(新增)
- `POST /api/jobs/{jobId}/match/run`:触发对码引擎,返回命中率与未命中清单。
- `GET /api/jobs/{jobId}/match/unmatched`:未命中/一对多候选(供H5)。
- `POST /api/match/{matchId}/confirm`:人工确认选择候选或新增,回写 `mapping_config`(累积迭代)。
---
## 七、对码引擎设计(核心难点)
### 7.1 匹配流程
```
order_item ──► 规范化(去空格/全半角/规格标准化) ──► 拼接 match_key(通用名+批准文号+规格+厂家)
│
├─ 精确命中 mapping_config(PRODUCT) ──► EXACT, 填 goods_id/code
├─ 模糊命中(规格/厂家容错规则) ─────► FUZZY(标记待复核)
├─ 同商品名多规格 ─────────────────► MULTI, 生成 candidates
└─ 无命中 ─────────────────────────► NONE, 建 user_task 转人工
```
### 7.2 规格标准化规则(针对痛点)
- `15×2 ↔ 30片`(乘积展开)、`干一瓶 ↔ ×1瓶`、进口厂家英文名容错(大小写/标点/B.V. S.r.l.)。
- 规则表化,可迭代补充;未覆盖的进 MULTI/NONE 人工。
### 7.3 迭代闭环
人工确认结果回写 `mapping_config`(新版本 `version_no`),下次自动命中。命中率统计写入 `job_event`,支撑阶段0/阶段3的准确率报告。
---
## 八、关键时序图
### 8.1 医药城端到端(正向)
```
定时器 → 状态中心: 触发建单(前一天医药城订单)
状态中心 → RPA: 启动 ORDER_FETCH
RPA → 医药城: 登录/导出Excel
RPA → 状态中心: complete(ORDER_FETCH, artifacts=Excel)
状态中心 → 对码引擎: PRODUCT_MATCH(解析order_item, 匹配)
对码引擎 → 状态中心: 全命中
状态中心 → RPA: 启动 CONTRACT_CREATE(need_pdf_upload=1)
RPA → 合同系统: 建单/选格式模板/改签约日/合并PDF上传/送审
RPA → 状态中心: complete(合同编号)
状态中心: 定时器等30分钟 → RPA: CONTRACT_APPROVE_WAIT 查审核
状态中心 → RPA: REBATE_HEADER(按合同号查总单填必填)
状态中心 → RPA: REBATE_IMPORT(细单模板,备注=订单号,特定补偿0)
状态中心 → RPA: REBATE_SUBMIT(送审) ⭐
状态中心 → RPA: M5_CREATE_ORDER(新增订单+细单,备注订单号,正常保管账,不保存)
RPA → 状态中心: complete(M5待保存)
状态中心 → 企业微信: 推送人工检查待办
计划员 → H5: 检查→保存→签订→生成收货通知单(阶段一人工)
状态中心: ARCHIVE_JOB 归档编号链
```
### 8.2 对码未命中(一对多)
```
对码引擎 → 状态中心: MATCH_MULTI(candidates)
状态中心 → user_task: 建待办(reason=MATCH_MULTI, candidates_json)
状态中心 → 企业微信: 推送"对码待确认"
计划员 → H5: 选择候选/新增
H5 → 状态中心: submit(SELECT_CANDIDATE)
状态中心 → mapping_config: 回写(累积迭代)
状态中心: job WAITING_USER → RUNNING, 继续 CONTRACT_CREATE
```
### 8.3 M5 质管资料缺失(转人工,不等质管)
```
RPA → M5: 新增订单保存时报错(供应商年报过期)
RPA → 状态中心: fail(M5_QC_MISSING, retryable=false)
状态中心: job → FAILED_MANUAL (不重试/不等质管)
状态中心 → 企业微信: 通知业务"此单转人工手工做"
业务: 次日质管维护后自行在M5手工完成
```
---
## 九、模块设计(状态中心内部)
| 模块 | 职责 | 关键接口/服务 |
| --- | --- | --- |
| intake_service | 建单入池、哈希/业务键去重、Excel解析入 order_item | POST /api/jobs |
| workflow_service | 状态机流转、节点调度、转移规则 | node start/complete/fail |
| match_service | 对码引擎、规格标准化、候选生成、迭代回写 | match/run, match/confirm |
| retry_service | 系统类自动重试、超时扫描升级 | 定时任务 |
| task_service | 待办生成、角色路由、提交幂等、回执 | tasks/my, tasks/submit |
| executor_router | 节点→平台路由(FIXED/PREFERRED/FALLBACK) | 内部 |
| ocr_service | 线下单OCR适配、置信度阈值 | OCR_EXTRACT |
| artifact_service | 证据落盘、可见角色、脱敏 | 内部 |
| wecom_service | 企业微信消息、卡片更新、身份 | wecom/callback |
| audit/query | 事件流、整单查询、审计导出 | GET /api/jobs/{id} |
---
## 十、与早期草案的设计差异(供评审)
| # | 早期草案 | 本设计 | 原因 |
| --- | --- | --- | --- |
| 1 | job=一份合同文件的流程 | job=同供应商同日一批订单 | 订单驱动,合同是产物非入口 |
| 2 | 无 order_item/product_match 表 | 新增两表 | 对码是核心难点须落库 |
| 3 | 节点以合同/OCR为主线 | 节点订单驱动重排(ORDER_FETCH起) | 骨架方向纠正 |
| 4 | 送审在合同系统 | 关键送审 REBATE_SUBMIT | 卡点位置纠正 |
| 5 | 质管缺料转质管重试 | M5_QC_MISSING→FAILED_MANUAL | 业务明确否决重试 |
| 6 | 无自动化开关 | job.auto_save_enabled 分阶段 | 分阶段上线诉求 |
| 7 | 技术栈未定 | 确定 Python+FastAPI+MySQL,骨架已跑通 | 与对码/OCR同栈,开发快 |
| 8 | OCR为全流程核心 | OCR仅线下单分支 | 范围纠正 |
---
## 十一、待确认的设计输入(阻塞项)
1. **OCR信息安全**:线上是否获批 / 本地部署预算 → 决定 `OCR_EXTRACT` 节点部署方式。
2. **返利中心重复订单**完整规则 → 决定 `REBATE_DUP_CODE` 是否可自动重建总单。
3. **医药城订单状态过滤**:只取"待收货"还是含其他 → `ORDER_FETCH` 过滤条件。
4. **加单场景**:父订单号关联与重做逻辑 → `parent_job_id/parent_order_no` 处理策略。
5. **测试账号/对码表(Oracle连接串+表名列名)/供应商对照表/邮箱/计划员表**到位时间 → 影响联调。
> 技术栈已确认:状态中心 = Python 3 + FastAPI + MySQL 8(骨架见 `state_center/`),不再作为阻塞项。
---
## 附录:联调顺序建议
1. 状态中心单独跑通建单/状态流转/待办。
2. 假OCR+假RPA回调打通全流程。
3. 接真实对码引擎(复用阶段0成果)。
4. 接真实企业微信+H5身份。
5. 接真实RPA(先医药城单来源)。
6. 返利中心/M5端到端联调。