Sfoglia il codice sorgente

docs(verify): 活动重复报名修复独立核验报告(6 项清单 PASS,可放行)

iwt 2 settimane fa
parent
commit
bccb42d473

+ 3 - 2
docs/superpowers/PROJECT-OVERVIEW.md

@@ -1,7 +1,7 @@
 # 浠艾福 项目全景 — 阶段性需求与设计汇总
 
-**文档版本:** v2.21
-**日期:** 2026-10-21
+**文档版本:** v2.22
+**日期:** 2026-10-24
 **状态:** 已确认(v2.1 Phase 2-4 全栈完成)+ 虚拟支付改造(Tasks 1-12 已完成,退款闭环实施中)+ 新用户注册引导(12 Tasks 全栈完成)+ 家庭成员关系条增强(✅ 已实施)+ TabBar 重构(✅ 4Tab + 中间⭐扇形菜单,设计已对齐实现)+ **LIFETIME 终身会员(✅ 11 Tasks 全栈完成)** + **健康数据中心(✅ 全栈完成)** + **SKU 价格单位统一分 + 规格选择响应式修复(✅ 7 Tasks 全栈完成)** + **AI健康教练人格分化与管家自助选择(✅ 11 Tasks 全栈完成)** + **优惠券全链路改为家庭维度(待实施)** + **关系三图重构(关键人拓展章鱼图/珍珠图/能力图,设计规格已完成)** + **注册零填写+信息按需补充(✅ 全栈完成:登录直接进首页 + 家庭邀请独立页真实姓名/家庭身份必填 + 功能→信息字段映射表 + 添加成员手机号冲突邀请加入)** + **健康报告归属重分配(✅ 后端接口 + 管理端页面 + 小程序入口已实现,见 `specs/2026-10-05-health-report-reassign-design.md`)**
 **指标变化曲线·指标速查·指标关联(✅ 全栈完成:后端11任务+小程序M1-M8+管理端W1-W6;HBuilderX人工验收待用户执行,见 `specs/2026-10-09-indicator-change-curve-design.md`)**
 **饮食/睡眠/运动打卡接入任务完成链路(✅ 全栈完成:实体taskId字段 + 三个Controller isDailyProgress分流回写 + RepeatTaskGenerator每日生成打卡任务 + tasks.vue跳转 + 三打卡页taskId透传 + API文档;见 `specs/2026-10-13-health-checkin-task-linkage-design.md` 与 `plans/2026-10-13-health-checkin-task-linkage.md`)**
@@ -9,6 +9,7 @@
 **菌群知识并入 dan_knowledge_base(🔄 实施中:809 篇文章以 source_type='article' 并入统一知识表 + 后端 sourceType/excludeSourceType 过滤 + langgraph 三源重建 + 管理端统一展示,见 `specs/2026-10-20-knowledge-unify-design.md` 与 `plans/2026-10-20-knowledge-unify.md`)**
 **限时特价(扫码闪购)(✅ 全栈完成:迁移389/390 → SpecialPriceService 7 方法 + 双控制器(公开 info 白名单 + admin 6 接口)→ 特价下单 createSpecialDealOrder + cancel/approveRefund/OrderTimeoutCancelTask 三处 releaseQuota 回滚 + 供应商改价拒绝 + 小程序商品页 sp 解析/倒计时/特价区块/checkout 透传 + cfc-web SpecialPriceManage.vue;5 个测试类 31 case 全过;见 `specs/2026-10-20-limited-time-special-price-design.md` 与 `plans/2026-10-20-limited-time-special-price.md`)**
 **后台多渠道发货(🟡 设计稿待审查:管理端发货对齐微信小程序后台四种方式——快递 logistics_type=1 / 同城配送 2(deliveryMethod 新增编码 6)/ 用户自提 4(不再跳过微信同步)/ 虚拟商品 3(支付成功自动发货 autoShipVirtualOrder,幂等 fail-open);另修正 upload_shipping_info URL 缺 /wxa 前缀;cfc-web 发货对话框按类型切换;见 `specs/2026-10-22-fulfillment-multi-mode-design.md`)**
+**活动重复报名提示修复(✅ 已完成并经独立核验:问题为用户对同一活动已报名后再次报名时前端误提示"网络异常";根因是 utils/api.js 的 request() 对 body code!=200 一律 reject,而 activity-detail.vue 的 .catch 一律显示"网络异常",覆盖了业务提示。修复:新增 showApiError() 区分后端业务错误(modal 展示后端 message,如"已报名: xxx")与真实网络错误(仍显示"网络异常");后端三条报名入口的重复拦截(Result.error)本就存在无需改动;见 `plans/verify/fix-duplicate-signup.md` 与独立核验 `plans/verify/verify-duplicate-signup.md`)**
 **维护:** 所有需求变更需更新本文档
 
 ---

+ 92 - 0
docs/superpowers/plans/verify/verify-duplicate-signup.md

@@ -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 确认属于「日期格式化」任务的独立改动,与本次「重复报名」修复无关,本核验未触碰、亦未纳入本报告结论。