# KECE 题解模块落地方案 更新:2026-11-05 · v4。状态:交互 Demo 已认可;后端数据与接口基础已实现,前端页面和正式联调尚未完成。 按已确认的方向实施:**用户打开 Markdown 编辑页,写完直接发布;修改后直接更新。没有审核流程。** 本版保留 v3 的简单产品范围,并细化接入位置、数据约束、开发批次、验收和上线顺序。此前 v1/v2 的审核、分步提示和复杂版本设计不再适用。 ## 1. 使用流程 题目详情 → 题解列表 → 写题解 → 填标题和 Markdown 正文 → 发布题解 → 立即公开。 - 第一版支持算法题 ALGO、数据库题 DB,客观题继续使用现有答案解析。 - 登录用户可以发布,不要求先 AC,不限制一人一题只能写一篇。 - 列表按发布时间倒序,显示标题、作者、时间,复用分页;修改文章不改变首次发布时间。 - 阅读页直接显示完整 Markdown 内容,代码按代码块展示,无提示解锁或二次展开流程。 - 本人可编辑、删除自己的题解;修改成功后读者立即看到新内容。 - 管理员可在发布后隐藏不适合公开的内容,这是事后管理,不是发布前审核。官方标记以后按需要补充,不额外增加发布流程。 ## 2. 编辑页 页面只保留:关联题目、标题输入框、Markdown 编辑区、预览切换和「发布题解」按钮。编辑已有文章时,按钮改为「更新题解」。 沿用已认可 Demo 的编辑/预览切换,保持 KECE 的黑白、直角风格。以现有 MarkdownEditor 和 renderMarkdown 为基础补齐能力,支持代码块、表格、链接和公式;首版不另做并排编辑模式。 输入编辑、预览和阅读均支持代码语法高亮,按 fenced code 的语言标识解析 Python、C/C++、Java、SQL、JavaScript/TypeScript;未标注或不支持的语言回退为纯文本。复用项目已安装的 Prism/Monaco 能力,不增加发布步骤。 标题非空且不超过 80 字;正文非空且不超过 64 KiB UTF-8,前后端同时校验。点击发布直接发送,不再弹投稿检查页。 提交期间按钮禁用;失败保留全部输入并允许重试,成功跳转到题解详情。未发布输入只保留在当前页面,离开前提示,首版不建设服务端草稿、自动保存或历史修订系统。 首版不做导读、提示字段、评论、点赞、附件上传、审核后台和站内通知。 ### 后台:题解管理 在管理后台侧栏增加「题解管理」,首版仅 ADMIN 可访问;教师管理权限留到后续。这里管理已发布内容,不出现待审核、通过审核或退回投稿。 - **筛选区**:标题/作者关键词搜索、独立题号输入(精确匹配)、题型(全部/算法/SQL)、状态(全部/已发布/已隐藏)。题号相同时按题型区分;修改筛选回到第 1 页,复用每页 20 条及跳页组件。 - **横向列表**:标题、关联题目(题型 + 题号 + 名称)、作者、更新时间、状态、操作。按更新时间倒序、ID 倒序排列;默认不展示已删除内容,不增加无实际用途的统计卡片。 - **查看**:点击标题或「查看」展开页内全文区,保留当前筛选与列表,关闭即可继续管理;展示题目关联、作者、发布时间及 Markdown 全文,代码高亮与前台一致。窄屏同样使用全宽详情,不挤压列表列宽。 - **隐藏/恢复**:已发布行显示「隐藏」,已隐藏行显示「恢复公开」。操作直接生效,无审批;隐藏后即使作者修改正文也不能自动公开。恢复前服务端确认题目仍存在且公开、文章未删除。 - **删除**:二次确认中显示文章标题,确认后软删除;取消不修改数据。首版不提供批量删除。隐藏可附简短原因,原因不是必填审核意见。 - **反馈状态**:操作中禁用当前按钮,成功后更新该行;失败保留列表并提示重试。空结果提供清空筛选,读取失败与空列表区分。 后台列表和全文示例仅用于交互演示,演示中的隐藏、恢复、删除不操作生产数据。 ## 3. 一张数据表 新增 solution 表: | 字段 | 用途 | | --- | --- | | id、question_type、question_id | 文章及关联题目;题型和题号共同定位,避免 ALGO/DB 题号冲突 | | author_id、title、content_md | 作者、标题、原始 Markdown 正文 | | status | PUBLISHED / HIDDEN,创建成功即 PUBLISHED | | version | 乐观锁序号,只用于防止多窗口覆盖,不是内容版本库 | | client_request_id、request_hash | 创建请求幂等键,按作者唯一;服务端保存首次创建内容的 SHA-256 指纹,超时重试不重复发文 | | last_management_reason | 可空,记录最近一次管理员隐藏的简短原因,不新增审核或审计流程表 | | created_at、updated_at、deleted_at | 发布时间、更新时间、软删除时间 | 列表索引:(question_type, question_id, status, deleted_at, created_at, id);作者索引:(author_id, created_at, id)。不增加修订表、审核表或举报流程表。 同一创建幂等键、同一请求指纹重试返回原文章及其当前状态;题目、标题或正文不同则返回冲突。request_hash 创建后不变,不能用文章修改后的正文判断原始请求是否重复。所有变更都带 version,成功后递增;冲突提示用户重新读取并保留本地文字,不自动覆盖。 ## 4. 接口 沿用 /api/v1 与现有 ApiResponse;以下省略前缀。 | 接口 | 用途 | | --- | --- | | GET /solutions?questionType=ALGO&questionId=1&page=1&size=20 | 题目下的公开题解列表 | | GET /solutions/{id} | 读取详情;隐藏内容仅本人/管理员可查看 | | POST /solutions | 登录用户直接发布,authorId 取当前身份 | | PUT /solutions/{id} | 本人修改,携带 version;不允许修改题目、作者和状态 | | DELETE /solutions/{id}?version=1 | 本人或管理员软删除,带当前 version | | GET /me/solutions | 当前用户的题解列表及编辑入口 | | GET /admin/solutions?keyword=&questionId=&questionType=&status=&page=1&size=20 | ADMIN 管理列表;关键词匹配标题/作者,题号精确匹配,支持题型、发布/隐藏状态及分页;不返回正文 | | PATCH /admin/solutions/{id}/status | ADMIN 隐藏/恢复,携带 version;隐藏可带 reason,写入 last_management_reason | 隐藏文章即使由作者修改也保持 HIDDEN,不能通过普通更新解除隐藏。已删除文章不能再更新或恢复为公开。 管理员删除复用 DELETE /solutions/{id},首版不增加删除原因输入;隐藏原因仅在管理员状态接口接收。所有管理操作在服务端校验 ADMIN 身份,不能只靠菜单隐藏。 ## 5. 必要的实现规则 - 公共列表和详情同时检查:题目存在且公开、文章 PUBLISHED、未删除。题目转私有或删除后,公共题解立即不可读。 - 私有题不允许公开发布题解;课练口令和考试上下文不赋予题解发布/阅读权限。本人可查看自己曾发布的正文,但不因此获得私有题面、题名或标准答案,关联位置显示「题目当前不可用」。 - JWT 验证登录身份,服务端校验所有权;不能依赖前端隐藏按钮实现权限。管理员恢复仍需检查题目公开状态。 - Markdown 预览与阅读使用同一净化规则,禁止脚本和危险链接;不执行正文中的代码,不自动读取 SQL 标准答案或他人提交。 - 先直接使用 MySQL 索引查询,列表不加载正文,作者信息批量查询;无需新增 Redis 正文缓存、消息队列或搜索服务。 - 返回作答页不自动填入题解代码,不覆盖已有作答草稿。接口失败或版本冲突时保留编辑内容。 ## 6. 页面落位与导航契约 题解入口放在具体题目内,阅读、写作沿用前台侧栏;个人主页负责本人管理,后台负责内容管理。不新增全站「题解广场」。 | 页面 | 路由约定 | 接入方式 | | --- | --- | --- | | 算法题题解列表 | `/questions/:id?tab=solutions&page=1` | 现有题目详情增加「题目 / 题解(n)」切换,标题区和作答入口保留 | | 数据库题题解列表 | `/database/:id?tab=solutions&page=1` | 与算法题共用列表组件,传入 DB 和题号 | | 写题解 | `/solutions/new?questionType=ALGO&questionId=4` | 只填写标题、正文;关联题目从路由确定,提交时服务端再次校验 | | 阅读题解 | `/solutions/:id` | 全文、作者、关联题目、返回列表和作答入口;本人显示编辑、删除 | | 编辑题解 | `/solutions/:id/edit` | 从详情回填正文与 version;题目关联只读 | | 我的题解 | `/profile?tab=solutions&page=1` | 仅本人主页出现此页签;不扩展其他用户的隐藏内容入口 | | 后台题解管理 | `/admin/solutions` | ADMIN 专属路由及侧栏项,筛选、分页、页内全文和管理操作 | 导航行为: 1. `router/index.ts` 增加懒加载页面;前台页面放在 DefaultLayout 下,后台页面放在 AdminLayout 下。题解写作路由设置 requiresAuth,后台设置 ADMIN;直接访问 URL 同样受保护。 2. 阅读、编辑页保留确定的返回来源:题目列表或本人列表及其页码;只接受已知来源值,不接受任意外部 returnUrl。刷新后仍能定位关联题目,缺失来源时回到该题的题解列表。 3. 从题集、课练进入时,仅透传已知的 `setId/itemId/practiceCode/pqId`,用于返回和作答。题解 API 不接收这些参数作为访问授权;独立考试路由不增加题解入口。 4. 私有题即使可通过课练口令查看,也不展示公共题解入口。公开题目参与课练时仍按公开题规则读取;隐藏入口不能替代服务端校验。 5. 列表页码写入 URL;发布成功返回列表时重新读取,数量来自同一分页响应的 total。加载失败不伪装成「0 篇」;删除当前页最后一条后回退到有效页码。 6. 题型、题号、页码变化时刷新对应数据,并取消或丢弃旧请求结果;只切换题目/题解页签不重复加载题面。 7. 返回作答仅执行路由跳转。保留现有代码草稿键 `kece:code_draft:q_{id}_{language}` 和 SQL 草稿键 `kece:sql_draft:q_{id}` 的行为,不写入题解示例代码。 ## 7. 现有代码接入清单 以下为当前仓库核对结果;标为「新增」的文件尚不存在,不代表功能已经实现。 | 现有入口 | 本次职责 | | --- | --- | | `frontend/src/router/index.ts`、`layouts/DefaultLayout.vue` | 注册题解路由、标题和面包屑,复用当前站点外壳 | | `frontend/src/views/QuestionDetailView.vue`、`DatabaseQuestionDetailView.vue` | 接入题解页签;补齐同组件切换题号后的加载和竞态保护 | | `frontend/src/views/ProfileView.vue` | 扩展 TabKey、URL 页签识别和本人判断;将题解列表下沉到独立组件 | | `frontend/src/layouts/AdminLayout.vue` | 增加仅 ADMIN 可见的管理项;不沿用部分旧页面 ADMIN/TEACHER 共用权限 | | `frontend/src/components/MarkdownEditor.vue` | 保持 v-model 接口及既有调用默认行为,增加题解使用的切换布局和源码高亮模式 | | `frontend/src/components/CodeEditor.vue`、`components/monaco-setup.ts` | 复用编辑器基础能力,补充 Markdown 及 fenced code 语言支持,检查模型/监听器释放 | | `frontend/src/utils/markdown.ts`、`markdown.test.ts` | 在现有 marked + KaTeX + DOMPurify 流程中补齐代码高亮及回归测试 | | `frontend/src/api/request.ts`、`components/Pagination.vue`、`composables/useToast.ts`、`useConfirm.ts` | 复用请求解包、ApiError、分页、提示和删除确认 | | `backend/.../common/security/AuthInterceptor.java`、`AuthContextHolder.java`、`RequireRole.java` | 公开读可选登录、写入强制登录、后台强制 ADMIN;Service 再检查所有权 | | `backend/.../common/response/ResultCode.java`、`PageParam.java`、`PageResult.java` | 复用业务码和分页包装,新增冲突码;题解分页限制极大 offset | | `backend/.../user/mapper/UserMapper.java` | 通过 findByIds 批量装配作者信息,避免逐行查用户 | | `backend/src/main/resources/db/migration/` | 当前最高 V23;新增暂定 V24__solution.sql,编码时重新确认序号 | | `backend/src/test/java/com/kece/AbstractIntegrationTest.java` | 复用 MySQL + Redis Testcontainers 和 MockMvc 集成测试基座 | 建议新增结构: ```text backend/src/main/java/com/kece/solution/ controller/ SolutionController、MySolutionController、AdminSolutionController service/ SolutionService、SolutionQuestionAccess entity/ Solution dto/ 创建/更新/状态请求、列表项、详情响应 mapper/ SolutionMapper、SolutionQuestionMapper backend/src/main/resources/mapper/ SolutionMapper.xml、SolutionQuestionMapper.xml frontend/src/api/solution.ts frontend/src/types/solution.ts frontend/src/components/solutions/ QuestionSolutionList.vue、MySolutionList.vue、SolutionArticle.vue frontend/src/views/ SolutionReadView.vue、SolutionEditView.vue frontend/src/views/admin/AdminSolutionsView.vue ``` 其中 SolutionQuestionAccess 只负责 ALGO/DB 的存在性和公开性,不复用题面详情缓存、不读取测试数据或 SQL 标准答案。SolutionArticle 统一负责正文展示,供前台阅读和后台全文使用;预览也调用相同 renderMarkdown。页级组件负责取数和导航,避免把所有逻辑堆进现有题目页和个人主页。 ## 8. 编码前固定的数据与接口细节 ### 8.1 数据约束 - `content_md` 使用 MEDIUMTEXT,应用层限制 65,536 个 UTF-8 字节,避免 TEXT 的字节边界与产品限制不一致。前端用 TextEncoder,后端按 UTF-8 编码长度校验。 - `title` 去除首尾空白后非空,最长 80 个 Unicode 码点;前后端口径一致,不仅依赖 HTML maxlength。正文只用空白判断是否为空,不擅自改写代码缩进。 - `client_request_id` 使用 UUID,建立 `UNIQUE(author_id, client_request_id)`;`request_hash` 使用固定长度 64 字符,保存服务端对固定字段顺序的创建 payload 计算的摘要。 - `version` 从 1 开始。创建后题型、题号、作者、幂等键和 request_hash 不可变。创建时间不因编辑、隐藏或恢复而改变。 - 在原列表索引之外,将本人查询索引明确为 `(author_id, deleted_at, created_at, id)`,后台默认排序索引为 `(deleted_at, updated_at, id)`。是否另加状态筛选索引以 EXPLAIN 和数据量为依据。 - 原算法题、数据库题目前均物理删除;不增加指向两种表的多态外键,不修改原题表删除方式。读取时检查关联源表;题目消失后保留作者已有正文,但公共接口不暴露内容。 - 列表只投影元信息,不查 content_md;列表与 count 使用一致的权限条件,不能先分页再在 Java 中过滤隐藏项。 ### 8.2 DTO 与错误码 | 请求/响应 | 固定内容 | | --- | --- | | 创建请求 | `questionType, questionId, title, contentMd, clientRequestId` | | 更新请求 | `title, contentMd, version`;不得携带可写的 authorId、questionId、status | | 状态请求 | `status, version, reason?`;status 仅 PUBLISHED/HIDDEN,reason 最多 200 个码点 | | 分页响应 | 复用 `list, total, page, size`,默认 20、最大 100;先用 long 校验 offset 范围再转现有参数类型 | | 列表项 | `id, questionType, questionId, questionTitle, authorId, authorName, title, status, version, createdAt, updatedAt`,以及派生的题目是否可公开信息;无正文 | | 详情 | 列表元信息 + `contentMd`;管理原因仅返回作者或 ADMIN,普通读者不获得内部字段 | | 创建/更新/状态成功 | 返回最新详情和 version;前端以服务端返回状态为准,不自行假设一定公开 | 目前项目业务异常使用 **HTTP 200 + ApiResponse.code**,不是 HTTP 403/409。题解沿用现有协议:成功 200、参数 4001、未登录 4002、禁止操作 4003、不可读取 4004、系统错误 5000;新增 `CONFLICT=4009` 表示版本或幂等冲突。前端通过已有 ApiError.code 分支处理,本次不改写全站异常协议。 公开读取对不存在、已删除、无权查看的隐藏文章或私有题关联文章统一返回 4004。本人/管理员可读正文的例外按下一节执行。访问公开 GET 时失效 token 目前会被当作游客;所有写接口必须带登录要求,不能依赖可选身份解析。 ### 8.3 可见性和状态 | 情况 | 公众 | 作者 | ADMIN | | --- | --- | --- | --- | | 公开题 + PUBLISHED + 未删除 | 可读 | 可读、编辑、删除 | 可读、隐藏、删除 | | 公开题 + HIDDEN + 未删除 | 不可读 | 可读、编辑、删除;不能自行恢复 | 可读、恢复、删除 | | 关联题私有或已删除 + 文章未删除 | 不可读 | 可读已有正文、编辑、删除;不展示受限题名/题面 | 可管理正文;不能恢复公开 | | 文章已删除 | 不可读 | 不可读、不可改、不可恢复 | 首版不提供回收站或恢复 | 教师在题解管理中与普通登录用户相同,只能管理自己的文章。ADMIN 不通过普通更新接口代改他人正文。题目变私有不修改文章 status;题目重新公开后,原 PUBLISHED 文章恢复可读,原 HIDDEN 文章仍保持隐藏。 ### 8.4 重试与竞争处理 1. 新建编辑页在首次发布时生成请求键,保存本次提交 payload。网络超时后保留同一键和 payload,以原请求重试;未确定结果前不生成新键再次发同一篇。可以继续编辑本地文字,但应先确认旧请求结果,再将新内容作为原文章更新。 2. 服务端以数据库唯一约束兜底。两个相同请求并发插入时,只产生一行;撞唯一键后按原 hash 比对。同键不同内容返回 4009,不当作系统错误。 3. 原文章已被编辑时,重试创建仍按不可变 request_hash 判断,返回原文章当前状态;原文章已删除时返回明确冲突,不能复活或补建。 4. 更新、隐藏、恢复、删除均使用 `WHERE id=? AND version=? AND deleted_at IS NULL`,成功时 version 加 1,并检查受影响行数。作者更新 SQL 不写 status。 5. 写入公开题解、恢复公开在事务中锁定并检查关联题目行,固定先题目后文章的锁顺序;普通正文编辑不更改公开性。公共读取仍实时判断关联题目,不使用可能过期的题面详情缓存。 6. 发生版本冲突,编辑页保留本地全文,展示「内容已在其他位置更新」并提供复制本地内容、读取最新版本。读取最新版本覆盖输入前须确认;不静默替用户合并或覆盖。 7. 题解读写失败不影响判题和其他页面。首版不增加正文缓存、异步写入、消息队列或全文搜索服务;日志只记录操作人、文章 ID、结果和耗时,不打印正文或 token。 ## 9. 按六个批次开发 各批次可独立提交和本地验收;正式开放入口以六批均完成为准。后端基础与编辑器可以并行,前台联调依赖两者,后台随后接入,最后统一回归与发布。 ### 第 1 批:后端数据与权限底座 - [ ] 固定第 8 节 DTO、状态和业务码,补 Solution 类型、请求校验及 OpenAPI 注解。 - [ ] 新增 Flyway 迁移、实体和 Mapper:分页、count、详情、插入、本人列表、后台列表、版本条件更新。 - [ ] 完成只读取题目 ID/标题/status 的 SolutionQuestionAccess;所有公开查询同时约束文章和关联题目。 - [ ] 完成直接发布、更新、删除、幂等和并发控制;后台隐藏/恢复复用同一服务规则。 - [ ] 建立 SolutionIntegrationTest 和 SolutionConcurrencyIntegrationTest,验证两种题型同号不串数据。 **完成标准:** 不依赖前端即可用接口跑通发布→公开读取→修改→隐藏→作者修改仍隐藏→恢复→删除;越权、重试和并发用例通过。迁移能在空测试库和现有 V23 测试库上执行。 ### 第 2 批:Markdown 编辑与渲染基础 当前 MarkdownEditor 是普通 textarea,renderMarkdown 只有 Markdown/公式渲染与净化;首页局部 Prism 样式不会自动覆盖题解。高亮需真实实现,不能只添加语言标签。 - [ ] 给 MarkdownEditor 增加可选的切换布局和高亮输入模式,题解启用;保留旧调用的默认布局和 v-model 行为。 - [ ] 高亮输入优先复用现有 Monaco CodeEditor,以异步组件仅在编辑页加载;注册 Markdown,验证 fenced code 内的 Python、C/C++、Java、SQL、JS/TS 分词及语言别名。普通阅读不能加载 Monaco。 - [ ] 预览/阅读在 renderMarkdown 统一配置 Prism 代码块渲染,再执行 DOMPurify;保留现有 KaTeX 配置。未标注或未知语言安全退回纯文本。 - [ ] 增加统一正文高亮样式;代码保留换行与缩进,超长行只在代码块内滚动,不撑宽整页。 - [ ] 编辑/预览切换保留正文、光标和撤销记录;中文输入法组合输入不中断。组件退出释放编辑器 model、监听器和观察器。 - [ ] 更新 markdown.test.ts,检查表格、链接、公式、代码、恶意标签/危险 URL;补编辑/预览交互用例,并回归已有后台 Markdown 编辑页面。 **完成标准:** 编辑、预览、阅读同一份内容均有关键字高亮,复制出的原始源码与输入一致;不能为修复高亮放宽安全净化。正文长度上限附近仍可输入和预览,不随每次按键重建 Monaco。 ### 第 3 批:题目入口、阅读、直接发布 - [ ] 新增 solution API 模块和 TS 类型,复用 http 与 ApiError,不再创建第二套 Axios 实例。 - [ ] 接入两类题目页的页签、题解列表、数量、分页和空态;首屏并行获取第一页轻量列表,切换页签直接复用,全文按需加载。 - [ ] 实现阅读页和新建/编辑共用页面:题目关联、标题、Markdown、预览、发布/更新。 - [ ] 处理提交中防重复、失败保留输入、4009 冲突、重新登录后的重试;只有明确成功才清除 dirty 状态并跳转。 - [ ] 用路由离开/参数更新守卫和 beforeunload 处理未发布修改;切换预览不触发离开确认。不增加题解自动保存。 - [ ] 完成题集/课练上下文透传、请求竞态保护、受限关联题目的降级显示及返回作答验证。 **完成标准:** ALGO、DB 各完成一次真实发布→列表出现→全文读取→编辑更新→返回原题作答;发布无需审核。输入保留、刷新深链接、前进后退和失败重试均可用。 ### 第 4 批:本人管理与后台管理 - [ ] ProfileView 的本人页签接入 MySolutionList;隐藏内容显示原因和状态,题目不可用时禁用题目跳转但可管理本人正文。 - [ ] 本人删除确认显示准确标题;成功后同步列表与数量;删除失败保留当前内容。 - [ ] 完成 ADMIN 侧栏、路由和列表,标题/作者搜索、题号精确检索、题型/状态筛选与跳页。 - [ ] 后台全文使用 SolutionArticle,在列表下方全宽展开;关闭保留筛选和页码。 - [ ] 隐藏、恢复、删除均使用当前 version;操作中只禁用相应行。完成后刷新列表和已打开详情,处理并发改动及页码收缩。 - [ ] 搜索提交/筛选变化回到第一页,取消或忽略旧查询;空态、读取失败、操作失败分别显示。 **完成标准:** 普通用户、教师访问后台页面和直接调用管理接口均被拒绝;管理员隐藏后公开列表、数量、详情一致变化;取消删除不改数据,作者修改不能解除隐藏。 ### 第 5 批:联调、回归与结构清理 - [ ] 按第 10 节逐条执行接口、前端交互和真实浏览器验收,记录结果与失败修复。 - [ ] 检查 100% 缩放下 1920、1440、1092、768、390、320px 视口:侧栏、正文、编辑器、按钮和分页无整页横向溢出;操作不依赖悬停。 - [ ] 检查键盘焦点、编辑/预览按钮状态、错误提示、删除取消和登录失效;题解加载失败时题面和作答仍可用。 - [ ] 用样本数据检查公开/本人/后台查询的 EXPLAIN;记录不同文章数量下的查询耗时与响应大小,确认列表不携带正文、无作者 N+1。关键词包含搜索是否需专门索引根据结果决定,不先加搜索服务。 - [ ] 清理临时 mock、占位路由、重复样式、无用 imports 和旧审核设计残留;Demo 的模拟发布逻辑不进入生产。 - [ ] README 与 CHANGELOG 仅记录已经通过验收的功能;方案中的完成勾选与测试记录保持一致。 **完成标准:** 新增逻辑和受影响旧页面全部通过检查,生产包不包含示例文章/假发布;没有未解释的跳过用例或构建失败。 ### 第 6 批:预发布验证与正式上线 - [ ] 在测试环境完成数据库备份恢复验证、V24(或实际新序号)迁移、后端部署、前端部署和深链接刷新验证。 - [ ] 用游客、作者、其他用户、教师、管理员账号执行最小闭环;测试数据使用专用账号和可识别标题。 - [ ] 发布前保留现有前端产物、后端包、版本号和数据库备份;先部署兼容旧前端的后端和加表迁移,后端验证通过后再发布前端入口。 - [ ] 上线检查读写错误、冲突处理、查询耗时,以及原题库/作答入口;核对浏览器加载的是新资源,无缺失文件。 - [ ] 如需回滚,先撤回前端入口再回滚应用包;新增表和已写题解保留,不以 DROP TABLE 回滚。测试环境先验证旧应用可与新 schema/Flyway 历史共存;若校验不允许,修正发布配置或采用向前修复,不临时删除迁移记录。 **完成标准:** 线上发布、查看、编辑、本人管理和管理员隐藏恢复均实测可用;原作答链路无回归,回退步骤经过测试。正式部署是开发完成后的单独执行步骤,本次尚未执行。 ## 10. 验收清单与执行命令 | 编号 | 场景 | 通过条件 | | --- | --- | --- | | A01 | 游客打开公开题解;游客尝试发布 | 可读全文,写入返回未登录;登录后返回正确关联题目 | | A02 | 同号算法题与数据库题 | 列表、数量、发布目标、返回路由完全隔离 | | A03 | 本人发布、修改;他人伪造作者/状态字段 | 正常发布立即可读;修改保持原 ID/首次发布时间;越权字段不能生效 | | A04 | 管理员隐藏后作者编辑 | 文章仍 HIDDEN,公众列表、total、详情均不可读取 | | A05 | 题目转私有、物理删除、重新公开 | 后续公共读取实时遵循第 8.3 节;不泄露题名/正文/标准答案,不依赖旧详情缓存 | | A06 | 创建同时重试、原文编辑后重试创建、删文后重试 | 只生成一条;按原 hash 识别;不会复活文章 | | A07 | 两窗口更新、隐藏与编辑/删除竞争 | 两个真实并发事务只有符合版本者成功;本地正文不丢失 | | A08 | 超时、断网、登录失效、接口报错 | 页面保留标题和正文;重试沿用正确请求身份;不能显示虚假成功 | | A09 | 高亮、未知语言、HTML/script、危险 URL、公式 | 三处高亮有效,源码不改写,危险内容不能执行,公式和表格保持正常 | | A10 | 题解列表→阅读→返回;题集/课练→题解→作答 | 原题型、题号、列表页码和上下文正确;原作答草稿不被覆盖 | | A11 | 我的题解、后台权限、筛选分页与删除末行 | 本人内容不串到他人主页;教师无管理权;题号精确检索,页码不滞留空页 | | A12 | 宽窄屏、100% 缩放、中文输入、键盘操作 | 全部主要操作可达,正文可读,代码块局部滚动,焦点可见 | 自动化分工:后端集成测试负责 A01–A07 的权限、状态、幂等和真实并发;Vitest 负责 Markdown、API 错误处理、编辑状态和列表交互;真实浏览器覆盖 A08–A12 的布局、导航、输入法和登录体验。并发测试不能用同线程顺序调用代替。 前端在 `frontend` 目录执行,与当前 CI 一致: ```powershell npm run lint npm run test:unit -- --run npm run build ``` 后端在 `backend` 目录执行;新增测试先定向执行,通过后执行一次完整检查: ```powershell .\mvnw.cmd -B spotless:check .\mvnw.cmd -B test .\mvnw.cmd -B package -DskipTests ``` 集成测试依赖可用的 Docker/Testcontainers MySQL 与 Redis;MockMvc 测试路径沿现有基座不加 `/api/v1`,生产由 context-path 添加前缀。Docker 不可用时记录为「集成测试未执行」,不能将跳过当作通过。题解不改判题机,无须为文档更新运行判题机测试。 ## 11. 当前开发状态 - 已确认:简单 Markdown 直接发布模式、前台入口和完整交互 Demo、后台管理方向。 - 已实现:V24 题解表、公开/本人/ADMIN 接口、关联题目公开性检查、创建幂等键、版本条件更新、软删除;算法题与数据库题的题目页入口、题解列表、阅读、新建/编辑、本人列表和 ADMIN 管理页;Markdown 编辑器语言分词,以及预览/阅读 Prism 代码高亮。 - 已验证:后端 Java 17 编译、Spotless 格式检查、后端跳过测试打包;前端 ESLint(0 error,仓库既有 23 条 warning)、类型构建和 Markdown 高亮单测(3 项)。 - 已部署:2026-11-05 已在生产环境完成后端包与前端原子发布;Flyway V24 已成功迁移,`solution` 表已创建,公开题解 API、首页与 `/admin/solutions` 均返回 200。发布前已保存数据库 SQL 压缩备份、后端 jar 备份与前端旧版本目录。 - 尚未执行:带登录身份的真实浏览器全流程与多视口验收。Docker 守护进程未运行,Testcontainers 集成测试尚无法执行;不能将其记为通过。