Skip to content

02 — 事项推送(HMAC)

第三方将待办 / 流程知会等写入门户事项台账(portal_matter*)。
用户侧入口:/api/portal/matters/**不是通知公告收件箱)。

广播类平台公告请改走 07;选型见 08

代码锚点:OpenMatterControllerOpenMatterHmacFilterOpenMatter*Request

基路径/api/open/matters
方法:全部为 POST(含删除语义的 /delete)。


1. 鉴权头

Header必填说明
X-Client-IdSSO client_id
X-TimestampUnix (epoch seconds)
X-SignatureHMAC-SHA256 的 Hex(大小写不敏感比对)
X-Idempotency-Key记入 portal_matter_push_log,便于对账

时间窗:±300 秒。超出返回 code=401「X-Timestamp 超出允许时间窗」。

Client 须:存在、启用、绑定 system_code、CONFIDENTIAL 且可解析 client_secret。否则 401/403


2. 签名算法

签名串(三个部分用 换行符 \n 拼接,含请求体原文):

text
{clientId}\n{timestamp}\n{rawBody}
规则
算法HmacSHA256,密钥为 client_secret 的 UTF-8 字节
输出Hex 字符串 → 放入 X-Signature
rawBodyHTTP 请求体原始字节按 UTF-8 解码后的字符串;须与实际上送 JSON 字节完全一致(空格、字段顺序都会影响验签)
timestampX-Timestamp同一字符串

伪代码(Java / Hutool 风格,与门户实现一致):

java
String payload = clientId + "\n" + timestamp + "\n" + rawBody;
String signature = new HMac(HmacAlgorithm.HmacSHA256, secret.getBytes(UTF_8)).digestHex(payload);

Node.js 示意:

js
const crypto = require('crypto');
const payload = `${clientId}\n${timestamp}\n${rawBody}`;
const signature = crypto.createHmac('sha256', clientSecret).update(payload, 'utf8').digest('hex');

3. 接口清单

方法路径语义
POST/api/open/matters/upsertsystem_code + externalId 创建或更新事项及参与人
POST/api/open/matters/status更新办理态 / 第三方原文态(不做迁移边强制校验)
POST/api/open/matters/reopen重开;默认 bizStatus=REOPENED
POST/api/open/matters/delete按绑定 system_code + externalId 软删

成功响应 data

字段说明
matterId门户事项雪花 ID
externalId对方业务主键

仅落库成功才 code=0,失败应重试(可选配合 X-Idempotency-Key)。

system_code 始终取自 Client 绑定,不要在 body 里依赖自带 systemCode。


4. UPSERT — POST /api/open/matters/upsert

请求字段

字段必填说明
matterTypeTODO / NOTICE / ALERT / REMIND
externalIdTODO 场景必填对方业务主键;与绑定 system_code 组成唯一键
title标题
summary摘要
content正文
pcUrl / mobileUrl跳转 URL
bizType业务类型
priority优先级
bizStatus门户标准办理态,如 PENDING(合法枚举见门户事项约定)
deadlineAt截止时间(ISO-8601 本地日期时间)
initiatorExtId / initiatorUsername发起人
externalStatusCode / externalStatusLabel第三方原文态
sourceUpdatedAt乱序控制:对方更新时间;若比库中更旧,内容更新可能被忽略但仍返回现有 ID
payloadJson扩展 JSON 字符串
replacePartiestrue 时整表替换参与人
parties参与人列表

parties[]

字段必填说明
username门户登录名(须已存在)
partyRoleASSIGNEE / CC / RECIPIENT

curl 示例

bash
CLIENT_ID="demo-client"
SECRET="your-client-secret"
TS=$(date +%s)
BODY='{"matterType":"TODO","externalId":"WF-1001","title":"请假审批","bizStatus":"PENDING","parties":[{"username":"zhangsan","partyRole":"ASSIGNEE"}]}'
SIG=$(node -e "const c=require('crypto');const b=process.argv[1],id=process.argv[2],ts=process.argv[3],s=process.argv[4];console.log(c.createHmac('sha256',s).update(id+'\n'+ts+'\n'+b).digest('hex'))" "$BODY" "$CLIENT_ID" "$TS" "$SECRET")

curl -sS -X POST "https://portal.example.com/api/open/matters/upsert" \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: $CLIENT_ID" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -H "X-Idempotency-Key: upsert-WF-1001-1" \
  -d "$BODY"

5. STATUS — POST /api/open/matters/status

字段必填说明
externalId对方业务主键
bizStatus空表示不改门户标准态
externalStatusCode / externalStatusLabel第三方原文态(建议带 label)
sourceUpdatedAt乱序控制
resetAssigneeUnread是否重置承办人未读,默认 false

6. REOPEN — POST /api/open/matters/reopen

字段必填说明
externalId对方业务主键
bizStatus默认 REOPENED
externalStatusCode / externalStatusLabel第三方原文态
sourceUpdatedAt乱序控制
resetAssigneeUnread默认 true

7. DELETE — POST /api/open/matters/delete

软删除;不是 HTTP DELETE 方法。

字段必填说明
externalId对方业务主键

事项须已存在,否则 404「事项不存在: {systemCode}/{externalId}」。

bash
# 签名方式同 UPSERT;body 示例:
# {"externalId":"WF-1001"}
curl -sS -X POST "https://portal.example.com/api/open/matters/delete" \
  -H "Content-Type: application/json" \
  -H "X-Client-Id: $CLIENT_ID" \
  -H "X-Timestamp: $TS" \
  -H "X-Signature: $SIG" \
  -d '{"externalId":"WF-1001"}'

8. 错误与联调提示

现象排查
缺少 HMAC 鉴权头三个头是否齐全、是否被网关剥离
HMAC 签名校验失败body 是否被重新序列化;换行是否为 \n;secret 是否最新
Client 无可用 client_secret是否 PUBLIC 或未生成 secret
Client 未绑定 system_code管理端补绑应用编码
成功但内容未变检查 sourceUpdatedAt 是否比库中旧(乱序忽略)

设计背景见 doc/16-待办与站内通知.md;公共约定见 01-接入准备.md


9. 下载 Demo