结合后端实际(
serve_template题目/比赛模块已全部实现并运行中,Swagger 在线:8080/v3/api-docs)与前端现状(仅完成 auth + 用户管理)。 实施节奏:一次性完成 Phase 0–5,最后统一npm run build+ 手工联调。 提交风格沿用仓库实际约定:# feat: .../# api: .../# style: ...(todo.md 中的Commit(ui):为标注,不作为 commit message 前缀)。
| 事项 | 结论 |
|---|---|
| API 生成 | 后端已全部完成,只需在 Phase 1 开头跑一次 npm run api:gen,一次性生成 problem-controller.ts / category-controller.ts / contest-controller.ts / user-controller.ts / auth-controller.ts + 全部 models |
| 生成物约束 | src/api/models/ 与 src/api/*.ts 由 orval clean:true 生成,禁止手改;所有手写扩展类型放 src/type/ 或组件内 |
| 类型缺口 | Swagger 将 GET /contest/{id} 声明为 Response<ContestVo>,真实返回 ContestDetailVo(继承 ContestVo,多 description/creatorUsername/registered/problems);ContestProblemVo 无 schema。必须手写扩展类型并强转 |
| 响应体 | 统一 Response<T>:code 200=成功,400=错误;axios-instance.ts 已兼容 code 0/200,无需改动 |
| 角色 | 后端角色常量:user / coach / admin(User.java)。教练相关接口均用 hasAnyRole('ROLE_coach','ROLE_admin') |
| 状态/难度/类型常量 | 比赛状态 pending/running/ended;比赛类型 public/private/password;题目难度 easy/medium/hard;isPublic、isHidden 均为 0/1 |
src/type/index.ts
User.role 联合类型由 'admin' | 'user' | 'guest' 改为 'admin' | 'coach' | 'user' | 'guest'(当前缺 coach,是 TS 硬伤,登录后角色即来自 AuthTokenVo.role)。src/router/index.ts
requiresCoach 语义:admin 或 coach 可访问;requiresAdmin 保留为仅 admin。路由守卫 beforeEach 增加分支:
if (to.matched.some(r => r.meta.requiresAdmin)) -> 仅 admin,否则拒绝
else if (to.matched.some(r => r.meta.requiresCoach)) -> admin 或 coach,否则拒绝
未登录一律跳 /login?redirect=fullPath
adminRoutes 重构:
/admin)meta 去掉 requiresAdmin,改为 requiresCoach(否则 coach 进不了后台)。requiresAdmin(仅 admin)。/admin/other(组件为 AdminLayout 嵌套自身,属隐藏 bug)。redirect: '/admin/problems'(Phase 2 注册后生效;暂可先指 /admin/users)。src/layout/AdminLayout.vue
menus 计算属性按角色过滤子路由:
meta.requiresAdmin → 仅 admin 显示;meta.requiresCoach → admin|coach 显示;src/layout/UserLayout.vue
isAdmin 改为 isStaff = computed(() => ['admin','coach'].includes(role));v-if="isStaff"。npm run build 通过。# feat: 路由与布局支持coach后台入口
npm run api:gencurl http://localhost:8080/v3/api-docs 返回 200)。src/api/problem-controller.ts、src/api/category-controller.ts、src/api/contest-controller.ts;models 新增 problemVo、problemDetailVo、problemDto、problemCaseVo/Dto、problemTestVo/Dto、categoryVo/Dto、contestVo/Dto、contestProblemDto、contestRegisterDto、contestRegistrationVo、rankVo、pageVoListProblemVo、pageVoListContestVo、responseContestVo 等。src/view/problem/ProblemListView.vue(/problems)GET /category → CategoryVo[])、公开状态(isPublic 0/1,可空)。GET /problem,参数 title/difficulty/categoryId/isPublic/pageNum/pageSize → ResponsePageVoListProblemVo。UserView.vue 的 el-pagination 模式。src/view/problem/ProblemDetailView.vue(/problems/:id)GET /problem/{id}(游客可访问,SecurityConfig 已开放)→ ProblemDetailVo。timeLimitMs(转秒/ms 展示)、memoryLimitMb、tryCount/acceptedCount、description、分类标签、样例列表 cases(input/output/description/sortOrder)。createdByUsername 可作作者展示。/problems、/problems/:id(meta 带 title 与 icon,icon 可选用 Document / Files)。menus 逻辑复用 userRoutes[0].children)。# api: 题目与比赛模块api(通过orval生成)(Step 1.1 单独提交)# feat: 题目列表与详情页src/view/admin/ProblemManageView.vue(/admin/problems)UserView.vue:搜索卡 + 工具栏 + 表格 + 弹窗 + 分页。ProblemDto):标题、描述(textarea)、时间限制(ms)、内存限制(MB)、难度、是否公开;分类勾选 categoryIds: number[](el-checkbox-group / el-select multiple)。GET /problem/{id} 的 cases;操作 POST /problem/{id}/case(body ProblemCaseDto 含 problemId)、PUT /problem/case、DELETE /problem/case?id=。字段:input/output/description/sortOrder。GET /problem/{id}/test → ProblemTestVo[];操作 POST /problem/{id}/test、PUT /problem/test、DELETE /problem/test?id=。字段:input/output/sortOrder、isHidden 隐藏标记(0/1,el-switch)。该接口仅 coach/admin 可见。DELETE /problem 为 hasRole('ROLE_admin'))。src/view/admin/CategoryManageView.vue(/admin/categories)GET /category、POST /category、PUT /category(body CategoryDto:id/categoryName/description)、DELETE /category?id=(单 id 查询参数,与 user 的 ids 数组签名不同,以 orval 生成为准)。/admin/problems、/admin/categories,meta 标 requiresCoach + icon(Collection 等)。# feat: 题目管理与分类管理后台页面
src/type/contest.ts)import type { ContestVo } from '@/api/models'
export interface ContestProblem {
problemId: number
title: string
difficulty: string
displayOrder: number
score: number
}
export interface ContestDetail extends ContestVo {
description?: string
creatorUsername?: string
registered?: boolean
problems?: ContestProblem[]
}
组件内强转:const detail = res.data as unknown as ContestDetail(生成类型 ResponseContestVo.data 为 ContestVo)。
src/view/contest/ContestListView.vue(/contests)GET /contest(游客可访问,仅见 public 型)→ ResponsePageVoListContestVo。YYYY-MM-DD HH:mm)、状态标签、分页。src/view/contest/ContestDetailView.vue(/contests/:id)/contest/{id} 未对游客开放):未登录跳 /login?redirect=...;401 由 axios 拦截器兜底跳登录。GET /contest/{id}(不带 password);type==='password' 且返回体 无 problems 字段(锁定态 base VO)→ 弹密码框 → 带 ?password= 重拉;status 为 pending/running 且 registered !== true。POST /contest/{id}/register,body ContestRegisterDto { password? }(密码型需再次传密码,后端会校验)。registered。/contests/:id/ranking。src/view/contest/ContestRankingView.vue(/contests/:id/ranking)GET /contest/{id}/ranking(需登录)→ RankVo[](userId/username/totalScore/maxAcMinutes/regTime)。GET /contest/{id}。/contests、/contests/:id、/contests/:id/ranking;菜单加"比赛"(icon Trophy 等)。# feat: 比赛列表、详情与排名页面
src/view/admin/ContestManageView.vue(/admin/contests)ContestDto):
el-date-picker type="datetime" value-format="YYYY-MM-DDTHH:mm:ss",对齐后端 LocalDateTime 反序列化(ISO 格式)。GET /problem(分页 + 标题搜索 + 多选)选题目;displayOrder(显示序号)+ score(分值);POST /contest/{id}/problem(新增)/ PUT /contest/{id}/problem(改)/ DELETE /contest/{id}/problem?problemId=(移除),body ContestProblemDto { problemId, score, displayOrder }。GET /contest/{id}(含 problems)回显。GET /contest/{id}/registrations(coach/admin)→ ContestRegistrationVo[](id/userId/username/createdAt)表格,独立弹窗或 Tab。demand.md),操作失败按后端消息提示。/admin/contests,meta requiresCoach;侧边菜单自动渲染。# feat: 比赛管理后台页面
npm run build(vue-tsc 严格模式)全绿。noUnusedLocals/noUnusedParameters 严格报错;ContestDetail 强转写法;orval 生成类型使用是否正确。npx prettier --write . 统一格式。npm run dev,需后端在线)完整链路:
/admin/** 被拒;coach 访问 /admin/users 被拒。/home/yangyi/OJ/AGENTS.md("仅 User 域实现"已过时,题目/比赛模块已上线)。/contest/{id} 类型不符:Swagger 类型是 ContestVo,真实返回 ContestDetailVo,必须用 src/type/contest.ts 扩展 + 强转,不能依赖生成 models。coach 角色缺失:src/type/index.ts 未含 coach,Phase 0 必须补,否则登录为 coach 时 TS 报错。YYYY-MM-DDTHH:mm:ss,后端 LocalDateTime 才可反序列化;展示时 ISO → 可读格式。ids 数组;category/case/test 删除用单个 id 查询参数,按 orval 生成签名调用。problems 字段区分 base VO 与 detail(Swagger 未提供显式标记)。api:gen 前置:需后端在线;clean:true 会重写全部 controller/models,属正常。/admin 无默认页:Phase 0 加 redirect;点击 UserLayout"后台管理"应先跳到默认后台页。RankVo[] 空数组给空态 UI 即可。