docs: 同步交接摘要 / PRD / 原型,并记录品牌改名「深学在线」

- 交接摘要补 §一·三十五~§一·三十八:角色与授权、需求收敛(4 角色 / 10 菜单 / 取消二期)、
  PC 端重搭后的 UI 修复与卡片式基线、品牌改名与新标识
- PRD 与功能逻辑卷只改标题行与产品代号线;**文件名不动** —— verify-docs.py 按路径读,
  全库还有 20+ 处按文件名引用,改名会连带炸脚本
- 原型 index / admin-web / teacher-app / student-app 的品牌区改「深学在线」,
  admin-web 的 .mark(10 处)换成内嵌的新标 data URI,保持原型"单文件自包含"的约定
- 根 AGENTS.md 与 frontend-pc 的规范同步:接线检查补第 ⑨ 项(SVG 资产是不是合法 XML ——
  SVG 注释里出现连续两个减号就 not well-formed,图标裂开而控制台一句话都不报)、
  「最容易写错的约定」6 -> 7 条(新增品牌标)
- deploy/docker-compose.yml 只改头注释里的项目名(compose 的 name 与七牛对象前缀都没动)
This commit is contained in:
2026-10-10 16:49:04 +08:00
parent 1f3c485ceb
commit 7df078e0ea
22 changed files with 1158 additions and 1093 deletions
+13 -100
View File
@@ -212,7 +212,7 @@ FUNCTION normalize(rawName, rosterItem, materialDef, template):
expected=mask(roster.idCardNo),
actual=owner.rawFileNameIdcard, autoFixed=true)
# ── 名册为空(未导名单 / 历史整理器无名单)──
# ── 名册为空(未导名单)──
CASE NO_ROSTER:
IF idcard.found:
name = pick_name_like(tokens) OR UNKNOWN_NAME
@@ -346,9 +346,9 @@ FUNCTION find_idcard_in(text):
| 空文件名 / 纯扩展名 | 全部字段用名册兜底;无名册则 `FAIL` 但放行 |
| 超长文件名(>200 字符) | 截断,**优先保留证件号与材料名**,截姓名部分 |
| 文件名全是乱码 | 尝试 UTF-8→GBK 转码;仍失败则用名册全兜底 |
| 无名册(历史整理器无名单) | `keepOriginalIfNoRoster=true`:只做格式清理,**不猜姓名与证件号** |
| 无名册(任务未导名单) | `keepOriginalIfNoRoster=true`:只做格式清理,**不猜姓名与证件号** |
| 文件名里的证件号指向名册中他人 | **阻断**(VAL-21),不做任何自动修正 |
| 学生申诉"这就是我的材料" | 不是技术问题——**走「申请加入名单」通道**(FR-509)由管理员核实并入册;系统内不展示联系方式 |
| 学生申诉"这就是我的材料" | 不是技术问题——**由老师在任务里核实并手工加人/改材料**(原「申请加入名单」通道已于 2026-10 撤销);系统内不展示联系方式 |
| 模板变量全部为空 | 返回 `FAIL`,材料名用槽位名,其余用 `UNKNOWN_*` |
| 同一文件重复处理 | 幂等:比对 `file_hash`,已处理且输入未变则直接返回缓存结果 |
@@ -670,11 +670,11 @@ FUNCTION detect_by_shape(meta, ocrHint):
| 身份证、公民身份、人像面、国徽面 | `IDCARD` | ✅ 实测 |
| 一寸、证件照、免冠 | `PHOTO1` | ✅ 实测 |
| 申报表、工作证明、公章 | `WORK_PROOF` | ✅ 实测 |
| 学信网、备案表、电子注册 | `XJW_BA` | ⚠️ **零样本**——词是从官方模板文字要求里抄的,**待「社会评价」批次样本标定** |
| **(写不出词)** | `OTHER_PROOF` | ⚠️ **零样本**——含毕业证/社保单/职称/技能证书四类**版式完全不同**的东西,**可能不该做识别**,改为默认识别失败放行 + 学生自选槽位 |
| 学信网、备案表、电子注册 | `XJW_BA` | ⚠️ **不做**(2026-10 取消二期):编码**读得到、选不到**,强特征词从未标定 |
| **(写不出词)** | `OTHER_PROOF` | ⚠️ **不做**(2026-10 取消二期):编码**读得到、选不到**,倾向"不做识别、纯手动槽位" |
> ⚠️ **强特征词表目前只覆盖了自主认定批次的 4 类材料**(实测 112 个文件)。
> `XJW_BA` 与 `OTHER_PROOF` **没有一份真实样本**——它们的词、形态阈值、压缩档位都是**推测**。
> `XJW_BA` 与 `OTHER_PROOF` **没有一份真实样本**(且已确定**不做**)——它们的词、形态阈值、压缩档位都是**推测**。
> 详见确认函 D4。
**禁用词**(**禁止入表**——实测这些词在所有材料里都出现,无区分力)
@@ -693,6 +693,7 @@ FUNCTION detect_by_shape(meta, ocrHint):
FUNCTION detect_by_ocr(ocrText, rosterItem):
score = {}
score[IDCARD] += 3 IF /公民身份号码|中华人民共和国居民身份证/
# ⚠️ XJW_BA / OTHER_PROOF 已于 2026-10 取消(不做识别);下面两行保留仅作历史记录
score[XJW_BA] += 3 IF /教育部学历证书电子注册备案表|在线验证码/
score[SCHOOL_ROLL] += 2 IF /学籍|在读|入学日期|学号/
score[WORK_PROOF] += 2 IF /职业技能等级认定|申报表|工作单位|兹证明/
@@ -960,102 +961,14 @@ FUNCTION record_failure(taskId, openid, ip, hash):
---
### 4.9 名单加入申请通道(FR-509 · 客户确认后新增)
### 4.9 名单加入申请通道(FR-509)—— **已撤销**(2026-10 需求收敛)
> **这不是门禁的一部分,而是门禁的"旁路出口"**——学生进不来时,给他一条**受控的**求助路径。
#### 4.9.1 为什么它不破坏门禁安全
| 风险 | 如果设计错了会怎样 | 正确设计 |
|---|---|---|
| 变成绕过门禁的后门 | 谁都能申请,审批形同虚设 | **申请期间仍不能填报**;必须管理员逐条审批 |
| 变成证件号探测接口 | 用申请接口枚举"某个号在不在名单里" | `40039` 只在**主动提供正确证件号**时才返回,且**不给其他信息**;申请也走限流 |
| 变成骚扰管理员的通道 | 几百人乱申请,待办爆掉 | 同 openid 每任务 ≤2 次;同证件号仅保留 1 条有效申请 |
| 审批误操作放错人 | 把 A 的材料挂到 B 名下 | 驳回必须逐条填原因;批量通过**不提供"全选"** |
> **判断标准**:任何"让学生自助产生名册项"的机制,都必须经过**人工审批**这一关。否则门禁就只是装饰。
#### 4.9.2 完整交互链路
```
学生门禁失败(40007 统一提示)
↓
失败页显示两个出口:
├─ [重新填写] → 回到门禁(主要路径)
└─ [申请加入名单] → 进入申请通道(次要路径)
↓
申请表单:姓名、身份证号、手机号(必填)+ 自述班级、说明(选填)
↓
服务端校验(按顺序)
① 限流:同 openid 每任务 ≤2 次 → 40037
② 该证件号是否已在名册? → 40039(引导回去重填门禁)
③ 该证件号是否已有待审/已通过申请? → 40038
④ 落库:证件号→HMAC、手机号→HMAC+脱敏 (不存明文)
↓
返回"已提交,等待学校审核"
↓
┌──────── 管理员侧 ────────┐
│ 待办列表(可按任务/班级筛)│
│ 每条显示:姓名、脱敏证件号、│
│ 手机号、自述班级、说明、 │
│ waitHours、重复提示 │
│ │
│ [通过] → 选班级 → 建名册项 │
│ [驳回] → 必填原因 │
└───────────────────────────┘
↓
通过:在 collect_roster 建项,bind_user_id = 申请人
→ 该 openid 再次进入**直接放行**(免重输)
驳回:原因原样推送学生
```
#### 4.9.3 审批通过的事务边界
```
BEGIN
① SELECT ... FROM collect_join_request WHERE id=? FOR UPDATE -- 行锁,防重复审批
② 校验 status = 0,否则 40040
③ SELECT ... FROM collect_roster
WHERE task_id=? AND id_card_hash=? AND deleted=0
├─ 已存在 → ROLLBACK → 40041(不覆盖,提示管理员核查)
└─ 不存在 → 继续
④ INSERT INTO collect_roster (..., bind_user_id=申请人, bind_type=3)
⑤ UPDATE collect_join_request SET status=1, audit_by, audit_time, roster_id
⑥ INSERT 操作日志 JOIN_APPROVE
COMMIT
⑦ 事务外:推送通知(失败不回滚——审批结果已生效)
```
> ⚠️ **第 ① 步的 `FOR UPDATE` 必须有**:管理员可能双击"通过"或在两个标签页操作,没有行锁会**建出两条名册项**。
> 这与门禁并发绑定是同一类问题(见 §4.4)。
> ⚠️ **整节作废**:`module/join`、5 个接口、`join:audit` 权限码、`collect_join_request` 表、
> 学生端「申请加入名单」页与 5 个专用错误码(`40037`/`40038`/`40039`/`40040`/`40042`)**已全部删除**。
>
> ⚠️ **第 ⑦ 步在事务外**:通知发送失败不该回滚审批(用户已经点过了)。失败进重试队列。
#### 4.9.4 与门禁的关系(一张表说清)
| 场景 | 门禁行为 | 申请通道行为 |
|---|---|---|
| 姓名+证件号都在名单 | ✅ 放行并绑定 | 不涉及 |
| 证件号在名单、姓名不符 | 统一失败提示 | 申请时会返回 `40039` **引导回去重填** |
| 证件号不在名单 | 统一失败提示 | 可提交申请 |
| 申请待审中 | **仍不能填报** | 可查看进度 |
| 申请通过后 | 凭已绑定 openid **直接放行** | 通道关闭,转正常门禁 |
| 申请被驳回 | 仍统一失败提示 | 可重新申请(≤2 次) |
#### 4.9.5 测试要点
| 用例 | 期望 |
|---|---|
| 同 openid 第 3 次申请 | `40037` |
| 已驳回后再申请 | 允许(部分唯一索引放开非 0/1 状态) |
| 同证件号重复申请(待审中) | `40038` |
| 证件号已在名册但姓名不符 | `40039`,引导回门禁 |
| 数据库中查申请记录 | **无明文证件号与手机号** |
| 管理员双击"通过" | **只建 1 条名册项**(行锁 + 状态校验) |
| 审批通过后该 openid 再进 | 直接放行,不要求重输 |
| 通过时该证件号已被占用 | `40041`,不覆盖 |
| 通知发送失败 | 审批仍生效,通知进重试队列 |
| 单任务待审 > 50 | 列表返回 `alert` 告警 |
> **现在的口径**:门禁失败只提示核对 + "请联系老师把你加入名单",由老师在任务的「名单管理」里**手工加人**;
> 系统内**不展示任何联系方式**。安全要求(限流、失败原因不区分、名单不下发、不记明文日志)**继续适用于门禁本身**,
> 只是不再有一条"申请"链路。
---