Files
xiaotutongxue/AGENTS.md
T
lovife 31e7483464 docs: 同步契约 §6.5~6.9 / §13.4、问题清单 F-112~F-124、交接摘要 §一·三十九
1) docs/03-接口设计.md:
   - §6.5 补 expireBefore **含当天**的语义与"格式不对返回 40001,不静默忽略筛选";补
     "列表 contactPhone 恒为 null"与"packageName 与内嵌 usage{} 不实现(200 行内嵌用量会变成
     N+1 统计)"。
   - §6.6 的 tenantType 由 3 值改回真实的 **4 值**(1 学校 / 2 认定机构 / 3 报名点 / 4 企业)、
     packageId 由 ✅ 必填改为可选(DTO 里本来就是可选),并补副作用(发放 6 个内置角色 + 建管理员
     + 写 CREATE_TENANT 日志)。
   - §6.7 的响应示例原来写着 ocrCount=3105 / smsCount=420,**与实现恒 0 矛盾** —— 改成 0 并补齐
     契约没写的 notes 与 alerts.used/limit,再加三条口径:实时值 vs 周期、恒 0 的原因(编数字会让
     运营拿假数据去谈续费)、**只算这一个租户**(含"库里只有一所学校时看不出"这一点)。
   - **新增 §6.8 / §6.9**(PUT /tenants/{id} 与 GET /tenants/{id}):PUT 此前只在 §4.2 的总览表里
     出现过一次、全文**没有详情节**;GET 更是只登记在 §17.1 的"清单漏列"组里。§6.8 含字段表、
     "不传的字段不修改"、"tenantCode 不可修改"的理由,以及留痕对照表(status→0 是 DISABLE_TENANT,
     不是 UPDATE_TENANT)+ "这些日志行的 tenant_id 是 0,要用 target_type/target_id 查"的警告。
   - §13.4 的 Query 补 targetType(并说明为什么 targetType+targetId 才是最重要的查询方式:
     关键字全文匹配会把"改了它"和"顺带提到它"混在一起,而合规结论不能建立在这种匹配上);
     响应补 request_method / request_uri / extraJson;动作表由 9 行扩到**全集 27 个**并按来源分组
     (任务 / 名册 / 导出 / 角色 / 租户);写明 request_param / duration_ms 刻意不采集;
     并记下"一期只做查询、不做日志导出"这条**此前只写在 LogController 类注释里**的口径
     (导出等于把"谁在什么时候看了谁的身份证"这份名单落到本地磁盘,而导出行为本身也要留痕)。
   - 登录日志响应补 tenantId,并写明 userType 的权威枚举与 location 恒为 null。

2) docs/08-完成度盘点与问题清单.md:新增 **F-112~F-124** —— 用量越界(含"为什么本地完全看不出来")、
   PUT 明文写入加密列、两个枚举能建不能改、租户零留痕、expireBefore 差一天、AUTO_CLOSE 字面量、
   模块下拉里有死项、LogController 描述与事实相反、request_param/duration_ms 有列无写入点、
   schema-postgresql.sql 缺三张表、**"平台专属权限码 + 查带 tenant_id 的表"这个风险面**、
   登录日志缺租户列、userType 注释错位。F-112 标注为本轮最严重的一条。

3) docs/10-用户手册与客服话术.md:「三种角色,三个入口」→ **四种角色**(补平台运营,并写明它是
   唯一跨单位的角色、用"组织与账号/收集管理"这类单位内页面之前要先选"当前学校");
   §4 速查表的菜单名与路径同步(平台运维 → 日志,含两个页签;新增租户管理一行);
   附录的排查话术补 traceId("日志详情里能拿到,那是后端排查时最有用的一个线索")。

4) docs/交接摘要.md:新增 **§一·三十九**,九节 —— 一、口径变更(旧注释原文 + 推翻理由);
   二、用量越界那个 bug 的完整来龙去脉(权限码 → ignoreTable → 实测数据分布 → 修法与夹具);
   三、另外四条后端缺陷表;四、日志详情为什么不新增接口(轻字段 vs 全库 0 行有值的重字段);
   五、AUTO_CLOSE 与检查 ⑩ 的关系(**"这一族检查的价值取决于被检查的那一侧有没有被规范化"**);
   六、顺带修掉的五处;七、前端刻意用分段按钮而不是 NTabs 的两条原因;八、验证表 +
   "改了后端必须重新打包重启、判据是看返回体里有没有新字段"的提醒;九、约定同步。
   另把 E2E 断言总数 1595 → **1617**(四处)。

5) 根 AGENTS.md:接线检查说明补第 ⑩ 项,并写明做这一项时才发现 AUTO_CLOSE 是字符串字面量写的
   —— "提成常量"是让这个检查真正生效的前提。

验证:python scripts/verify-docs.py **22/22**;python scripts/check-wiring.py 未接线 **0 条**
(⑦ 11 个权限码 / ⑧ 0 处漏 import / ⑨ 3 个 SVG 全合法 / ⑩ 27 个动作码两端逐字一致);
python scripts/check-api-parity.py 契约 **119** / 已实现 118 / 缺口 1 —— **没变**,符合本轮
"不新增后端接口"的决定。
2026-10-13 14:04:59 +08:00

13 KiB
Raw Blame History

AGENTS.md

面向 AI 编码助手的项目指南。用简体中文交流、用中文写文档。

需求已冻结:文档是需求源,代码是它的实现,不要按"更合理"去改业务口径。 docs/交接摘要.md 是唯一状态源 —— 动手前先读它最近的两节(§一·三十五、§一·三十六) 与 ## 二、已锁定的铁律;那里面大量决定"读代码看不出来、但都有理由"。

⚠️ 2026-10 需求收敛(见 §一·三十六):只做 4 个角色(平台运营 / 学校管理员 / 班主任 / 学生)、 10 个 PC 菜单、不设二期。删掉的东西不要再"顺手加回来":副班主任 / 审核员两个角色、 历史整理器、加入申请(含学生端自助申请入口)、collect_task_org.deputy_user_ids。

一、四个可运行部分

目录 是什么 入口
backend/ Java 21 + Spring Boot 4.1.1 + MyBatis-Plus + PG16 + Redis7 xt-server/(xt-common/ 放响应体/错误码/加解密)
frontend-pc/ 管理端 纯 Vue 3 + Naive UI 2.42(2026-10 从 Gi Admin Pro + Arco 重搭) pnpm dev → 5173
frontend-mp/ 学生端 uni-app(H5 与微信小程序同一套代码) pnpm build:h5 / pnpm dev:mp-weixin
deploy/ 本地基础设施(PG + Redis,没有对象存储) docker compose up -d

docs/prototype/ 是视觉与交互的唯一基准(单文件自包含、零外部依赖、不用 emoji)。

范围只做「学校收集 + 自主认定」:collectMode=2 / certType=2,3 一律返回 40044。 不设二期 —— 与「二期」有关的任何设计(社会收集、社会评价、专项职业能力、数据范围可配等)都不做, 别顺手实现;OCR 与材料识别是「缓做,等供应商选型」。

二、常用命令(备注里是容易猜错的地方)

基础设施

cd deploy && docker compose up -d     # xt-pg(5432) + xt-redis(16379)

⚠️ 本机还有别的项目的 pg-verify(55432) 与 redis-local(6379),不要动它们。

后端

cd backend
mvn -DskipTests package              # 改了 Java 必须重新打包
java -jar xt-server/target/xt-server-1.0.0-SNAPSHOT.jar --spring.profiles.active=dev
  • ⚠️ 工作目录必须是 backend/,两处相对路径依赖它: application-local.yml(不提交的本地私有配置)就在 backend/; H5 静态目录配的是 file:../frontend-mp/dist/build/h5/
  • 跑的是 jar 不是热加载 → 改后端要重新 package(先停进程,否则 jar 被占用)
  • 动过 db/migration/ 必须 mvn clean package(Flyway 已到 V19;迁移文件发布后不许改,只能新加)
  • 本地私有配置机制是 profiles.active=dev + spring.profiles.include: local (模板 backend/application-local.yml.example)。 不要改用 spring.config.import —— 它优先级低于 profile 文件,会静默失效(见 application.yml 注释)

管理端(PC)

cd frontend-pc && pnpm install && pnpm dev
pnpm typecheck   # vue-tsc      pnpm lint   # eslint
  • 用 pnpm(仓库里是 pnpm-lock.yaml;别 npm install,会多出 package-lock.json)
  • dev 通过 vite proxy 把 /api 转发到 http://localhost:8080(见 .env.development), 没有 Mock(mock/ 与 vite-plugin-mock 都已删除)→ 后端不起着就没有数据
  • 接口模块都在 src/apis/xt/(按业务域分文件);没有 auto-import,组件与 API 全显式 import
  • ⚠️ 2026-10 已从 Gi Admin Pro + Arco 重搭为纯 Vue 3 + Naive UI,因此:
    • pnpm lint:style(Stylelint)已移除,只剩 pnpm lint(ESLint);pnpm typecheck 现在 0 错
    • 权限判断统一写 userStore.has('码')(没有 v-hasPerm 指令),字符串必须与后端 PermissionDef 逐字一致
    • n-tag 的 type 必须过 @/utils/tag.ts 转换:apis/xt/*.ts 里的展示映射仍是 Arco 色词 (green/arcoblue/gold/gray),直接透传会静默变灰且不报错
    • 图标:scripts/extract-pc-icons.py 生成 src/icons/icon-data.ts(37 个); AppIcon 对缺失图标是空占位、不报错 → 图标集升级后要重跑该脚本
  • PC 前端有独立的规范来源:frontend-pc/AGENTS.md + frontend-pc/.agents/(已按 Naive UI 重写)。 改那里的代码前先读它,不要另起一套写法

学生端(uni-app)

cd frontend-mp && pnpm install
pnpm build:h5          # → dist/build/h5(由后端托管)
pnpm dev:mp-weixin     # → dist/dev/mp-weixin(导入微信开发者工具)
  • H5 现在有独立 dev server 了(pnpm dev:h5 → http://localhost:5174): vite.config.ts 加了 /api 代理,.env.development 把 VITE_API_BASE 设成空串, 于是请求走相对路径、由代理转发到 8080。改一行不用再 build:h5。 ⚠️ 三端行为不同,改动这块前先看 src/config/index.ts 的注释: dev server → 代理;构建产物(后端托管)→ location.origin;小程序 → 写死地址
  • 改前端后浏览器要硬刷新(/assets/** 是 immutable 缓存)
  • 构建时 Dart Sass 的 legacy-js-api DEPRECATION 属正常
  • vue-tsc 有 2 条已知报错(src/config/index.ts 的 #ifdef 重复导出),不是新引入的。 ⚠️ PC 端不同:重搭后 pnpm typecheck 是 0 错 —— 那边一有报错就是新引入的
  • 当前节奏:先做 H5(可自动化测试、不依赖小程序资质);小程序端代码同步维护

三、怎么验证(本仓库最不显然的一点)

后端的单元测试很少(backend/*/src/test 只有 12 条纯函数断言,mvn test 跑)。 主验证手段是 scripts/e2e-*.py:打真实接口 + docker exec 进 PG 查库断言。

python scripts/e2e-m3-file.py     # 单跑一个;前提是后端 8080 在跑、xt-pg 容器在跑
  • 全量:把 scripts/e2e-*.py 依次跑一遍(37 个脚本 / 1617 条断言,以最近一次 verify-all --with-e2e 的输出为准;2026-10 需求收敛删掉了 e2e-m2-join.py 与 e2e-m7-cleanup.py), 或一条命令 python scripts/verify-all.py --with-e2e
  • 脚本自己造 MP 的 JWT:读 application-dev.yml 的 fill.jwt.secret + application.yml 的 issuer 手搓 HS512 —— 不需要先登录
  • 改了后端忘了重新打包/重启,测试会对着旧 jar 通过,这是最容易踩的空转
  • 跑完 e2e-m3-* 会清掉名册绑定(collect_roster.bind_user_id); 想继续用浏览器验演示态,要先重新绑定(见交接摘要的"演示态"一节)
  • 清理残留:python scripts/dev-clean-e2e.py(默认 dry-run; --only-orphan-files --apply 只清磁盘孤儿文件 —— 压测跑几轮就能攒下几百 MB; --apply 清 e2e 租户与测试组织,会先算「仍被存活任务引用的组织」并保留)、 python scripts/dev-clean-test-users.py、python scripts/dev-clean-qiniu.py (清真实桶里 xiaotutongxue/ 前缀的测试对象,默认 dry-run,删前先核实库中零引用)
  • 文档一致性:python scripts/verify-docs.py
  • 接线检查:python scripts/check-wiring.py("声明了但没接上" —— 权限码有没有地方校验、错误码有没有被引用、站内信模板有没有发送点、 bizType 有没有被用过、菜单键 ↔ PC 端 XtMenu 是否对得上、 PC 端 user.has('码') 引用的权限码 ↔ PermissionDef(第 ⑦ 项)、 模板里的 <n-*> 组件有没有漏 import(第 ⑧ 项 —— 漏了会被当未知元素渲染,样式整块丢、 编译与 lint 都不红)、 .svg 资产是不是合法 XML(第 ⑨ 项 —— SVG 注释里出现连续两个减号就 not well-formed, 图标变裂图而控制台一句话都不打;2026-10 换品牌标时连踩两次)、 后端会写的日志动作码 ↔ PC 端 OPERATION_ACTIONS 的文案(第 ⑩ 项 —— 缺一个文案, 日志页会原样回显英文码;做这一项时才发现 AUTO_CLOSE 是字符串字面量写的, 所以"提成常量"是让这个检查真正生效的前提)。 ⚠️ 这一族错编译 / lint / review 都不会红(类在、表在、常量在、注释还写得很清楚), 只有"从入口走一遍"才暴露 —— 所以把它固化成命令;有意不接线的写在脚本上方的例外表里(带理由)。 ⚠️ 检查器自己也会失效:PC 端重搭后权限码的写法从 permissions.includes / v-hasPerm 改成了 user.has,第 ⑦ 项曾因此"一个码都扫不到却判为通过"。 加/改这类检查时必须反向验证一次(故意写错,它得红)
  • 一条命令跑完上面这些:python scripts/verify-all.py --with-e2e (9 项;跳过项会如实列出来,不是静默跳过)

四、接口与数据契约(不显然、容易做错)

  • 响应恒为 {code,msg,data};业务错误也用 HTTP 200,只有 401/403/429/5xxxx 用真状态码
  • 软删除字段名是 deleted(不是 is_deleted);业务表都带 tenant_id
  • 租户拦截器默认拒绝:匿名请求触及租户表会直接 40101。匿名接口(如扫码落地页) 必须走专用 Mapper + @InterceptorIgnore(tenantLine="true") + 显式带 tenant_id, 且集中放(module/mp/mapper/MpSceneMapper.java),就是为了"关掉框架保护后由人保证隔离"这件事有地方可审
  • 权限码写在代码里(security/PermissionDef、security/BuiltinRole);库里只有 sys_role_permission 做「角色 → 权限码」的分配,没有 sys_permission 定义表 —— 加权限要改代码,不要想着往表里插一行
  • 动态表单 = 内置字段(固定列、可统计)+ 扩展字段(extras_json,只展示与导出)。 字段 key 全是 snake_case,权威来源 module/form/service/BuiltinFields.java(21 项)—— 改口径要同步它,别按直觉造 key
  • collect_task.status:0 草稿 / 1 收集中 / 2 已截止 / 3 已归档 / 4 暂停 (接口设计文档曾写反,以代码为准)
  • collect_roster.id_card_no 存的是 AES 密文(另有 id_card_hash / id_card_mask), 明文只在导入与门禁时存在 → 提交环节做不到身份证校验位校验,别去"补"这个
  • 材料上传是一步:POST /mp/files/upload 收上来就压缩 + 按模板改名,默认只存规范件 (fill.storage.keep-raw=false);process 只是"有原始件时的重新规范化"
  • 图片处理有两条踩过真 bug的规则,动之前先读交接摘要 §一·二十 / §一·二十一: ① 服务端必须按 EXIF 转正,否则手机竖拍的身份证会被存成躺倒的(且 E2E 的测试图不带 EXIF,抓不到); ② 浏览器与小程序对 EXIF 的处理不一致,前端必须"量出来再决定",写死必错一半

五、环境与流程坑

  • 本地 H5 走花生壳穿透(http://394nb68658le.vicp.fun → 192.168.3.139:8080, 配在 application-dev.yml 的 mp-h5)。硬刷新后首屏常要 1~2 分钟, 探测要给足,否则会把"还在加载"误判成失败
  • 打桩登录(仅 dev:fill.mp-h5.stub-enabled,生产必须 false,代码里还会再判一次 profile): /api/v1/mp/h5/stub-login?openid=<openid>&scene=t1-k17 → 302 回 H5,token 放在 URL fragment 里
  • scene 三段式:t{tenantId} / t{tenantId}-k{taskId} / t{tenantId}-k{taskId}-o{orgId} (微信限 32 字符)。它可穷举,所以匿名接口只返回最少信息 + 按 IP 限流
  • 存储默认 provider=local(落 backend/data/uploads,只适合开发:多副本必然丢文件); 七牛用 provider=qiniu,AK/SK 只放 backend/application-local.yml 或环境变量。 MinIO 已删,本地不再有 S3,别去找
  • 七牛空间是私有的 → 下载走签名链接(有有效期): URL 不能入库、不能发出去让人明天再打开,每次要用现取
  • 改中文文档、或跑任何会碰到中文文本的脚本,一律写成 .py 文件再执行: PowerShell 不支持 heredoc(<<'PY' 直接报错);@"..."@ here-string 会吃掉双引号 (return "bin"; 变成 return bin;)—— 带引号的代码用编辑工具改,别过转义层

六、提交前

  • 不要提交:backend/data/、dist/、application-local.yml、deploy/.env (都已在 .gitignore,但 git add -A 容易顺手带上 —— 先 git status 看一眼)
  • deploy/ 里只有 .env.example 与 docker-compose.yml 被 git 跟踪 (*.pem 在 ignore 里,deploy/rsa-public-key-dev.pem 是本地开发公钥)
  • 改动接口 / 库表 / FR-VAL 编号 → 重跑 python scripts/verify-docs.py, 并同步 docs/00-交付计划.md 与 docs/交接摘要.md