|
|
@@ -0,0 +1,92 @@
|
|
|
+# 核验报告:「活动重复报名」修复
|
|
|
+
|
|
|
+> 核验对象:修复提交 `94e093949 fix(frontend): 活动重复报名时业务错误应展示友好提示而非"网络异常"`(HEAD)及被调用的既有后端代码。
|
|
|
+> 原则:独立读码核验,不信任对方报告(`docs/superpowers/plans/verify/fix-duplicate-signup.md`),一切以代码实际为准。
|
|
|
+> 只做核验与必要修正,未触碰 BASIC 会员配额文件,未手改 dist/unpackage。
|
|
|
+
|
|
|
+## 核验结论
|
|
|
+
|
|
|
+**结论:可放行。** 修复提交针对的根因真实存在且修复正确:后端对已报名已返回明确业务结果,前端修复后业务错误以友好提示(modal 展示后端 message)呈现,仅真实网络失败才提示「网络异常」;首次报名成功路径未受影响;后端未改动,本仓库 `cd cfc-backend && mvn clean compile -q` 退出码 0(已实际运行);未发现支付、报名统计、报名列表的回归。
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 清单逐项核验
|
|
|
+
|
|
|
+### 1. 后端对已报名返回明确业务结果(而非抛异常 / 返回 500) —— PASS
|
|
|
+
|
|
|
+证据(代码实际):
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/service/ActivityRegistrationService.java` 第 98 行:
|
|
|
+ `if (isRegistered(activityId, memberId)) return Result.error("您已报名此活动");`
|
|
|
+- 同文件 `batchRegister()` 第 60-62 行:`if (isRegistered(activityId, memberId)) { return Result.error("已报名: " + name); }`
|
|
|
+- `cfc-backend/src/main/java/com/etotem/cfc/service/ActivityOrderService.java` 第 78-79 行(前端真实报名入口 `/api/activity/order/create` 走此路径):
|
|
|
+ `if (memberId != null && isRegistered(activityId, memberId)) { return Result.error("已报名: " + name); }`
|
|
|
+- `common/Result.java` 第 37-42 行:`Result.error(String)` 设置 `code=500`(业务错误码)+ `message`,HTTP 响应状态码仍为 200,**不是** HTTP 500。
|
|
|
+- 唯一键兜底:`schema.sql` 第 2086 行 `UNIQUE KEY uk_activity_child (activity_id, child_id)`;极端并发下的 `DuplicateKeyException`(`DataIntegrityViolationException` 子类)被 `config/GlobalExceptionHandler.java` 第 57-61 行拦截,返回 `Result.error(400, …)`,不会暴露成 HTTP 500。
|
|
|
+
|
|
|
+结论:重复插入导致的唯一键冲突不会被直接暴露成 500;正常路径已在插入前拦截并返回带 code/message 的业务结果。
|
|
|
+
|
|
|
+### 2. 前端对该结果展示友好提示,不落到网络异常 catch 分支 —— PASS
|
|
|
+
|
|
|
+证据(代码实际):
|
|
|
+- `utils/api.js` 第 300-349 行(`request()`):`res.data.code === 200` → `resolve(res.data)`;否则 → `uni.showToast(res.data.message)` + `reject(res.data)`;`fail` → `uni.showToast('网络请求失败')` + `reject(err)`(uni 原始错误对象)。**即业务错误(code≠200)走 reject(err 携带 code/message),真实网络失败也 reject(err 无 code)。**
|
|
|
+- `cfc-frontend/pages/activity/activity-detail/activity-detail.vue` 新增 `showApiError(err)`(约第 600-610 行):
|
|
|
+ `err.code !== undefined && err.code !== 200 && err.message` → `uni.showModal({ title: '提示', content: err.message, showCancel: false })` 并返回 true,否则返回 false。
|
|
|
+- `doCreateOrder()`(约第 957-965 行)与 `onCancelRegistration()`(约第 634-641 行)的 `.catch(err)`:`if (!self.showApiError(err)) { uni.showToast({ title: '网络异常', icon: 'none' }) }`。
|
|
|
+ → 重复报名时后端返回「已报名: xxx」/「您已报名此活动」,`showApiError` 弹 modal 展示后返回 true,不再显示「网络异常」;仅真实网络错误(fail 对象无 code)才显示「网络异常」。
|
|
|
+
|
|
|
+修正说明(相对核验中一度出现的误判):前序核验曾认为 `.then` 的 `else` 分支(显示 `res.message`)对业务错误可达——**这是错误的**:`request()` 对 code≠200 一律 reject,`else` 分支实际不可达。修复前代码正是在 `.catch` 中一律 toast「网络异常」,覆盖了 request 内部先弹出的业务 message,用户最终只看到「网络异常」,与用户报告的 bug 一致。`94e093949` 的 `showApiError` 正是针对真实根因的有效修复。
|
|
|
+
|
|
|
+### 3. 首次报名仍然成功,未被破坏 —— PASS
|
|
|
+
|
|
|
+证据:
|
|
|
+- `doCreateOrder()` 成功分支未改:`res.code === 200` → 免费活动提示「报名成功」并 `loadDetail` 刷新;付费活动 `requiresPayment` → 跳 `order-pay` 页。`showApiError` 只作用于 `.catch`,不干预成功路径。
|
|
|
+- `ActivityRegistrationService.register()`(第 88-118 行)/ `ActivityOrderService.create()` + `handlePaymentSuccess()` + `writeRegistrations()`(第 64-121 / 148-164 / 177-215 行):`isRegistered` 为 false(首次报名)时继续 `occupyQuota()` → `insert` → `Result.success`,逻辑完整失效无破坏。
|
|
|
+
|
|
|
+### 4. Java 代码编译通过 —— PASS(后端本次无改动,且已实际编译验证)
|
|
|
+
|
|
|
+- 本次修复提交 `94e093949` 仅改动前端 `activity-detail.vue`(diff 确认:2 个文件,均为 frontend + 报告文档),**后端无代码改动**。
|
|
|
+- 已在当前工作目录实际运行:
|
|
|
+ ```
|
|
|
+ cd /sc-data/cfc/cfc-backend && /tmp/opencode/apache-maven-3.9.9/bin/mvn clean compile -q
|
|
|
+ → 退出码 0(本机未预装 mvn,PATH 无 mvn,故使用 /tmp/opencode/apache-maven-3.9.9/bin/mvn 实际执行)
|
|
|
+ ```
|
|
|
+ 后端代码未变,该编译结果对 HEAD 仍然有效。
|
|
|
+
|
|
|
+### 5. 未引入回归(支付、报名人数统计、报名列表、语法/分支) —— PASS
|
|
|
+
|
|
|
+- 支付流程:`ActivityOrderController.create/pay/handlePaymentSuccess/status`、`ActivityOrderService` 相关方法未改动(mvn compile 通过佐证)。
|
|
|
+- 报名人数统计:`occupyQuota/releaseQuota` 仍基于 `approved_count` 原子维护,口径未动。
|
|
|
+- 报名列表:`getMyRegistrations` / `getRegistrationsByActivity` / `listRegistrationsForAdmin` 及详情页 `registrationStatus` 回显(`ActivityService.java` 第 600-630 行)未改动。
|
|
|
+- 前端语法:`cfc-frontend/scripts/verify-vue-compile.js` 对 `activity-detail.vue` 的模板段、样式段均通过(script 段因该自检以 `.mjs` 加载 ES module 的 import 而报告警,非源码语法错误;`showApiError`/`.catch(err)` 改动本身已逐行目视核,闭合正确、无未定义变量)。
|
|
|
+- 取消报名链路:catch 改造为「业务错误弹 modal / 网络错误 toast」,行为更准确,无回归。
|
|
|
+
|
|
|
+### 6. 遗留缺陷与建议改法 —— 列出(均不阻塞放行)
|
|
|
+
|
|
|
+| # | 位置 | 问题 | 建议改法 |
|
|
|
+|---|------|------|----------|
|
|
|
+| 1 | `ActivityRegistrationService.register()` L98、`batchRegister()` L60-62、`ActivityOrderService.create()` L78-79 | 前置 `isRegistered` 与 `insert` 之间存在检查-再插入的非原子竞态;极端并发下唯一键冲突走 `GlobalExceptionHandler.handleDataIntegrityViolation`,返回 400 通用文案「数据不完整或格式错误…」,而非「您已报名此活动」 | Service 层插入前加 `try-catch DataIntegrityViolationException` 转为 `Result.error("您已报名此活动")`(注意同步回滚已占用名额);与对方报告结论一致,风险较高建议单独评估 |
|
|
|
+| 2 | `utils/api.js` 非 200 分支 + `showApiError` | request 内部对业务错误先 `toast(message)`,页面 catch 再 `showModal(message)`——同一提示先 toast 一闪再弹 modal,略显冗余 | 可接受(最终用户看到 modal 友好提示);若要彻底一致,可让 request 对 code=500/400 不先 toast,交由页面统一处理;非阻塞 |
|
|
|
+| 3 | `showApiError` 判定条件 | 依赖 `err.code !== 200 && err.message`;若将来后端以 HTTP 非 200(uni fail 分支)返回,会回退为「网络异常」 | 后端契约为「统一 HTTP 200 + body code/message」,当前一致;后端接口变化时需同步 revision,非阻塞 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 证据清单(文件:行号)
|
|
|
+
|
|
|
+| 项目 | 文件 | 行 |
|
|
|
+|------|------|----|
|
|
|
+| 后端单人报名重复拦截 | `cfc-backend/src/main/java/com/etotem/cfc/service/ActivityRegistrationService.java` | 98 |
|
|
|
+| 后端批量报名重复拦截 | `cfc-backend/src/main/java/com/etotem/cfc/service/ActivityRegistrationService.java` | 60-62 |
|
|
|
+| 后端订单报名重复拦截 | `cfc-backend/src/main/java/com/etotem/cfc/service/ActivityOrderService.java` | 78-79 |
|
|
|
+| 业务错误封装(HTTP 200 + body code) | `cfc-backend/src/main/java/com/etotem/cfc/common/Result.java` | 37-42 |
|
|
|
+| 唯一键约束 | `cfc-backend/src/main/resources/schema.sql` | 2086 |
|
|
|
+| 并发唯一键兜底(非 HTTP 500) | `cfc-backend/src/main/java/com/etotem/cfc/config/GlobalExceptionHandler.java` | 57-61 |
|
|
|
+| request() 对 code≠200 的 reject 语义 | `cfc-frontend/utils/api.js` | 300-349 |
|
|
|
+| 修复后 showApiError + doCreateOrder/onCancelRegistration catch | `cfc-frontend/pages/activity/activity-detail/activity-detail.vue` | ~600-610, ~634-641, ~957-965 |
|
|
|
+| 修复提交 | git `94e093949`(仅前端 + 报告,后端无改动) | — |
|
|
|
+| 编译验证 | `mvn clean compile -q`(实际运行) | 退出码 0 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 关于工作区未提交的其他改动
|
|
|
+
|
|
|
+工作区另有一批未提交/未跟踪改动(`cfc-frontend` 三个活动页面的 `formatDateTime` 日期格式化重构、`cfc-web` 4 个 admin 页面、以及 `docs/superpowers/plans/verify/verify-datetime-format.md`),经 diff 确认属于「日期格式化」任务的独立改动,与本次「重复报名」修复无关,本核验未触碰、亦未纳入本报告结论。
|