# Mach-CMS 联调测试 Checklist > 版本: 0.2.0 > 日期: 2026-08-09 > 用途: Agent1(后端)+ Agent2(前端)验收 + 联调 --- ## 0. 环境准备(前置条件) | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 0.1 | PostgreSQL 16 已启动 | `psql -U postgres -c "\l"` 能列出数据库 | ☐ | | 0.2 | 数据库 `cms` 已创建 | `createdb -U postgres cms` 执行成功或已存在 | ☐ | | 0.3 | Node.js 20+ 已安装 | `node -v` 输出 v20.x | ☐ | | 0.4 | JDK 21 已安装 | `java -version` 输出 21 | ☐ | | 0.5 | `openapi.yaml` 已放置于后端仓库根目录 | 文件存在且版本为 0.1.0+ | ☐ | | 0.6 | `openapi.yaml` 已放置于前端仓库可引用路径 | `openapitools.json` 中 `inputSpec` 路径正确 | ☐ | --- ## 1. Agent1 后端独立验收 ### 1.1 构建与启动 | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 1.1.1 | `./gradlew build` 无编译错误 | 控制台 `BUILD SUCCESSFUL` | ☐ | | 1.1.2 | `./gradlew test` 通过 | 所有测试通过(至少包含 Article 状态机测试) | ☐ | | 1.1.3 | `./gradlew bootRun` 正常启动 | 控制台出现 `Started CmsApplication` 且无 ERROR | ☐ | | 1.1.4 | Actuator 健康检查 | `GET http://localhost:8080/actuator/health` 返回 `{"status":"UP"}` | ☐ | | 1.1.5 | Swagger UI 可访问 | `http://localhost:8080/swagger-ui.html` 正常渲染 | ☐ | ### 1.2 数据库迁移 | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 1.2.1 | Flyway V1 迁移成功 | `flyway_schema_history` 表有 V1 记录 | ☐ | | 1.2.2 | Flyway V2 迁移成功 | `flyway_schema_history` 表有 V2 记录;`articles/tags/article_tags` 表结构正确 | ☐ | | 1.2.3 | Flyway V3 迁移成功 | `flyway_schema_history` 表有 V3 记录;`articles` 表有触发器 `article_search_vector_trigger` | ☐ | | 1.2.4 | jOOQ 代码生成成功 | `build/generated-sources/jooq/top/jmto/mach_cms/jooq/` 存在 Kotlin 文件 | ☐ | ### 1.3 认证接口(独立测试) | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 1.3.1 | 用户注册 | `POST /api/auth/register` 返回 `Response`,code=0 | ☐ | | 1.3.2 | 用户登录 | `POST /api/auth/login` 返回 `Response`,包含 accessToken + refreshToken | ☐ | | 1.3.3 | Token 刷新 | `POST /api/auth/refresh` 传入 refreshToken,返回新的 TokenPair | ☐ | | 1.3.4 | 用户登出 | `POST /api/auth/logout` 返回 code=0;再次用旧 refreshToken 刷新应失败 | ☐ | | 1.3.5 | 密码加密 | 直接查 PG `users` 表,`password` 字段为 BCrypt 哈希(以 `$2a$` 开头) | ☐ | ### 1.4 文章接口(独立测试) | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 1.4.1 | 创建文章(Admin) | `POST /api/admin/articles` 带 Bearer Token,返回 `Response`,slug 自动生成 | ☐ | | 1.4.2 | 文章 slug 唯一性 | 同标题创建第二篇文章,slug 自动加后缀(如 `hello-world-1`) | ☐ | | 1.4.3 | 查询文章详情(前台) | `GET /api/articles/{slug}` 无认证可访问,返回结构与 YAML 一致 | ☐ | | 1.4.4 | 文章列表(前台) | `GET /api/articles?page=1&size=10` 返回 `Response>` | ☐ | | 1.4.5 | 状态过滤 | `GET /api/articles?status=PUBLISHED` 只返回已发布文章 | ☐ | | 1.4.6 | 发布文章 | `PATCH /api/admin/articles/{id}/publish` 后,status 变为 PUBLISHED,publishedAt 有值 | ☐ | | 1.4.7 | 归档文章 | `PATCH /api/admin/articles/{id}/archive` 后,status 变为 ARCHIVED | ☐ | | 1.4.8 | 状态机保护 | 对已发布文章再次调用 `publish`,返回业务错误(非 500) | ☐ | | 1.4.9 | 更新文章 | `PUT /api/admin/articles/{id}` 更新后内容正确,updatedAt 变化 | ☐ | | 1.4.10 | 删除文章 | `DELETE /api/admin/articles/{id}` 后,再次查询返回 404 或 null data | ☐ | | 1.4.11 | 未认证访问 Admin | `POST /api/admin/articles` 不带 Token,返回 401 | ☐ | | 1.4.12 | 无权限访问 Admin | 用 VISITOR 角色 Token 访问 Admin 接口,返回 403 | ☐ | ### 1.5 搜索接口(独立测试) | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 1.5.1 | 全文搜索 | `GET /api/articles/search?q=spring` 返回相关文章列表 | ☐ | | 1.5.2 | 搜索结果排序 | 标题含关键词的文章排在内容含关键词的前面(ts_rank 权重 A>B>C) | ☐ | | 1.5.3 | 空关键词处理 | `q=` 或缺失,返回业务错误(code ≠ 0)或空列表 | ☐ | | 1.5.4 | 搜索触发器生效 | 创建文章后立即搜索标题关键词,能搜到结果 | ☐ | ### 1.6 标签接口(独立测试) | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 1.6.1 | 创建标签(Admin) | `POST /api/admin/tags` 返回 `Response` | ☐ | | 1.6.2 | 标签列表(前台) | `GET /api/tags` 返回所有标签,带 `articleCount` | ☐ | | 1.6.3 | 文章关联标签 | 创建文章时传入 `tagIds`,详情页返回正确 `tagNames` | ☐ | | 1.6.4 | 删除标签(Admin) | `DELETE /api/admin/tags/{id}` 成功,关联文章不再显示该标签 | ☐ | ### 1.7 媒体接口(独立测试) | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 1.7.1 | 文件上传 | `POST /api/media/upload` 上传图片,返回 `Response`,url 可访问 | ☐ | | 1.7.2 | 文件本地存储 | `uploads/images/` 目录下存在上传的文件 | ☐ | | 1.7.3 | 目录遍历防护 | 上传时篡改 `directory` 参数为 `../etc`,被拒绝或存储到安全路径 | ☐ | ### 1.8 响应格式一致性 | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 1.8.1 | 所有业务接口返回 `Response` | 抽查 5 个接口,结构均为 `{code, message, data}` | ☐ | | 1.8.2 | 成功时 code=0 | 所有成功响应 code 字段为 0 | ☐ | | 1.8.3 | 错误时 code≠0 | 业务错误(如 slug 重复)code 为对应 ErrorCode 值 | ☐ | | 1.8.4 | 分页接口返回 `PageResult` | 列表接口包含 `list/total/page/size/totalPages` | ☐ | | 1.8.5 | Swagger UI 与 YAML 一致 | Swagger 中显示的接口路径、参数、响应模型与 `openapi.yaml` 完全一致 | ☐ | --- ## 2. Agent2 前端独立验收 ### 2.1 构建与启动 | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 2.1.1 | `npm install` 成功 | 无依赖安装错误 | ✓ | | 2.1.2 | `npm run api:generate` 成功 | `src/generated/api/` 目录存在且包含 api.ts + models/ | ✓ | | 2.1.3 | `npm run dev` 正常启动 | 控制台无 ERROR,访问 `http://localhost:5173` 正常 | ✓ | | 2.1.4 | TypeScript 严格模式无错误 | `npm run build` 通过(或 `vue-tsc --noEmit` 无类型错误) | ✓ | ### 2.2 代码规范检查 | # | 检查项 | 通过标准 | 结果 | |---|--------|---------|------| | 2.2.1 | 无手写 API 类型 | `src/generated/api/` 外无自定义的 `ArticleDetail`/`TokenPair` 等接口 | ✓ | | 2.2.2 | 统一 Axios 客户端 | 全局搜索 `import axios from 'axios'`,除 `client.ts` 外无其他直接引用 | ✓ | | 2.2.3 | ``,前台渲染时不执行脚本 | ☐ | | 5.5 | SQL 注入防护 | 搜索关键词输入 `' OR 1=1 --`,后端正常处理不报错 | ☐ | | 5.6 | 目录遍历防护 | 上传时 `directory=../../etc` 被安全处理 | ☐ | | 5.7 | JWT 安全 | Token 中不包含敏感信息(密码等);Secret 不是硬编码的弱密钥 | ☐ | --- ## 6. 问题记录模板 联调中发现的问题按以下格式记录: ### Issue #1 — 搜索接口复现为后端旧进程所致(已解决) - **发现时间**: 2026-08-10 00:00 - **发现人**: Agent2(前端) - **问题描述**: 初测时 `GET /api/articles/search?q=Spring` 返回 `{code:3001,"文章不存在: search"}`。后端重启(`./gradlew bootRun` 全新进程)后同一请求返回 code=0 正确搜索结果,确认该现象由旧后端进程(未包含 SearchController 的旧构建)造成,非契约/前端问题。 - **复现步骤**: 旧进程下 `curl "http://localhost:8080/api/articles/search?q=spring"` - **预期结果**: 返回搜索结果 - **实际结果**: 重启后返回 `{code:0, data:{list:[...]}}`,2.3.5/1.5.1 通过 - **根因分析**: 后端旧进程加载了过期构建产物,未包含搜索路由 - **责任方**: Agent1(后端,运维) - **修复方案**: 联调前确保后端为最新 `bootRun` 进程 - **验证结果**: ☐ 已解决(2026-08-10 复验通过) ### Issue #2 — 后端 `POST /auth/logout` 未吊销 Refresh Token - **发现时间**: 2026-08-10 00:05 - **发现人**: Agent2(前端) - **影响**: 前端 2.4.6 通过(本地清理 localStorage 正常);仅后端吊销语义缺失(1.3.4) - **问题描述**: 前端 `auth.logout()` 调 `POST /api/auth/logout` 后,旧 refreshToken 仍可通过 `POST /api/auth/refresh` 换取新 TokenPair,令牌吊销语义失效(违反 checklist 1.3.4)。 - **复现步骤**: 1) 登录获取 refreshToken;2) 调 `/auth/logout`(code=0);3) 用旧 refreshToken 调 `/auth/refresh` → 仍返回 code=0 新 token(在最新 bootRun 进程上复验一致) - **预期结果**: logout 后旧 refreshToken 刷新应失败(401/业务错误) - **实际结果**: 刷新成功并返回新的 TokenPair - **根因分析**: 后端登出逻辑未将 refreshToken 加入黑名单/吊销列表(JWT 无状态未记录失效) - **责任方**: Agent1(后端) - **修复方案**: 服务端维护 refreshToken 吊销集合(DB/redis),refresh 时校验 - **验证结果**: ☐ 待修复 ### Issue #3 — 契约路径加 `/api` 前缀后前端 baseURL 适配(已处理) - **发现时间**: 2026-08-10 00:20 - **发现人**: Agent2(前端,承接 Agent1 提示) - **问题描述**: Agent1 将契约路径改为带 `/api` 前缀(`servers.url=http://localhost:8080`)。重新 `npm run api:generate` 后,生成的 localVarPath 变为 `/api/auth/...`、方法名变为 `apiXxx` 前缀、新增 `PublicApi`/`UserApi`。若前端 axios `baseURL` 仍为 `/api`,会拼成 `/api/api/...` → 404。 - **Agent1 提示**: 需把 client.ts 中 `new XxxApi(config, '/api', ...)` 的 `/api` 改为 `''`。经核实**不正确/不完整**:生成的 `common.ts` 拼接逻辑为 `axios.defaults.baseURL ? '' : (configuration?.basePath ?? basePath)`,axios 的 `baseURL` 优先于 XxxApi 的 basePath 参数;且 axios 会自行 combineURLs。若仅改 XxxApi 参数而保留 baseURL=`/api`,仍会双前缀。 - **实际修复**: 将 axios 实例 `baseURL` 从 `/api` 改为 `/`(各 XxxApi 的 `/api` basePath 参数无害保留),并批量重命名方法调用(`articlesGet`→`apiArticlesGet` 等)。实测 `/api/articles` 单前缀、经 vite 代理到 8080 正常,2.3~2.6 全部用例复验 PASS。 - **责任方**: 前端(Agent2) - **验证结果**: ☑ 已处理(2026-08-10 复验 26/26 通过) ```markdown ### Issue #{编号} - **发现时间**: YYYY-MM-DD HH:MM - **发现人**: Agent1/Agent2/架构师 - **问题描述**: - **复现步骤**: - **预期结果**: - **实际结果**: - **根因分析**: - **责任方**: Agent1(后端)/ Agent2(前端)/ openapi.yaml(契约) - **修复方案**: - **验证结果**: ☐ 待修复 / ☐ 已修复 / ☐ 已验证 ``` --- ## 验收签字 | 角色 | 姓名 | 日期 | 签字 | |------|------|------|------| | 架构 Owner | | | | | Agent1(后端) | | | | | Agent2(前端) | | | | --- ## 附录:快速测试命令 ```bash # 后端健康检查 curl http://localhost:8080/actuator/health # 注册 curl -X POST http://localhost:8080/api/auth/register \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 登录 curl -X POST http://localhost:8080/api/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 创建文章(替换 $TOKEN) curl -X POST http://localhost:8080/api/admin/articles \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"title":"Hello World","content":"This is a test article."}' # 搜索 curl "http://localhost:8080/api/articles/search?q=hello&page=1&size=10" # 前台列表(无认证) curl "http://localhost:8080/api/articles?page=1&size=10" ```