dieya_risk_prediction_backend/docs/外部系统风险评估报告接口.md

25 KiB
Raw Blame History

外部系统风险评估报告接口

1. 文档说明

本文档定义外部业务系统调用蝶雅风险预警平台生成新品风险评估 Markdown 报告的接口协议。

外部接口代码已实现。启用前需执行数据库迁移、配置系统调用密钥并开通本地钉钉用户;测试和正式地址由部署方提供。本接口与内部 POST /api/v1/risk-predictions/preview 分离,不依赖内部用户 JWT 或聊天会话。

1.1 接口用途

外部系统提交产品、包装、供应商和开发阶段等风险评估参数,蝶雅平台完成以下处理:

  1. 校验调用方系统身份和请求签名。
  2. 根据钉钉用户标识,或管理员预先批准的外部账号映射,匹配蝶雅平台用户。
  3. 使用蝶雅平台风险数据库、标准库和评分模型完成风险评估。
  4. 生成 Markdown 格式的风险评估报告并直接返回。

1.2 当前数据边界

外部系统与蝶雅平台使用不同数据库时,蝶雅平台默认只使用自己的历史异常案例、采购订单、供应商主数据和标准规则库。外部系统数据库中的历史数据不会自动参与评分;如需使用,双方另行约定数据同步或 externalEvidence 扩展协议。

2. 接口信息

项目 内容
请求方式 POST
请求路径 /api/v1/integrations/risk-assessments/report
请求类型 application/json
成功响应类型 text/markdown; charset=utf-8
字符编码 UTF-8
接口超时时间建议 120 秒
幂等依据 sourceSystem + requestId;Idempotency-Key 必须与 requestId 一致

生产环境完整地址由蝶雅平台提供,例如:

https://risk.example.com/api/v1/integrations/risk-assessments/report

3. 调用鉴权

接口使用“调用方系统身份 + HMAC-SHA256 签名”鉴权。外部系统不使用蝶雅平台用户 JWT,也不直接传入蝶雅平台本地 userId。

3.1 请求头

请求头 必填 说明
Content-Type 是 固定为 application/json
X-Client-Id 是 蝶雅平台分配的调用方系统编号,例如 dieya-plm
X-Timestamp 是 Unix 时间戳,单位为秒
X-Nonce 是 8~64 位随机字符串,仅允许字母、数字、下划线、短横线,单次请求唯一
X-Signature 是 HMAC-SHA256 签名,固定使用标准 Base64 编码(带填充 =)
Idempotency-Key 是 与请求体中的 requestId 保持一致

3.2 签名计算

蝶雅平台为每个 X-Client-Id 分配独立密钥 clientSecret。POST 调用使用以下原文计算签名:

X-Timestamp + "\n" + X-Nonce + "\n" + 原始请求Body

伪代码:

message = f"{timestamp}\n{nonce}\n{raw_body}"
signature = base64.b64encode(
    hmac.new(client_secret.encode("utf-8"), message.encode("utf-8"), hashlib.sha256).digest()
).decode("ascii")

注意事项:

  • 签名必须基于发送时的原始 JSON 字节内容计算,不能先格式化后再计算。
  • 服务端默认只接受时间误差不超过 5 分钟的请求。
  • 已认证请求的 X-Nonce 在数据库中保留 10 分钟,跨进程和重启后仍拒绝重复使用;正常 JSON 的字段校验失败也会消耗 Nonce。
  • clientSecret 只通过双方约定的安全渠道保存,不放在前端代码、日志或请求体中。
  • 生产环境建议叠加 IP 白名单;高安全场景可使用 mTLS。

3.3 用户身份映射

请求中的用户身份使用钉钉标识:

  • dingtalkUnionId:首选跨系统关联标识。
  • dingtalkUserId:钉钉用户 ID,作为辅助校验和追踪字段。
  • employeeNo、name:展示和审计信息,不能单独作为身份依据。

蝶雅平台按以下顺序处理:

  1. 按 dingtalkUnionId 查找本地用户。
  2. 必要时结合 dingtalkUserId 校验用户身份。
  3. 用户不存在、停用或 UnionId 匹配到多个本地用户时返回 403,不自动创建账号。
  4. 评分、审计和报告记录使用蝶雅平台本地用户 ID;该 ID 不返回给外部系统作为跨库主键。

用户需要拥有本地 risk.assess 操作权限(管理员直接允许)。如传入 dingtalkUserId,必须与本地用户一致。报告中的姓名使用本地用户资料,不使用调用方提交的姓名作为认证或显示依据。查询报告时再次校验原操作人的账号状态及权限。

3.4 非钉钉操作人

没有钉钉身份的用户可使用 operator.externalUserId,前提是风险平台管理员已为当前调用系统批准映射。两种身份模式必须且只能选一种;外部账号模式不传 dingtalkUnionId、dingtalkUserId,可以省略或传 null。不允许按姓名、员工编号自动匹配用户。

例如 PLM 账号 superadmin(周子墨)的操作人参数为:

{
  "externalUserId": "superadmin",
  "name": "周子墨"
}

这个对象替换请求中的整个 operator,其他字段及系统签名要求不变。PLM 后端必须从当前已登录用户取出该账号标识,不能让前端任意指定别人的 externalUserId。平台通过 X-Client-Id + externalUserId 查映射并检查本地账号及权限,未批准映射返回 403。外部系统的管理员角色不授予风险平台管理员权限。

GET 查询时也会重新校验记录中的外部账号映射;撤销映射或停用本地账号后禁止查询。

如果两个钉钉应用返回的 UnionId 不一致,双方需要先确定统一的钉钉身份源或增加统一身份映射表。

4. 请求参数

4.1 请求体

{
  "requestId": "PLM-D-FT-003-0001",
  "sourceSystem": "dieya-plm",
  "operator": {
    "dingtalkUnionId": "ding_union_xxx",
    "dingtalkUserId": "ding_user_xxx",
    "employeeNo": "A10001",
    "name": "张三"
  },
  "assessment": {
    "subjectId": "SKU-20261007-001",
    "productName": "新品洗发水",
    "productCategory": "洗发水/洗护类",
    "specification": "500ml",
    "materialType": "PET瓶/泵头",
    "supplier": "某某供应商",
    "factory": "某某加工厂",
    "stageNew": "产品开发",
    "defectLevel": null,
    "description": "新品上市前风险评估",
    "asOfDate": "2026-10-07"
  }
}

4.2 顶层字段

字段 类型 必填 说明 校验建议
requestId string 是 调用方请求唯一编号,用于幂等 1~128 个字符,仅允许字母、数字、_、.、:、-
sourceSystem string 是 来源系统编号 必须是已登记的 X-Client-Id
operator object 是 发起评估的钉钉用户 见下表
assessment object 是 风险评估数据 见下表

4.3 operator 字段

字段 类型 必填 说明
dingtalkUnionId string/null 条件必填 钉钉用户模式必填,与 externalUserId 二选一
externalUserId string/null 条件必填 非钉钉用户的调用方稳定账号标识,1~128 字符,必须在当前调用系统下预先批准映射
dingtalkUserId string 建议 钉钉用户 ID
employeeNo string 否 员工编号,用于审计展示
name string 否 用户姓名,用于报告展示

4.4 assessment 字段

字段 类型 必填 说明
subjectId string 是 产品、SKU 或评估对象编号
productName string 否 产品名称
productCategory string 是 按当前风险评分的八类传值,完整清单及兼容规则见 4.6
specification string 否 规格或容量,例如 500ml、100g
materialType string 是 包装材质或物料组合,例如 PET瓶/泵头
supplier string 否 供应商名称
factory string 否 加工厂名称
stageNew string 否 开发阶段
defectLevel string/null 否 缺陷等级,可取 A、B、C
description string 否 评估背景或补充说明
asOfDate string/null 否 评估数据截止日期,格式 YYYY-MM-DD,不早于 1900-01-01、不允许未来日期;省略时使用首次受理的上海日期

stageNew 建议使用以下枚举值:

需求发现
产品立项
产品开发
量产上市
产品生命管理

4.5 字段约束

  • productCategory、materialType、subjectId 缺失时,接口返回 422。
  • description 仅作为评估背景,不直接作为已确认的检测结果或历史事实。
  • 调用方不要提交最终风险分数、风险等级或模型版本;这些字段由蝶雅平台统一计算。
  • 当前版本不接收外部数据库连接信息、SQL、文件路径或可执行脚本。
  • 可选字段允许省略或传 null。stageNew、defectLevel 非空时必须符合枚举;未知字段(包括调用方提交的分数、角色)返回 422。
  • 参数长度:subjectId 128、productName 200、productCategory 100、specification 100、materialType 200、supplier/factory 300、description 2000;dingtalkUnionId 64、dingtalkUserId 50、employeeNo 64、name 50。请求体最大 64KB。

4.6 产品品类字典(当前评分口径)

本次 PLM 对接采用现有七维风险评分服务 normalize_category() 的八类口径。以下为当前评分分类清单,并非数据库字典接口的返回结果。

PLM 优先将产品品类映射成下表的标准值后提交 assessment.productCategory,并保存映射关系。无法确定具体类别时,不应仅凭 PLM 的宽泛业务大类强行映射。

标准值(建议提交值) 当前兼容识别关键词
洗发水/洗护类 洗发、洗护、洗发水、洗发露、洗发乳
沐浴露类 沐浴、沐浴露
面霜/护肤类 面霜、乳液、精华、面膜、护肤
牙膏/口腔护理类 牙膏、口腔
清洁剂类 清洁剂、清洁、洗洁
脱毛类 脱毛
彩妆/唇部类 彩妆、口红、粉底、眼影
香氛/止汗类 香氛、香水、止汗

匹配规则及评分边界:

  • 当前按上表顺序进行关键词子串匹配,多个类别同时命中时取第一类。
  • 空值仍按本接口必填规则返回 422。非空值无法识别时,按现有评分逻辑将“产品品类”维度设为默认 50 分,并在报告中注明无法归类;这不代表产品风险已经由历史数据验证。
  • 已识别品类的历史异常案例少于 10 条时,该维度按 45 分处理;其余情况按现有异常数量归一化规则计算。
  • 当前 防晒、洁面、护发素 等名称本身没有专门的评分匹配规则;包含其他兼容关键词的完整名称仍按现有规则归类,例如“防晒乳液”命中“乳液”。
  • 个护清洁 会命中“清洁”并归为“清洁剂类”,不能将该 PLM 大类整体作为防晒等产品的评分分类。
  • 报告应同时展示调用方提交的品类和实际评分归类;无法识别时注明默认分及原因。

历史案例的品类维度统计优先归一化案例的 product_category,无法识别时再尝试案例的 product_name。本对接口径采用评分规则;看板中的“身体护理类”“洁面/卸妆类”“头发护理类”“包材/物料专项”“待补标品类”不属于当前八类评分字典。

5. 成功响应

5.1 响应头

HTTP/1.1 200 OK
Content-Type: text/markdown; charset=utf-8
X-Request-Id: PLM-D-FT-003-0001
X-Assessment-Id: 10086
X-Model-Version: database-seven-dimension-v1
X-Risk-Score: 72.50
X-Risk-Level: moderately_low

5.2 响应体

响应体直接为 Markdown 文本,不再包裹 data、code 等 JSON 字段:

# 新品风险评估报告

## 一、评估信息

| 项目 | 内容 |
|---|---|
| 评估对象 | 新品洗发水 |
| 对象编号 | SKU-20261007-001 |
| 产品品类 | 洗发水/洗护类 |
| 规格 | 500ml |
| 包装材质 | PET瓶/泵头 |
| 供应商 | 某某供应商 |
| 加工厂 | 某某加工厂 |
| 开发阶段 | 产品开发 |
| 评估人 | 张三 |
| 评估时间 | 2026-10-07 |

## 二、综合风险结论

- 综合得分:72.50
- 风险等级:较低风险
- 模型版本:database-seven-dimension-v1

## 三、七维风险评分

| 风险维度 | 得分 | 权重 | 数据状态 | 说明 |
|---|---:|---:|---|---|
| 供应商表现 | 80.00 | 25% | database | 近三年采购订单和异常案例统计 |
| 物料品类 | 75.00 | 20% | database | 包装材质历史异常统计 |
| 品类历史 | 70.00 | 15% | database | 产品品类历史案例统计 |

## 四、相似异常案例

暂无直接匹配案例,建议结合相似品类进行验证。

## 五、标准规则

| 风险点 | 判定规则 | 来源 | 状态 |
|---|---|---|---|
| 密封性 | 按标准要求完成密封性验证 | 标准文件.xlsx | 待检测 |

## 六、风险预防建议

1. 打样阶段完成内容物与包装材料相容性测试。
2. 增加密封性和加速老化测试。
3. 首单加强来料检验和过程巡检。

## 七、数据口径说明

本报告基于蝶雅风险平台本地数据库中的历史案例、采购订单和标准库生成。
部分维度无实际数据时,按系统配置的中性值计算。

报告中的分数是安全得分,分数越高代表风险越低。风险等级阈值以蝶雅平台当前模型配置为准,调用方不要自行重新计算等级。

X-Risk-Score 为 0~100 的安全分,保留两位小数。X-Risk-Level 为 low(低风险)、moderately_low(较低风险)、medium(中等风险)、high(高风险)、very_high(极高风险)。X-Assessment-Id 按字符串存储。GET 和幂等重试返回原报告及相同结论响应头。

6. 幂等规则

requestId 是调用方生成的业务请求唯一编号,同一个 sourceSystem + requestId 只能对应一次评估请求。

首次请求

返回 200 和 Markdown 报告。

相同请求重复提交

如果请求体和原请求一致,服务端返回同一份已生成报告,仍返回 200,并在响应头中带回原始 X-Assessment-Id。

同一编号提交不同内容

如果 requestId 已经使用,但请求内容摘要不同,返回 409,避免覆盖历史报告。

处理中、失败及重试

  • 幂等比较基于校验后的 JSON 对象摘要,忽略 JSON 键顺序和排版,省略的可选字段与 null 等价;变更任意有效字段视为不同请求。
  • 重试保留原请求体(包括操作人和 asOfDate),重新生成时间戳、Nonce 和签名。主动重新评估必须使用新 requestId。
  • 同一请求仍在生成时,POST 或 GET 返回 202 JSON 和 Retry-After: 5,不会重复计算。
  • 每次生成最多 110 秒(可配置为 10~110 秒),超时或失败记录为 failed,返回 503;相同请求内容可以重新 POST 重试。生成权租约为 150 秒,进程异常退出后租约到期可重新 POST 接管。
  • 调用方网络超时并不代表服务端已经停止。应先 GET 查询,再决定是否使用原请求重试。
  • 省略 asOfDate 时首次受理日期单独持久化,跨天重试仍沿用首次日期。

处理中响应示例:

{"code":202,"message":"报告正在生成,请稍后查询","data":{"requestId":"PLM-D-FT-003-0001"}}

报告查询

GET /api/v1/integrations/risk-assessments/report?requestId=PLM-D-FT-003-0001

查询参数必须且只能包含一个 requestId;GET 不携带 Body,也不要求 Idempotency-Key。仅能查询与 X-Client-Id 相同来源的记录。成功返回 200 Markdown,处理中返回 202,失败返回 503,不存在返回 404。

GET 签名原文为:

"GET" + "\n" + 请求路径 + "\n" + 原始查询字符串 + "\n" + X-Timestamp + "\n" + X-Nonce + "\n"

请求路径包含 /api/v1,不含域名、查询字符串;原始查询字符串不含 ?,包括实际发送的 URL 编码,签名后不得重新编码。网关不能重写对外接口路径。以 UTF-8 字节计算 HMAC-SHA256,再以标准 Base64 编码。报告持久化保存,当前未启用自动清理策略。

7. 错误响应

错误响应统一使用 JSON:

{
  "code": 422,
  "message": "产品品类不能为空",
  "data": null
}
HTTP 状态码 code 场景 调用方处理建议
400 400 请求格式错误或签名原文不合法 修正请求格式后重试
401 401 缺少签名、签名错误、时间戳过期 检查系统密钥和签名算法
403 403 调用方未开通接口或用户无风险评估权限 联系平台管理员开通权限
404 404 当前调用系统未找到该请求 核对 requestId 和调用系统
409 409 requestId 重复但内容不一致 更换请求编号或查询原请求
422 422 参数缺失或字段值不符合约束 修正参数后重试
429 429 超过调用频率限制 按 Retry-After 延迟重试
500 500 平台内部处理异常 使用同一 requestId 重试并联系平台方
503 503 风险数据库、模型或依赖服务暂不可用 稍后重试,不要生成新的业务编号

8. 调用示例

8.1 cURL

curl -X POST 'https://risk.example.com/api/v1/integrations/risk-assessments/report' \
  -H 'Content-Type: application/json' \
  -H 'X-Client-Id: dieya-plm' \
  -H 'X-Timestamp: 1791331200' \
  -H 'X-Nonce: 8f3d4c1a9e' \
  -H 'X-Signature: Base64HmacSha256Value' \
  -H 'Idempotency-Key: PLM-D-FT-003-0001' \
  --data-raw '{
    "requestId": "PLM-D-FT-003-0001",
    "sourceSystem": "dieya-plm",
    "operator": {
      "dingtalkUnionId": "ding_union_xxx",
      "dingtalkUserId": "ding_user_xxx",
      "employeeNo": "A10001",
      "name": "张三"
    },
    "assessment": {
      "subjectId": "SKU-20261007-001",
      "productName": "新品洗发水",
      "productCategory": "洗发水/洗护类",
      "specification": "500ml",
      "materialType": "PET瓶/泵头",
      "supplier": "某某供应商",
      "factory": "某某加工厂",
      "stageNew": "产品开发",
      "description": "新品上市前风险评估",
      "asOfDate": "2026-10-07"
    }
  }' \
  --output risk-report.md

8.2 Python 签名示例

import base64
import hashlib
import hmac
import json
import secrets
import time

body = {
    "requestId": "PLM-D-FT-003-0001",
    "sourceSystem": "dieya-plm",
    "operator": {"dingtalkUnionId": "ding_union_xxx"},
    "assessment": {
        "subjectId": "SKU-20261007-001",
        "productCategory": "洗发水/洗护类",
        "materialType": "PET瓶/泵头",
    },
}
raw_body = json.dumps(body, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
timestamp = str(int(time.time()))
nonce = secrets.token_urlsafe(16)
message = timestamp.encode() + b"\n" + nonce.encode() + b"\n" + raw_body
signature = base64.b64encode(
    hmac.new(client_secret.encode(), message, hashlib.sha256).digest()
).decode()

调用方必须使用计算签名时的同一份 raw_body 发送请求。

9. 评分和报告处理说明

平台内部处理逻辑建议统一复用现有七维风险评分服务:

  • 供应商表现
  • 物料品类
  • 品类历史
  • 加工厂质量
  • 标准符合性
  • 节点阶段
  • 缺陷严重度

平台会在报告中标注数据状态。没有真实数据库数据的维度不得伪造历史结论,按模型配置的中性值处理并在报告中说明。

外部接口使用结构化参数直接评分,不再通过聊天语义提取参数。沿用现有评分区间起点(截止年份往前 3 年的 1 月 1 日),区间终点为 asOfDate,不包含截止日期之后的案例及采购订单。相似案例使用现有检索服务的文本和业务字段排序,输入同一统计区间内的案例;本接口不调用向量嵌入服务。

包装组合沿用现有单标签评分口径,例如 PET 瓶身 + PP 泵头 命中“泵头”,不代表分别评估了所有组件。报告展示原文及评分标签。缺陷维度来自历史 A/B/C 记录,不预测新品实际缺陷等级。

AI 只用于生成预防建议,等待最多 20 秒。AI 不可用时仍返回数据库评分及固定验证建议,并在报告中标注;数据库评分失败则返回 503,不生成模拟分数。

报告建议包含:

  1. 评估信息。
  2. 综合风险得分和风险等级。
  3. 七维评分明细。
  4. 品类、供应商和物料历史统计。
  5. 相似异常案例。
  6. 标准规则匹配结果。
  7. 风险预防建议。
  8. 数据口径和模型版本。

10. 联调前需要双方确认的事项

  1. 正式接口域名、环境地址和接口版本。
  2. sourceSystem、X-Client-Id 和密钥分配方式。
  3. 钉钉 unionId 是否可以作为双方统一用户标识;如果不能,需要提供统一身份映射方案。
  4. 操作人提前开通、UnionId 唯一映射及 risk.assess 权限;本接口已采用不存在返回 403 的策略。
  5. 是否启用 IP 白名单、mTLS 和调用频率限制。
  6. stageNew、defectLevel 枚举,以及 PLM 到第 4.6 节八类评分品类的映射关系;确认无法归类时的默认分提示。
  7. 外部系统历史数据是否需要同步到蝶雅平台。
  8. Markdown 报告的保存、下载和展示方式。
  9. 超时、重试和 requestId 查询机制。

11. 服务端启用配置

先执行迁移:

python -m alembic upgrade head

新增表为 integration_nonces 和 integration_assessments,分别保存已认证随机串及请求快照、处理状态、评分结果和 Markdown 报告;成功评估同时写入操作审计。现有用户表不自动添加外部用户。报告记录通过本地用户编号逻辑关联,不对已有用户表创建外键,迁移账号不需要该表的 REFERENCES 权限;用户状态和权限仍由接口校验。

在服务端环境配置中设置:

RISK_INTEGRATION_CLIENTS={"dieya-plm":"replace-with-at-least-32-random-characters"}
RISK_INTEGRATION_REQUESTS_PER_MINUTE=60
RISK_INTEGRATION_REPORT_TIMEOUT_SECONDS=110

上面的密钥仅为占位,须替换为至少 32 个字符的随机密钥;测试、正式各使用一套。调用系统编号仅允许字母、数字、下划线和短横线,最长 64 位。默认 {} 不允许任何系统调用。配置后重启服务。

也可以在后端项目根目录使用启用工具生成密钥,工具会保留 .env 中的其他设置及已有系统密钥,不打印密钥、不自动轮换已有密钥:

.\.venv\Scripts\python.exe scripts/setup_external_risk.py configure --client-id dieya-plm

让操作人在钉钉工作台中登录风险平台,正常免登会同步用户资料和 UnionId。然后使用 PLM 保存的真实 UnionId 检查映射与权限:

.\.venv\Scripts\python.exe scripts/setup_external_risk.py check-user --union-id "PLM操作人的真实UnionId" --dingtalk-user-id "同一人的真实UserId"

工具只检查、不修改账号或权限。不存在时先通过钉钉免登开通;已停用或无权限时由账号管理员确认处理;映射不唯一或身份不一致时核对两边钉钉身份源,不能按姓名猜测绑定。当前启用的 admin、product_manager、reviewer、user 角色均有风险评估操作权限,以本地 role_permissions 表实际记录为准。

限流按调用系统统计已通过签名的请求,POST、GET、幂等重试共用滚动 60 秒窗口;达到阈值返回 429 和 Retry-After: 60。IP 白名单和 mTLS 可在网关配置,当前应用层不提供这两项配置。

12. 联调调用工具

非钉钉用户需由风险平台管理员在本地显式开通,例如:

.\.venv\Scripts\python.exe scripts/setup_external_risk.py allow-user --client-id dieya-plm --external-user-id superadmin --employee-no plm_superadmin --name "周子墨"

工具创建无密码、无钉钉绑定的普通本地账号,并写入 .env:

RISK_INTEGRATION_OPERATOR_MAPPINGS='{"dieya-plm":{"superadmin":"plm_superadmin"}}'

映射目标是本地 emp_no。默认 {} 禁止所有非钉钉账号;已有映射不会自动指向其他账号,已有账号不会自动更改角色、姓名或启用状态。配置后重启后端,无需数据库迁移。

项目提供 scripts/call_external_risk_report.py,自动生成 POST/GET 签名并保存 Markdown。调用方将本系统分配的密钥写入环境变量 RISK_INTEGRATION_CLIENT_SECRET(客户端单个密钥,与服务端 JSON 密钥表是不同配置)。assessment.json 使用本文第 4.1 节请求结构,填入已开通的真实 UnionId。

python scripts/call_external_risk_report.py --url https://risk.example.com/api/v1/integrations/risk-assessments/report --client-id dieya-plm --body-file assessment.json --output risk-report.md

查询原请求:

python scripts/call_external_risk_report.py --url https://risk.example.com/api/v1/integrations/risk-assessments/report --client-id dieya-plm --request-id PLM-D-FT-003-0001 --output risk-report.md

只有 200 会保存报告;202/错误响应会展示 JSON 和重试等待时间,不会覆盖已有 Markdown。