做「平台运营的租户管理」时挖出来的,其中第 1 条与那个功能本身无关、但严重得多。 1) 🔴 GET /tenants/{id}/usage 一直算的是**全平台**的数。平台运营的 tenantId 是 0,而 FillTenantLineHandler.ignoreTable() 对平台身份直接返回 true —— 整张表的租户过滤被关掉, 于是 distinctStudents()(查 collect_roster)与存储量(查 collect_file)都不带租户条件, 返回全平台数字却标着某一所学校的名字,运营会拿它去谈续费。而这个接口的调用者**一定是** 平台运营(tenant:view 在 platformOnlyCodes 里),所以它从来只在"过滤被关掉"的上下文里跑过。 ⚠️ 本地完全看不出来:实测 34 行 collect_roster、22 行 collect_file 全在 tenant_id=1, 只有一所学校有数据时错误值与正确值**完全相等**。修法:统计段包进既有的 TenantScope.callAs, 切走后 isPlatformAdmin() 变 false、过滤自动恢复;**没有**手写 tenant_id 条件 —— 那等于 放弃拦截器的保护。 2) PUT /tenants/{id} 把手机号**明文**写进加密列(create 走 codec.encrypt,update 是裸赋值)。 AesGcmCipher.decrypt 对非 Base64 直接抛(该类没有 dev 旁路)→ 存过一次电话,这所学校的 详情页就永远打不开。库里 id=1 存的就是明文 13800138000。改为非空才加密写入(空 = 不修改), 详情返回脱敏值;**读路径必须容错**(解密失败按明文处理 + WARN),否则故障形态是"永久 500", 看起来像系统坏了、而不是"库里有一行旧数据"。历史明文行不做迁移(迁移里拿不到密钥)。 3) 租户两个枚举"能建不能改":tenantType 只放行 1~3(实体/DDL/契约/前端都是 4 值)、 status 只放行 0/1(实体有 2 试用,而前端的状态筛选按钮里就有「试用」,选了永远是空列表)。 已按真实枚举放开,报错文案与实体注释逐字对齐。 4) 开通/修改/停用租户原先一条日志都不写(只有给运维看的 log.warn/log.info)。新增 MODULE_TENANT 与 CREATE_TENANT/UPDATE_TENANT/DISABLE_TENANT/ENABLE_TENANT —— 停用与启用 不并进 UPDATE_TENANT:停用是这套系统里影响面最大的单个动作,出争议时第一个要看的就是这个 区别。⚠️ 这些行的 tenant_id 是 0(写日志用的是**操作者**的上下文),查"这所学校被谁停过" 要用 target_type=TENANT + target_id,**不能**用 tenantId 筛(反直觉,已写进常量注释)。 5) expireBefore 用 atStartOfDay() 与存进去的当天 23:59:59 比 → **差一天**,「今天到期」的租户 查不出来(同型的坑在 LogController.parseTo 上修过一次,那次是"查 9 月 30 日查不到当天")。 改为补到当天末尾。格式不对返回 40001 并说明格式 —— **刻意不学** parseTo 的"当没传": 那侧是界面上的自由文本输入,静默忽略是为了"输错了也别让人看不到日志";这侧的值只由界面按 当前日期算出来,静默忽略筛选会让人以为"我按到期日筛过了"。 6) 顺带: - AUTO_CLOSE(定时任务自动截止)在 TaskService 里是**字符串字面量**、不在常量表里 —— 也就是说"后端常量 ↔ 前端文案"这类检查**本来就扫不到它**,而它正是量最大的那类任务日志, 界面上会原样回显英文码。提成常量(连带 STATUS_CHANGE 兜底名)。 - 日志列表 SELECT 补 request_method/request_uri/extra_json 三个**轻字段**(实测一页 20 行 约 2 KB),日志详情因此不需要新开接口;真正重的 request_param/duration_ms 全库 1758 行里 **0 行有值**(从来没有 setXxx 过),不采集。 - LoginLogVO 补 tenantId:平台运营看到的登录日志是**跨单位**的,没有这一列就分不清一行是 哪所学校的,而"某个账号什么时候登进来的"一定得连着"哪个单位"一起回答。 - 修正 LoginLogVO.userType 的 Javadoc:原来写着「1 超管 / 2 管理员 / 3 教师 / 4 学生」, 与权威口径(1 平台运营 / 2 学校管理员 / 3 班主任 / 4 学生)错位 —— **同一个坑第三次出现**, 该错位枚举曾导致"把班主任当成社会人员拒掉"。 新增检查 scripts/check-wiring.py 第 ⑩ 项:后端会写的动作码 ↔ PC 端 OPERATION_ACTIONS 的文案, 逐字核对(缺一个文案,日志页就会原样回显英文码)。**已反向验证**:删掉一条文案 → 报红 + exit 1。 ⚠️ 写这个检查时踩到的坑:同文件里 MODULE_OPTIONS / TARGET_TYPE_OPTIONS 也是 { value: 'TASK' } 的形状,整文件扫会把模块名当成动作码收进来 —— 所以只从 OPERATION_ACTIONS 那个数组里取。 验证:python scripts/verify-all.py --with-e2e → **9 项全过 / 0 失败 / 0 跳过**(37 脚本 / 1617 断言)。e2e-m8-orgfile.py **74/74** —— 其中"用量只算这一所学校"这条**必须造夹具**才 证伪得了:它往新建的 E2E 租户**直插一行 collect_roster**(该表 NOT NULL 且无默认值的列只有 5 个, 不需要走导入夹具),再断言"接口值 == 库里 where tenant_id=1 的去重数",并**额外断言 "全平台去重数 > 一号学校去重数"**,否则这条断言是空转的。e2e-m5-logs.py 44/44(回归)。 重打包重启后再验一次:/logs/logins 返回 tenantId、/tenants/1 返回 138****8000(而不是 500)—— 判据是**看返回体里有没有新字段**,而不是"接口通了就算"。
深学在线
职业技能等级认定报名材料收集 SaaS 范围(2026-10 收敛):只做「学校收集 + 自主认定」(4 类材料,全部有实测样本)。社会收集 / 社会评价 / 专项职业能力 不做(传
collectMode=2、certType=2,3返回40044);不设二期 学生在微信小程序填报 · 班主任在手机初审 · 管理员在 PC 终审与导出 最后更新:2026-09-28 | 状态:需求已冻结(客户全部确认,可开工)
一、这是什么
客户(职业院校 / 认定机构)每年要收集几百名学生的认定报名材料(一寸照片、身份证、学籍表、工作证明)。现状是靠班主任在微信群里收、手工改名、手工压缩、手工核对——一个班 28 人就有 112 个文件、59 个命名违规、31 个超体积,全靠人眼。
本系统把这件事做成流程:门禁核验 → 按规则自动命名与压缩 → 材料识别 → 校验 → 分级审核 → 一键导出规范包。
核心增量:官方 Excel 模板里「报考职业」「报考工种」是自由填写、没有下拉——我们把它变成从名单聚合 + 一致性体检。
二、文档地图
按角色读
| 你是 | 从这几份开始 | 顺序 |
|---|---|---|
| 客户 / 业务方 | 01-客户与机构确认函.md → prototype/index.html |
先看确认函(有 23 项待拍板),再点开原型 |
| 项目经理 | 00-交付计划.md → 05-开发任务拆解.md → 交接摘要.md |
先看进度,再看 94 个任务与 14 周排期 |
| 产品 / 需求 | 数据信息填报系统PRD.md → 数据信息填报系统PRD-功能逻辑卷.md |
前者含库表接口,后者是纯功能逻辑 |
| 后端开发 | 03-接口设计.md → 04-核心模块设计说明.md → db/schema-postgresql.sql |
接口收发 → 引擎怎么做 → 建表 |
| 前端开发 | prototype/ → 03-接口设计.md |
原型是视觉与交互的唯一基准 |
| 测试 | 06-验收测试清单.md → 04-核心模块设计说明.md 附录 B |
36 条 Golden 断言 + 111 条功能验收 |
| 新加入的人 | 交接摘要.md → 本文件 |
一份文档看清全部状态 |
全部文档
| # | 文档 | 规模 | 内容 | 状态 |
|---|---|---|---|---|
| 00 | 00-交付计划.md |
8 KB | 7 项计划与进度追踪、风险跟踪 | ✅ |
| 01 | 01-客户与机构确认函.md |
18 KB | 23 项已全部确认 + 客户回执 + 落地动作清单 | ✅ 需求冻结 |
| 02 | 02-需求追溯矩阵.md |
10 KB | 115 条 FR ↔ 原型/库表/接口 的自审报告 | ✅ |
| 03 | 03-接口设计.md |
— | 119 个接口字段级定义、枚举全集、12 个 VO、54 个错误码 | ✅ |
| 04 | 04-核心模块设计说明.md |
56 KB | 6 个模块伪代码级说明 + 依赖图 + 26 项回归基线 | ✅ |
| 05 | 05-开发任务拆解.md |
31 KB | 100 个任务 / 387 人日 / 15 周,含关键路径、一期范围与风险 | ✅ |
| 06 | 06-验收测试清单.md |
29 KB | 36 条 Golden 断言 + 111 条功能验收 + 签署表 | ✅ |
| — | 交接摘要.md |
11 KB | 状态源:客户回执、13 条铁律、已完成、待办、环境、教训 | ✅ |
| — | 数据信息填报系统PRD.md |
190 KB | 主 PRD V2.3+:125 条 FR、21 条 VAL、20 张表 DDL、验收标准 | ✅ |
| — | 数据信息填报系统PRD-功能逻辑卷.md |
88 KB | 纯功能运转逻辑(不含库表/接口/代码) | ✅ |
| — | db/schema-postgresql.sql |
36 KB | 可执行 PG DDL(20 表),已在 PostgreSQL 16 实测零错误 | ✅ |
| — | prototype/(4 份) |
— | 29 屏交互原型:学生端 10 / 班主任端 9 / 管理端 10 | ✅ |
三、三分钟看懂产品
一条主线
┌─ 学生端(小程序)─┐ ┌─ 班主任端(手机)─┐ ┌─ 管理端(PC)─┐
名单 ──→ 门禁核验 ──→ 填报 ──→ 卡片流初审 ──→ 终审 ──→ 导出规范包
└─✗ 失败 → 只提示核对 + "请联系老师加你进名单"(老师手工加人)
▲ ▲ ▲ ▲
│ │ │ │
报考范围 识别 命名+压缩 汇总 Excel
从名单聚合 校验 (≤1M 无例外) + 目录树
两个必须知道的数字
| 数字 | 含义 |
|---|---|
| 59 / 112 | 真实批次里的命名违规率(52.7%)。系统自动修 |
| 475 MB → 37 MB | 原始总量 → 导出包,压缩率 92%。300dpi 清晰度零损失 |
三条铁律(不要试图绕过)
| # | 铁律 | 理由 |
|---|---|---|
| 1 | 必须先导名单,姓名+身份证匹配不通过不放行 | 门禁是产品根基 |
| 2 | 有不合格文件时直接禁用导出按钮 | 带问题打包出去,返工成本远高于当场修 |
| 3 | 识别失败一律放行 | 不阻断提交是完成率的底线 |
| 4 | 归属存疑一律阻断,且无"仍然继续" | 文件名的姓/证号指向别人 = 传错材料。自动改名会把错传洗白,审核看不出来、事后无从追溯 |
四、关键决策速查
完整 13 条见
交接摘要.md第二节。这里只列最容易被反复讨论的。
| 决策 | 内容 |
|---|---|
| 压缩 | 强制 ≤1M,无例外;默认 300dpi/q85(实测 28/28 达标,均值 806KB) |
| 材料来源 | 只有聊天记录 / 相册 / 手机文件,「直接拍照」默认关闭 |
| 报考范围 | 从名单聚合,不存在独立的"计划清单"文件 |
| 数据库 | PostgreSQL(纯 PG);软删除字段名 deleted |
| 滑动交互 | 班主任端只做右滑通过,驳回必须点按钮 |
| 批量通过 | 只带无问题项,红黄一律排除 |
| 动态表单 | 内置字段(固定列可统计)+ 扩展字段(extras_json) |
| 名单外的人 | 不做自助通道(2026-10 撤销):由老师在任务里手工加人(名册追加导入),不绕过门禁 |
| 归属判定优先级 | 证件号反查名册 > OCR 内容 > 姓名(仅辅助,不做独立规则) |
| 联系方式 | 系统内不展示任何班主任/管理员联系方式——学生本来就有;门禁失败只给「重新填写」一条系统内出路 + 一句"请联系老师" |
| 校验位的作用 | 失败 = 不是真实号码 → 可自动改;通过 = 真人 → 改了就是改身份 → 必须阻断 |
| 审批粒度 | 批量通过不提供"全选",驳回必须逐条填原因(身份认定误操作代价极高) |
五、目录结构
xiaotutongxue/
├── README.md ← 本文件
└── docs/
├── 00-交付计划.md
├── 01-客户与机构确认函.md
├── 02-需求追溯矩阵.md
├── 03-接口设计.md
├── 04-核心模块设计说明.md
├── 05-开发任务拆解.md
├── 06-验收测试清单.md
├── 交接摘要.md
├── 数据信息填报系统PRD.md
├── 数据信息填报系统PRD-功能逻辑卷.md
├── db/
│ └── schema-postgresql.sql
└── prototype/
├── index.html ← 三端入口,从这里打开
├── student-app.html
├── teacher-app.html
└── admin-web.html
六、快速上手
看原型
直接双击 docs/prototype/index.html。单文件自包含、零外部依赖,不需要服务器。
管理端原型支持 100% / 82% / 66% 缩放查看。
验证数据库
# 起一个 PG 容器
docker run -d --name pg-verify -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=filldb -p 5432:5432 postgres:16
# 执行 DDL
docker exec -i pg-verify psql -U postgres -d filldb < docs/db/schema-postgresql.sql
# 验证结果:20 张表 / 177 列注释 / 65 索引,零错误
docker exec -it pg-verify psql -U postgres -d filldb
容器仍在本机运行时可
docker exec -it pg-verify psql -U postgres -d filldb直接进入; 不需要了用docker rm -f pg-verify清理。
跑原型结构校验
python check2.py docs/prototype/admin-web.html
# 输出:标签配对 / CSS 变量 / SVG 图标 / emoji / 外部依赖 五项检查
六·二、把系统跑起来(三条命令)
# 1) 基础设施:PG 16 + Redis 7(没有对象存储,MinIO 已删)
cd deploy
copy .env.example .env # 首次
docker compose up -d
# 2) 后端:Flyway 自动建库 + 自动初始化账号
cd ../backend
mvn spring-boot:run
# 3) 验收自测(35 项)
cd ..
python scripts/e2e-m0.py
开发环境初始账号(仅本地,由 fill.seed.enabled 控制):
| 账号 | 密码 | 角色 |
|---|---|---|
admin |
Admin@123 |
学校管理员(租户 demo) |
platform |
Platform@123 |
平台运营 |
接口文档:http://localhost:8080/doc.html
代码结构
backend/ Java 21 + Spring Boot 4.1.1
├── xt-common/ 统一响应 / 54 个错误码 / AES-GCM / HMAC / RSA
└── xt-server/ 配置 / 鉴权 / 多租户拦截 / 业务模块(26 张表,Flyway 已到 V19)
deploy/ docker-compose + RSA 开发公钥
scripts/ E2E 验收(37 个脚本)、接线检查、DDL 同步、图标抽取
frontend-pc/ PC 管理端:**纯 Vue 3 + Naive UI**(pnpm dev → 5173)
frontend-mp/ 学生端 / 教师端:uni-app(H5 与微信小程序同一套代码)
⚠️ 搭环境前请先读
docs/07-开发实施计划.md §9「版本兼容踩坑记录」——
那里记了 10 条 Boot 4 / Flyway 10 / MyBatis-Plus 3.5.9 的真实坑,
其中多数是「依赖在、功能不在,且不报错」的类型,靠读文档和凭经验都躲不掉。
七、交付状态
| 阶段 | 内容 | 状态 |
|---|---|---|
| 需求 | 主 PRD(125 FR / 20 VAL / 20 张表)+ 功能逻辑卷 | ✅ |
| 设计 | 数据库 DDL(PG 16 实测)+ 接口设计(119)+ 模块设计(6) | ✅ |
| 原型 | 29 屏(学生 10 / 班主任 9 / 管理 10)= 29 | ✅ |
| 计划 | 100 任务 / 387 人日 / 15 周(一期范围已收窄) | ✅ |
| 测试 | 36 条 Golden 断言 + 111 条功能验收 | ✅ |
| 待外部 | ✅ 已确认,按建议执行 |
客户已确认(2026-09-28 · 可开工)
| 项 | 确认结论 |
|---|---|
| A1 | ✅ 认定机构接受 300dpi 输出 |
| B1 | ⚠️ 已回退(2026-10 需求收敛):加入申请通道整体撤销(模块/表/接口/页面/错误码一并删除)。它是确认函里唯一的功能增量,建议补变更确认单 |
| B3 | ✅ 字段三分类:姓名/证件号/报考三项锁定,其余可核对修改并留痕 |
其余 20 项均与现有设计一致,零改动。 需求已冻结,可按 05-开发任务拆解.md 的 M0 开工。
八、数据与测试基线
| 项 | 值 |
|---|---|
| Golden Test Set | D:\项目\202609机电填报系统\24中技数控1班\ |
| 规模 | 28 人 × 4 类材料 = 112 个文件,原始 475 MB |
| 命名违规 | 59(52.7%) |
| 双扩展名 | 56 |
| 体积超标 | 31 |
| 证件号异常 | 5(自动修 3 + 待人工 2) |
| 疑似串档 | 1(唯一必须人工判断的) |
| 压缩后 | 均值 806 KB,最大 831 KB,0 张超标 |
| 导出包 | ≈37 MB,压缩率 92% |
⚠️ 这个数据集不可替代。所有"实测"结论都来自它;换成构造数据,双扩展名 56/112 这种数字就复现不出来。 必须纳入版本管理或保留只读副本。
九、开发环境
| 项 | 说明 |
|---|---|
| 数据库 | PostgreSQL 16(DDL 已实测通过) |
| 开发顺序 | M1 命名引擎 → M4 门禁 → M2 压缩 → M5 校验 → M3 识别 → M6 导出 |
| 客户回执 | 全部 23 项按建议执行;唯一功能增量 B1(申请加入名单)已于 2026-10 需求回退取消 |
| 原型约束 | 单文件自包含、零外部依赖、内联 SVG 图标、无 emoji |
| 最长的两个单点 | 压缩引擎 12 人日、打包引擎 10 人日(加人无效) |
| 回归门禁 | 改动后必须复现 15 项基线指标(对应 29 条断言),偏差即回归 |
十、两条踩过的坑
| # | 教训 |
|---|---|
| 1 | 脚本审计前先验证脚本本身。追溯矩阵审计时脚本漏匹配加粗编号,误报 3 个"需求问题",若直接采信会引入 3 处错误 |
| 2 | 引用已有编号前先脚本核对,不凭记忆。模块设计里把 VAL 编号整体写错位(VAL-01 是姓名 vs 证件号格式),若不查会与 PRD 互相矛盾 |
详见
交接摘要.md第七节。
文档中心维护约定:任何改动后,请更新 00-交付计划.md 的执行记录与 交接摘要.md 的状态;改动接口/库表/FR 编号时,必须重跑跨文档一致性核对。