feat(open): §14 开放接口(V16)—— HMAC 签名对接认定机构
密钥存 AES 密文(HMAC 要能还原,不能只存哈希);签名 = HMAC-SHA256 + 5 分钟时间窗 + nonce 防重放,先验签再占 nonce;数据接口一律不返回明文证件号,文件走 30 分钟签名 URL; 推送重试的 next_retry_at 落库(内存定时器重启后就没了),推送挂在 afterCommit 上。 契约漏掉的接入方管理(F-67)一并补上;规则模板两个接口修回契约路径(F-68)。 验证:e2e-m8-open 60/60。
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
-- ============================================================================
|
||||
-- V16:开放接口(供认定机构对接,契约 §14)
|
||||
--
|
||||
-- 一、`collect_open_app`:接入方(认定机构)
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 鉴权是 `appKey + timestamp + nonce + HMAC-SHA256(appSecret, appKey+ts+nonce+body)`。
|
||||
-- ⚠️ 所以 `appSecret` **必须能被服务端还原**(要拿它算 HMAC),不能只存哈希。
|
||||
-- 做法:用现成的 `SensitiveCodec`(AES)加密后存 `app_secret_cipher` ——
|
||||
-- 与身份证号同一套处理:库里不是明文,但服务端能解出来用。
|
||||
-- 接口**永不返回** appSecret(只在创建时给一次)。
|
||||
--
|
||||
-- `scopes` 用逗号串(`submission:read,file:read`)而不是 JSON:
|
||||
-- 它是个短小的固定枚举集合,逗号串能直接被 SQL 的 `LIKE` 与代码的 `split` 用,
|
||||
-- 而 JSON 每次都要序列化/反序列化(这个字段在**每次请求**的鉴权路径上)。
|
||||
--
|
||||
-- 二、`collect_open_push_log`:推送记录与重试
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 契约 §14.4 写明重试策略:**失败重试 5 次,间隔 1/5/30/120/600 秒**;
|
||||
-- 全部失败后标记并告警,**支持人工重推**。
|
||||
-- 要能做到"间隔 1 秒后重试"而又不阻塞请求线程,就必须把"下次什么时候重试"
|
||||
-- 落到库里(`next_retry_at`),由定时任务扫 —— 内存里的定时器在重启后就没了,
|
||||
-- 而"审核通过后机构没收到"这种事不能因为一次重启就永远丢掉。
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collect_open_app (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
app_key VARCHAR(64) NOT NULL,
|
||||
app_secret_cipher VARCHAR(256) NOT NULL,
|
||||
app_name VARCHAR(128) NOT NULL,
|
||||
scopes VARCHAR(256) NOT NULL DEFAULT 'submission:read,file:read',
|
||||
-- 我方主动推送时的目标地址与它自己的验签密钥(契约 §14.4 是我方 → 机构)
|
||||
callback_url VARCHAR(512),
|
||||
push_secret_cipher VARCHAR(256),
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
remark VARCHAR(256),
|
||||
create_by BIGINT,
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by BIGINT,
|
||||
update_time TIMESTAMP,
|
||||
deleted SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
COMMENT ON TABLE collect_open_app IS '开放接口接入方(契约 §14):鉴权用 appKey + HMAC-SHA256(appSecret, appKey+ts+nonce+body)';
|
||||
COMMENT ON COLUMN collect_open_app.app_secret_cipher IS 'AES 密文:HMAC 要用明文密钥,所以**不能只存哈希**;接口永不返回它';
|
||||
COMMENT ON COLUMN collect_open_app.scopes IS '逗号分隔的权限集合(submission:read / file:read)。用逗号串而不是 JSON:它在每次请求的鉴权路径上';
|
||||
COMMENT ON COLUMN collect_open_app.push_secret_cipher IS '推送给我方时的签名密钥(机构侧校验用),同样是密文';
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_open_app_key ON collect_open_app (app_key) WHERE deleted = 0;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS collect_open_push_log (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL,
|
||||
app_id BIGINT NOT NULL,
|
||||
task_id BIGINT,
|
||||
submission_id BIGINT NOT NULL,
|
||||
event_type VARCHAR(32) NOT NULL DEFAULT 'SUBMISSION_APPROVED',
|
||||
status SMALLINT NOT NULL DEFAULT 0,
|
||||
attempts SMALLINT NOT NULL DEFAULT 0,
|
||||
last_http_code INTEGER,
|
||||
last_error VARCHAR(512),
|
||||
payload_json TEXT,
|
||||
next_retry_at TIMESTAMP,
|
||||
pushed_time TIMESTAMP,
|
||||
create_by BIGINT,
|
||||
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by BIGINT,
|
||||
update_time TIMESTAMP,
|
||||
deleted SMALLINT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
COMMENT ON TABLE collect_open_push_log IS '推送记录(契约 §14.4):失败重试 5 次,间隔 1/5/30/120/600 秒;全失败后标记并支持人工重推';
|
||||
COMMENT ON COLUMN collect_open_push_log.status IS '0 待推 / 1 成功 / 2 失败待重试 / 3 最终失败(需人工重推)';
|
||||
COMMENT ON COLUMN collect_open_push_log.next_retry_at IS '下次重试时间。**落库而不是放内存**:内存定时器在重启后就没了,而"审核通过后机构没收到"不能因为一次重启就永远丢掉';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_push_retry ON collect_open_push_log (status, next_retry_at)
|
||||
WHERE deleted = 0;
|
||||
-- 同一个(接入方, 填报单)只留一条主记录:人工重推与自动重试都更新它,
|
||||
-- 否则"这家机构到底收到没有"会有多个互相矛盾的答案
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_push_once ON collect_open_push_log (
|
||||
app_id, submission_id, event_type
|
||||
) WHERE deleted = 0;
|
||||
Reference in New Issue
Block a user