feat(join): B1 名单加入申请 —— 学生提交 + 管理端审批(V7 存 AES 密文)
按契约「服务端转 HMAC、不落明文」设计会写不进名册:collect_roster.id_card_no 是 NOT NULL 的密文列,而申请表只有 hash 与 mask(三条替代路都被排除)。 V7 给 collect_join_request 补 id_card_cipher(与名册同一把密钥/算法),审批时原样搬进名册, 明文全程只在提交请求的内存里存在过。V7 之前的老申请明确提示重新提交,不静默建一条 永远过不了校验的名册行。 另修 F-20:自述班级不能借用姓名规范化(姓名要「张 三」→「张三」,班级名里的空格有意义)。 验证:scripts/e2e-m2-join.py 50/50。
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
-- ============================================================================
|
||||
-- V7:加入申请补一列「证件号密文」
|
||||
--
|
||||
-- 为什么需要它(F-19,本轮发现)
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 审批通过时要往 `collect_roster` 插一行,而那一行的 `id_card_no` 是
|
||||
-- **NOT NULL**(AES-GCM 密文,见列注释"不可用于等值查询")。
|
||||
--
|
||||
-- 原设计(契约 §8.7.4)只说"服务端转 HMAC,不落明文",于是申请表里
|
||||
-- **只有 hash 与 mask,没有密文** → 审批到写名册这一步必然
|
||||
-- `null value in column "id_card_no" violates not-null constraint`(500)。
|
||||
--
|
||||
-- 三条路都不行:
|
||||
-- · 让申请人先过门禁再建名册 —— 那就变成"先有鸡还是先有蛋"(门禁靠名册判人);
|
||||
-- · 让管理员在审批时重新输入证件号 —— 他手里只有学生自述的号码,
|
||||
-- 重新输入等于把"学生写错一位"的风险复制到管理员身上;
|
||||
-- · 干脆不写这一列 —— DDL 有 NOT NULL,而且审核要看完整号时正是解的这一列。
|
||||
--
|
||||
-- 所以补一列 **AES-GCM 密文**(不是明文):
|
||||
-- `id_card_cipher` —— 与 `collect_roster.id_card_no` **同一把密钥、同一个算法**,
|
||||
-- 审批时原样搬过去即可,明文全程只在提交请求的内存里存在过。
|
||||
-- 它与 hash 的分工不变:hash 用于等值查重,cipher 用于"需要时解密"。
|
||||
--
|
||||
-- ⚠️ 可空是刻意的:本迁移之前提交的申请没有这一列的值,
|
||||
-- 审批时会明确提示"这条申请缺少证件号密文,请让管理员核对后重新提交"
|
||||
-- (**不能**静默建一条没有证件号的名册行 —— 那条行以后永远过不了名册校验)。
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE collect_join_request
|
||||
ADD COLUMN IF NOT EXISTS id_card_cipher VARCHAR(256);
|
||||
|
||||
COMMENT ON COLUMN collect_join_request.id_card_cipher IS
|
||||
'证件号 AES-GCM 密文(与 collect_roster.id_card_no 同算法同密钥);审批通过时原样写入名册,避免管理员重输';
|
||||
Reference in New Issue
Block a user