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:
2026-09-30 09:02:52 +08:00
parent ad86e2d521
commit bb91515590
12 changed files with 2221 additions and 1 deletions
+34 -1
View File
@@ -32,8 +32,19 @@
<view v-if="error" class="verify__error">{{ error }}</view>
<!--
核验失败时给一条**受控通道**(FR-509 / 客户确认后新增)。
注意两件事:
1) 这条通道**不绕过门禁** —— 申请期间仍然不能填,去申请页会说清这一点;
2) 它只在真失败过之后才出现(`error` 非空),不是一进来就摆个"申请"按钮。
否则会有学生绕过核验直接去申请,把管理员的待办当成人工客服。
-->
<button v-if="error" class="verify__btn verify__btn--ghost" @click="gotoJoin">
申请加入名单
</button>
<view class="verify__foot">
核对不上?先确认姓名和证件号没有打错。仍然不行请联系班主任。
核对不上?先确认姓名和证件号没有打错。仍然不行可点上面的「申请加入名单」。
</view>
</view>
@@ -214,6 +225,20 @@ function next() {
})
}
/** 去申请加入名单(FR-509 的入口)—— 带上已填的姓名与证件号,省他再输一遍。 */
function gotoJoin() {
const s = sceneFromUrl()
const params = new URLSearchParams({
scene: s,
taskId: String(ctx.value.taskId || 0),
// 只把**已经打过的字**带过去。这不是"预填答案":门禁刚判定它不匹配,
// 带过去是为了让他少打一遍,而不是暗示它是对的。
name: realName.value.trim(),
idCardNo: idCardNo.value.trim()
})
uni.navigateTo({ url: `/pages/join/join?${params.toString()}` })
}
onLoad((options) => {
loadContext(options?.scene || '')
})
@@ -318,6 +343,14 @@ page {
color: #0000004d;
background: #e8e8e8;
}
/* 次要动作("申请加入名单"):主按钮才是"核对并继续",
两个实心按钮并排会让人分不清哪个是推荐路径 */
&--ghost {
margin-top: 20rpx;
color: $primary;
background: #eaf1fd;
}
}
&__error {