用户决定:后端不再对外发静态文件,学生端 H5 交给网关。两者**仍然同源**
(微信只给一个授权回调域名;H5 的接口地址取自 location.origin),
所以前端业务代码、微信后台、application-dev.yml 的 h5-base-url 都不用改。
后端
- 删掉 dev/test/prod 三份 profile 里的 spring.web.resources.static-locations
- 主配置加 spring.web.resources.add-mappings=false —— 用开关而不是"指到空目录":
后者会连带启用 Spring Boot 的默认位置(classpath:/static 等),谁丢个文件进去就悄悄对外可见
- StaticCacheHeaderFilter → ApiNoStoreFilter,只剩 /api/** 的 no-store
(静态资源的缓存策略整体归 nginx,避免"改一处漏一处")
- 补 NoHandlerFoundException → 404:关掉静态映射后未匹配路径不再经过
ResourceHttpRequestHandler,抛的是 NoHandlerFoundException,只处理
NoResourceFoundException 时访问 / 会变成 500「系统繁忙」并刷 ERROR 日志
deploy
- 新增 xt-nginx(80):发 frontend-mp/dist/build/h5 + /api 反代 8080(host.docker.internal,
Linux 靠 extra_hosts host-gateway),端口与产物目录走 .env 的 H5_PORT/H5_DIR
- 三条"删了就会出事"的都已带上理由:client_max_body_size 50m(默认 1m,超了回 413
而后端日志一个字都没有)、/assets 的 immutable 与 *.html 的 no-cache(§一·十 那个
"发版后学生看到旧页面"的坑)、健康检查写 127.0.0.1(容器里 localhost 先解析到 IPv6,
nginx 只听 IPv4 → 永远 starting 而首页是好的)
CI 与文档
- CI:学生端 H5 构建从"e2e 的输入"改为编译检查,不再留 artifact(e2e 不消费它)
- 同步 AGENTS.md / README.md / docs/09(启动步骤、CI 表、发布注意、排障表)/ docs/11
- 交接摘要新增 §一·四十三(含理由、验证数字、要人工做的一步与环境限制)
验证
- mvn clean package 通过(必须 clean:删掉的旧类会留在 target/classes 被组件扫描)
- 后端直连:/ 与 /assets/* → 404({"code":40001,"msg":"接口不存在"})、
/api/.../captcha → 200 + no-store、/doc.html → 302→200、/v3/api-docs 与 /actuator/health → 200
- 经 nginx(80):首页 200 + no-cache、/assets/*.js 200 + immutable、/nope.js 404(无回退)、
/api 200 + no-store、gzip 生效;2MB multipart 上传 → 401(与直连一致 ⇒ 未踩 1m 的坑)、
55MB → 413
- e2e(XT_BASE=http://localhost,全程走 nginx):e2e-m0.py 37/37、e2e-m3-h5-auth.py 24/24、
e2e-m2-scope.py 38/38
- verify-all.py --quick 6/6;check-test-profile.py:test profile 仍能起来
14 KiB
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 + nginx,没有对象存储) | 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) + xt-nginx(80)
⚠️ 本机还有别的项目的 pg-verify(55432) 与 redis-local(6379),不要动它们。
⚠️ H5 由 xt-nginx 托管(80),不由后端托管(2026-10 调整,见 §一·四十三):
它把 frontend-mp/dist/build/h5 当静态站发出去,并把 /api 反代到后端 8080 ——
两者同源,这是微信授权与 H5 的接口地址(location.origin)共同要求的。
起它之前先 cd frontend-mp && pnpm build:h5(挂的是产物目录,空目录 = 页面 404)。
后端
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(不提交的本地私有配置)在那里, 是靠相对路径找到的(H5 静态目录那条相对路径已于 2026-10 随托管一起移走,见 §一·四十三) - 跑的是 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(由 xt-nginx 托管,不由后端托管)
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 → 代理;构建产物(由 nginx 托管、与/api同源)→location.origin;小程序 → 写死地址 - 改前端后浏览器要硬刷新(
/assets/**是 immutable 缓存) - 构建时 Dart Sass 的
legacy-js-apiDEPRECATION 属正常 vue-tsc有 2 条已知报错(src/config/index.ts的#ifdef重复导出),不是新引入的。 ⚠️ PC 端不同:重搭后pnpm typecheck是 0 错 —— 那边一有报错就是新引入的- 当前节奏:先做 H5(可自动化测试、不依赖小程序资质);小程序端代码同步维护
- ⚠️ 小程序端目前学生用不了(F-130,P0):前端从来没有调用过
/mp/login(token 永远是空 → 门禁 40101), 而后端那条路也不通 ——scene带 taskId 时/mp/login必然 500(MpLoginService.java:127匿名查租户表被租户拦截器拒)。 实测细节见docs/11§5.4 / §6。除登录外整条链路(落地页/门禁/表单/压缩上传/红黄校验/提交)都是好的 —— 用开发者工具自动化(wmp_*工具族)可以跑,wx.login()在页面上下文里能拿到真 code
三、怎么验证(本仓库最不显然的一点)
后端的单元测试很少(backend/*/src/test 只有 12 条纯函数断言,mvn test 跑)。
主验证手段是 scripts/e2e-*.py:打真实接口 + docker exec 进 PG 查库断言。
python scripts/e2e-m3-file.py # 单跑一个;前提是后端 8080 在跑、xt-pg 容器在跑
- 全量:把
scripts/e2e-*.py依次跑一遍(38 个脚本 / 1712 条断言,以最近一次verify-all --with-e2e的输出为准;2026-10 需求收敛删掉了e2e-m2-join.py与e2e-m7-cleanup.py, 之后又补了e2e-m3-flow.py—— 填报链路的覆盖缺口,见docs/11-填报流程端到端测试计划.md), 或一条命令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:80, 即xt-nginx;不是后端的 8080 —— 指到 8080 的表现是"接口好用、页面 404"。 配在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/里被 git 跟踪的只有.env.example、docker-compose.yml与nginx/xt.conf(*.pem在 ignore 里,deploy/rsa-public-key-dev.pem是本地开发公钥)- 改动接口 / 库表 / FR-VAL 编号 → 重跑
python scripts/verify-docs.py, 并同步docs/00-交付计划.md与docs/交接摘要.md