主题
02 — 事项推送(HMAC)
第三方将待办 / 流程知会等写入门户事项台账(portal_matter*)。
用户侧入口:/api/portal/matters/**(不是通知公告收件箱)。
代码锚点:OpenMatterController、OpenMatterHmacFilter、OpenMatter*Request。
基路径:/api/open/matters
方法:全部为 POST(含删除语义的 /delete)。
1. 鉴权头
| Header | 必填 | 说明 |
|---|---|---|
X-Client-Id | 是 | SSO client_id |
X-Timestamp | 是 | Unix 秒(epoch seconds) |
X-Signature | 是 | HMAC-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 |
rawBody | HTTP 请求体原始字节按 UTF-8 解码后的字符串;须与实际上送 JSON 字节完全一致(空格、字段顺序都会影响验签) |
timestamp | 与 X-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/upsert | 按 system_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
请求字段
| 字段 | 必填 | 说明 |
|---|---|---|
matterType | 是 | TODO / NOTICE / ALERT / REMIND |
externalId | TODO 场景必填 | 对方业务主键;与绑定 system_code 组成唯一键 |
title | 是 | 标题 |
summary | 否 | 摘要 |
content | 否 | 正文 |
pcUrl / mobileUrl | 否 | 跳转 URL |
bizType | 否 | 业务类型 |
priority | 否 | 优先级 |
bizStatus | 否 | 门户标准办理态,如 PENDING(合法枚举见门户事项约定) |
deadlineAt | 否 | 截止时间(ISO-8601 本地日期时间) |
initiatorExtId / initiatorUsername | 否 | 发起人 |
externalStatusCode / externalStatusLabel | 否 | 第三方原文态 |
sourceUpdatedAt | 否 | 乱序控制:对方更新时间;若比库中更旧,内容更新可能被忽略但仍返回现有 ID |
payloadJson | 否 | 扩展 JSON 字符串 |
replaceParties | 否 | true 时整表替换参与人 |
parties | 否 | 参与人列表 |
parties[]
| 字段 | 必填 | 说明 |
|---|---|---|
username | 是 | 门户登录名(须已存在) |
partyRole | 是 | ASSIGNEE / 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
- Java HMAC:/downloads/hmac-java.zip(
MatterUpsertDemo) - Bash:/downloads/bash-hmac-scripts.zip(
matter-upsert.sh) - 说明:sdk/README.md