Просмотр исходного кода

docs: 活动管理实现vs需求差异调研与整改方案

新增 docs/调研报告-活动管理实现vs需求-2026-10-22.html 与 docs/整改方案-活动管理差异修复-2026-10-22.md。覆盖主活动六环节(创建/发布/报名/取消报名/签到/活动总结)的实现与需求差异,含 4 项高危(名额失控、取消报名主路径断裂且无退款、签到审核字段失效)与一致性缺口,并给出 P0/P1/P2 代码级整改方案。
zxyj-web 3 недель назад
Родитель
Сommit
dc019ec46c

+ 184 - 0
docs/整改方案-活动管理差异修复-2026-10-22.md

@@ -0,0 +1,184 @@
+# 活动管理差异修复整改方案
+
+- **日期**:2026-10-22
+- **关联调研**:`docs/调研报告-活动管理实现vs需求-2026-10-22.html`
+- **范围**:cfc-backend 主活动(activities)全流程差异修复
+- **状态**:方案稿(待产品/架构确认后分批改)
+
+---
+
+## 一、目标与原则
+
+1. **先修阻断正确性的项(P0)**:名额失控、取消报名主路径断裂、无退款、签到审核字段失效——这四项直接影响业务正确性与用户信任,优先落地。
+2. **再修一致性与完整性(P1)**:计数口径、发布范式、评价门槛、总结能力。
+3. **低优收口(P2)**:定时化、子系统整合决策。
+4. **最小侵入**:每个修复点尽量收敛在既有 Service 方法内,避免跨模块重构;涉及支付/退款的改动必须走回归测试。
+5. **可回滚**:每个修复独立提交,便于按项回退。
+
+---
+
+## 二、整改项清单
+
+| 编号 | 级别 | 问题 | 涉及文件 | 方案要点 |
+|------|------|------|----------|----------|
+| P0-1 | 🔴 高 | 名额 maxParticipants 报名时不校验,允许超额 | `ActivityRegistrationService`、实体 `Activity` | 名额占用发生在"变为 approved"时点,用原子自增+上限约束防超卖 |
+| P0-2 | 🔴 高 | 取消报名仅支持 pending;无退款、无名额回滚 | `ActivityRegistrationService.cancel`、`ActivityOrderService`、`PaymentService` | 支持 approved 取消;付费活动联动退款;原子回退名额 |
+| P0-3 | 🔴 高 | 签到审核字段 requireCheckinReview 失效 | `ActivityService.checkin`、新增审核端点 | 落地"待审核"中间态;或移除字段(二选一) |
+| P1-1 | 🟡 中 | currentParticipants 实为签到计数,与名额口径错位 | `Activity` 实体、`ActivityService` | 拆分"报名人数"与"签到人数"两个口径 |
+| P1-2 | 🟡 中 | 两套发布范式不一致(直发不校验维度 / 审核强制权重100%) | `ActivityService.publish`、`ActivityAdminService` | 统一维度校验规则,消除状态歧义 |
+| P1-3 | 🟡 中 | 评价无门槛、无去重、status 恒 0 | `ActivityFeedbackService.submit` | 校验"已报名且已签到",按 child 去重 |
+| P1-4 | 🟡 中 | 活动总结无聚合/AI 生成;DTO summarizing 死值 | `ActivityService`、`ActivityDTO` | 新增 summary 端点聚合;或清理死代码 |
+| P2-1 | 🟢 低 | 过期自动结束仅查询触发+延迟7天 | `ActivityService.autoEndExpiredActivities` | 改为 Spring @Scheduled 定时任务 |
+| P2-2 | 🟢 低 | 三套活动子系统并存且互不相通 | 架构层 | 决策是否抽象统一 Activity 内核 |
+
+---
+
+## 三、详细方案
+
+### P0-1 名额原子校验(防超卖)
+
+**现状**:`register`(:75) / `batchRegister`(:39) 与 `checkRegistrationGate`(:318) 均不读 `maxParticipants`;名额只在 `getRegistrationRoster`(:225) 展示时计算"剩余"。
+
+**问题本质**:名额应占用在"审核通过 / 支付成功(即状态变为 approved)"的时点,而非报名(pending)时点。因此校验点应在**写入 approved** 处,而非 register 处。
+
+**方案**:
+1. 在实体 `Activity` 增加字段 `signedUpCount`(已通过报名人数,区别于签到计数),或直接复用原子更新保证上限。
+2. 在以下两处"变为 approved"的逻辑加原子上限约束(推荐使用 `UPDATE … SET current_participants = current_participants + 1 WHERE id = ? AND current_participants < max_participants`,以 affected rows 判断是否成功):
+   - `ActivityRegistrationService.approve`(:105)
+   - `ActivityOrderService.writeRegistrations`(支付成功写 approved 处)
+3. 若名额已满,approve 返回"名额已满,无法通过";支付成功写报名时若满员则触发退款并提示。
+4. `getRegistrationRoster` 的"剩余名额"改为读取同一计数,去掉 `Math.max(0,…)` 兜底的误导。
+
+**影响**:报名(pending)不受限、审核/支付通过时受限于名额——符合"满员截止"的业务语义;需处理并发(原子 SQL 已解决)。
+**测试**:并发 20 请求同时通过审核,名额=10 时仅 10 条 approved;支付回调并发同理。
+
+---
+
+### P0-2 取消报名支持 approved + 退款/名额回滚
+
+**现状**:`cancel`(:97) 用 `findRegistration(...,"pending")` 过滤,已 approved/已支付报名无法取消,且全程无退款、无名额回滚。
+
+**方案**:
+1. `cancel` 放宽到 `status IN ('pending','approved')`;`findRegistration` 去掉仅 pending 限制(或新增重载)。
+2. 判定是否付费:`hasPaidOrder(activityId, userId)`(已存在,:333)。若命中已支付订单:
+   - 调用 `PaymentService.refund(orderNo, amount)` 原路退款;
+   - 退款成功后置 `ActivityOrder.status = 'refunded'`,原报名记录置 `cancelled`。
+3. 名额回滚:在 approve 占用名额的前提下,取消 approved 时原子 `current_participants = current_participants - 1 WHERE id = ? AND current_participants > 0`。
+4. 记录 `cancelledAt` 与可选 `cancelReason`(实体需加 `cancelledAt` 字段)。
+5. 退款为异步/外部调用,需加失败重试与告警;退款失败时不置 cancelled,返回"退款处理中/失败"。
+
+**影响**:触及支付资金流,必须回归;建议先在非生产环境用小额订单验证退款链路。
+**测试**:免费活动 approved→取消成功、名额-1;付费活动 approved→取消触发退款、订单置 refunded、名额-1;pending→取消不变更资金。
+
+---
+
+### P0-3 签到审核字段落地(或移除)
+
+**现状**:实体有 `requireCheckinReview`,`checkin`(:198) 从不读取,一律直接 `Task.status="completed"`。
+
+**方案(二选一,建议 A)**:
+- **A 落地功能**:`checkin` 中若 `requireCheckinReview == 1`,生成 `Task.status = 'pending_review'`(待审核),返回 `needReview=true`;新增管理端/规划师端端点 `POST /api/admin/activity/checkin/review {taskId, action}` 审核通过后置 `completed` 并发积分/能量;不通过则删除 Task 或置 `rejected`。需同步 `applyDisplayState` 识别"已签到待审核"展示态。
+- **B 移除**:若业务确认不需要签到审核,从实体、建表、`ActivityDTO` 与后台配置表单中移除该字段,避免误导运营。
+
+**影响**:A 改动签到与 Task 状态机、新增端点;B 为清理型改动。
+**测试**:开启审核后签到→状态 pending_review→审核通过发积分/未通过不发;关闭时行为同现状。
+
+---
+
+### P1-1 currentParticipants 语义修正
+
+**现状**:`checkin`(:354) 中 `currentParticipants+1`,实为"签到人数",与 `maxParticipants`(报名名额)非同一口径,导致满员判断失真。
+
+**方案**:
+- 新增字段 `signedCount`(签到人数),`checkin` 时 `+1` 到 `signedCount`;
+- 报名名额占用(P0-1)使用独立的 `approvedCount`/`currentParticipants` 语义字段;
+- 对外展示与统计明确区分:报名人数(approved 计数)、签到人数(signedCount)。
+- 若不想新增字段,至少将 `currentParticipants` 明确命名为"签到人数",名额比较统一用 approved 实时统计,前端文案同步。
+
+**测试**:报名 10 人、签到 6 人时,报名人数=10、签到人数=6、剩余名额=max-10。
+
+---
+
+### P1-2 发布范式统一
+
+**现状**:C 端 `ActivityService.publish`(:173) 不校验维度;管理端审核流 `auditActivity` 强制五维权重合计=100%。
+
+**方案**:
+- 明确两类入口语义:**管理后台直发**(角色已受信任)与**规划师/需审核提交**(走审核流)。
+- 统一维度校验:**两路都要求 `dimensionCode` 非空**(最低一致性);审核流的"权重合计=100%"作为审核专属强校验保留,直发不强要求权重但必须有 dimensionCode。
+- 在 `publish` 增加 `if (dimensionCode == null) return error("请先设置活动维度")`。
+
+**影响**:小改动,消除"绕过审核直接 published 且维度缺失"的数据质量问题。
+**测试**:直发无 dimensionCode 被拒;审核流权重≠100% 被拒。
+
+---
+
+### P1-3 评价门槛与去重
+
+**现状**:`ActivityFeedbackService.submit`(:20) 直接 insert,无门槛、无去重,`status` 恒 0。
+
+**方案**:
+- 提交前校验:活动 `status=ended` 或 `now.after(endTime)`(已结束才可评);且 `isApproved` 且 `hasCheckedIn`(已报名且已签到),否则返回"仅参与活动的成员可评价"。
+- 去重:同一 `activityId + childId` 仅一条,重复提交时更新而非新增。
+- `status` 复用:若启用评价审核则 `pending/approved`;否则置 `approved`。
+
+**测试**:未报名/未签到用户提交被拒;同一 child 重复提交更新不新增。
+
+---
+
+### P1-4 活动总结能力(或清理死代码)
+
+**现状**:无服务端聚合/AI 总结;`ActivityDTO` 注释声明 `summarizing` 但 `applyDisplayState`(:527) 从不产出。
+
+**方案(二选一)**:
+- **A 落地总结**:新增 `POST /api/activity/summary {activityId}` 聚合 `approvedCount / pendingCount / signedCount / avgRating / feedbackList / 参与率`;可选接 LLM 生成一段自然语言总结,生成期间 DTO `displayStatus='summarizing'` 生效。
+- **B 清理**:若业务暂不需要"总结",删除 DTO 中 `summarizing` 死值声明与注释,避免前端误依赖。
+
+**影响**:A 为新增能力(需产品确认是否需要);B 为清理。
+**测试**:A 下 summary 返回正确聚合;B 下前端无"总结中"标签依赖。
+
+---
+
+### P2-1 自动结束定时化
+
+**现状**:`autoEndExpiredActivities`(:402) 在 `list/detail` 调用时触发,且 `endTime` 早于"当前-7天"才结束。
+
+**方案**:改为 Spring `@Scheduled(cron = "0 0 2 * * ?")` 每日 02:00 执行;结束阈值改为 `endTime < now()`(或保留短暂宽限如 1 天),去掉"查询才触发"与 7 天延迟。`list/detail` 中保留轻量实时判断作为兜底即可。
+
+**测试**:制造 endTime 昨天的活动,定时任务执行后状态=ended。
+
+---
+
+### P2-2 三套子系统整合决策(架构)
+
+**现状**:主活动 `activities`(报名/签到/评价)、规划师活动 `guide_activities`(无报名签到)、商品履约 `activity_provisions`(另一套签到核销)三者状态机/报名/签到互不相通。
+
+**建议**:作为**决策项**而非立即改代码。产品/架构需确认:
+- 规划师活动是否应复用主活动的报名/签到/评价能力(若需要,应合并或建立继承关系);
+- 商品履约签到是否应统一到主活动 Task 签到(避免同一活动双签到口径)。
+
+短期:在文档与代码注释中明确三者边界,避免后续误用。
+
+---
+
+## 四、实施与上线策略
+
+1. **分批提交**:P0-1/2/3 各自独立 commit;P1/P2 随后。每批可独立回退。
+2. **依赖顺序**:P0-1(名额)应在 P0-2(退款回滚)之前,因为回滚逻辑依赖名额字段口径;P1-1 与 P0-1 最好合并处理计数口径。
+3. **回归测试清单**:
+   - 报名满员拦截(并发)
+   - 免费/付费取消报名(含退款)
+   - 签到审核开关两种行为
+   - 评价门槛/去重
+   - 过期活动自动结束定时任务
+4. **上线策略**:P0 三项先在预发环境灰度(尤其退款链路用小额订单);确认无误再上生产。
+5. **回滚**:任一修复独立 commit,出问题 `git revert` 单笔即可。
+
+---
+
+## 五、待确认决策点(请产品/架构拍板)
+
+1. P0-3:签到审核是**落地功能(A)**还是**移除字段(B)**?
+2. P1-4:是否需要"活动总结"自动聚合/AI 生成(A),还是**清理死代码(B)**?
+3. P2-2:规划师活动、商品履约活动是否纳入统一活动内核?(影响中长期架构)
+4. 名额占用时点采用"审核/支付通过即占名额"是否达成一致(影响候补/排队逻辑设计)?

+ 317 - 0
docs/调研报告-活动管理实现vs需求-2026-10-22.html

@@ -0,0 +1,317 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>活动管理实现 vs 需求差异调研报告</title>
+<style>
+  :root {
+    --bg: #ffffff; --fg: #1f2933; --muted: #647082; --line: #e3e8ef;
+    --brand: #1565c0; --brand-bg: #e8f1fb;
+    --red: #c62828; --red-bg: #fdecea; --red-line:#f5c6c0;
+    --amber: #b26a00; --amber-bg: #fff4e0; --amber-line:#f3d79a;
+    --green: #2e7d32; --green-bg: #e9f6ea; --green-line:#bfe3c2;
+  }
+  * { box-sizing: border-box; }
+  body { font-family: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;
+         color: var(--fg); background: #f6f8fb; margin: 0; line-height: 1.65; }
+  .wrap { max-width: 1080px; margin: 0 auto; padding: 32px 28px 64px; }
+  header { border-bottom: 3px solid var(--brand); padding-bottom: 16px; margin-bottom: 8px; }
+  h1 { font-size: 26px; margin: 0 0 6px; color: var(--brand); }
+  .sub { color: var(--muted); font-size: 14px; }
+  h2 { font-size: 19px; margin: 36px 0 12px; padding-left: 10px; border-left: 4px solid var(--brand); }
+  h3 { font-size: 16px; margin: 22px 0 8px; color: var(--fg); }
+  p { margin: 8px 0; }
+  table { width: 100%; border-collapse: collapse; margin: 12px 0 18px; background: var(--bg);
+          font-size: 14px; box-shadow: 0 1px 3px rgba(0,0,0,.05); }
+  th, td { border: 1px solid var(--line); padding: 9px 11px; text-align: left; vertical-align: top; }
+  th { background: var(--brand-bg); color: var(--brand); font-weight: 600; }
+  tr:nth-child(even) td { background: #fbfcfe; }
+  code { background: #eef1f6; padding: 1px 5px; border-radius: 4px; font-size: 12.5px; color:#283593; }
+  .sev { display: inline-block; font-weight: 700; padding: 2px 9px; border-radius: 999px; font-size: 12px; }
+  .s-high { color: var(--red); background: var(--red-bg); border:1px solid var(--red-line); }
+  .s-mid  { color: var(--amber); background: var(--amber-bg); border:1px solid var(--amber-line); }
+  .s-low  { color: var(--green); background: var(--green-bg); border:1px solid var(--green-line); }
+  .card { border: 1px solid var(--line); border-radius: 10px; padding: 16px 18px; background: var(--bg); margin: 14px 0; }
+  .gap { border-left: 4px solid var(--red); }
+  .note { border-left: 4px solid var(--amber); }
+  .ok { border-left: 4px solid var(--green); }
+  ul { margin: 8px 0 8px 4px; padding-left: 20px; }
+  li { margin: 5px 0; }
+  .tag { font-size: 12px; color: var(--muted); background:#eef1f6; padding:1px 7px; border-radius:5px; margin-right:4px;}
+  .verdict { background: var(--brand-bg); border:1px solid #b9d4f3; border-radius:10px; padding:16px 20px; margin:18px 0; font-size:15px;}
+  .verdict b { color: var(--brand); }
+  footer { margin-top: 40px; color: var(--muted); font-size: 12.5px; border-top:1px solid var(--line); padding-top:12px;}
+  .matrix td:first-child { font-weight: 600; white-space: nowrap; }
+</style>
+</head>
+<body>
+<div class="wrap">
+<header>
+  <h1>活动管理「实现 vs 需求」差异调研报告</h1>
+  <div class="sub">调研日期:2026-10-22 | 项目:浠艾福 CFC(cfc-backend)| 主线:主活动(activities)全流程</div>
+</header>
+
+<div class="verdict">
+  <b>总体结论:</b>活动管理的<b>主链路(创建→发布→报名→签到→评价)已基本打通</b>,付费报名+微信支付、报名审核、积分/能量/CF 发放、评价与资料上传均已落地。
+  但与流程需求之间存在 <b>4 项高危差异</b>(名额失控、取消报名主路径断裂且无退款、签到审核字段失效)与 <b>多项一致性/完整性缺口</b>;
+  并且系统里<b>并存三套互不相通的活动子系统</b>,是后续维护与需求对齐的最大隐患。建议优先修复 🔴 高危项,再处理 🟡 一致性问题。
+</div>
+
+<h2>一、调研依据与范围</h2>
+<div class="card">
+  <p><b>需求侧(事实源):</b></p>
+  <ul>
+    <li><code>docs/flows/activity-flow.md</code> —— 用户使用流程图,定义了创建、发布(含审核)、报名、取消报名、报名审批、签到、评价的端到端流程与 24 个端点。</li>
+    <li><code>docs/superpowers/specs/2026-06-09-article-activity-publishing-design.md</code>、<code>2026-08-01-activity-registration-upgrade-design.md</code>、<code>2026-07-31-activity-registration-form-design.md</code>、<code>2026-10-07-activity-share-referral-design.md</code>、<code>2026-10-17-activity-poster-template-design.md</code>、<code>2026-10-22-activity-registration-roster.md</code> —— 围绕发布与报名的增强规格。</li>
+  </ul>
+  <p><b>实现侧:</b><code>cfc-backend</code> 的 <code>controller/ActivityController</code>、<code>controller/admin/AdminActivity*</code>、<code>controller/ActivityOrderController</code>、<code>service/ActivityService</code>、<code>ActivityRegistrationService</code>、<code>ActivityOrderService</code>、<code>ActivityFeedbackService</code>、<code>ActivityMaterialService</code> 等及其实体/Mapper。</p>
+  <p class="note" style="margin-top:10px;"><b>范围说明:</b>本报告"活动管理"指流程图中的<b>主活动(activities)</b>。系统另含「规划师活动(guide_activities)」与「商品履约活动(activity_provisions)」两套独立子系统,见第五节。</p>
+</div>
+
+<h2>二、六环节逐一对比</h2>
+
+<h3>① 创建</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>创建入口</td>
+    <td>管理后台/规划师端可创建,含标题、描述、封面、分类、地点、日期、最大人数、价格等字段</td>
+    <td><code>ActivityService.create</code>(:153) 强制 <code>draft</code>、<code>currentParticipants=0</code>;规划师端 <code>GuideActivityService.createActivity</code></td>
+    <td>主活动<b>无管理后台"新建"端点</b>(AdminActivityController 只有审核/列表),只能 C 端带 activity_admin 角色创建,职责割裂</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>字段校验</td>
+    <td>表单级字段约束(价格、时间、名额等)</td>
+    <td><code>create</code>/<code>update</code>(:163) 仅校验存在性,业务字段几乎零校验(价格/时间/名额/维度均可空)</td>
+    <td>合法性完全依赖前端,后端不兜底</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>海报模板</td>
+    <td>—</td>
+    <td><code>ActivityPosterTemplateService</code> 含启用/停用、配色、JSON 校验</td>
+    <td>已实现,且超出基础需求</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+</table>
+
+<h3>② 发布</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>发布范式</td>
+    <td>流程图中存在「C端直发 publish」与「管理端审核流」两条路径</td>
+    <td><code>ActivityService.publish</code>(:173) 仅 <code>draft→published</code>,<b>不校验维度</b>;<code>ActivityAdminService.auditActivity</code> 审核流强制<b>五维权重合计=100%</b></td>
+    <td><b>两套发布范式不一致</b>:同一条数据可绕过审核直接 published,也可走审核,状态机存在歧义</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+  <tr>
+    <td>审核/撤回</td>
+    <td>提交审核 → 通过/驳回 → 撤回/转草稿</td>
+    <td><code>submitForReview</code> / <code>auditActivity</code> / <code>withdraw</code> / <code>reDraft</code> 均已实现,含五维权重落库</td>
+    <td>功能完整</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>自动结束</td>
+    <td>活动到期自动结束</td>
+    <td><code>autoEndExpiredActivities</code>(:402) 仅当 <code>list/detail</code> 被调用时触发,且 <code>endTime</code> 早于"当前-7天"才转 ended</td>
+    <td><b>弱实现</b>:非定时任务、延迟 7 天才结束;活动过期后若无人查询仍显示进行中</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+  <tr>
+    <td>标签/推荐</td>
+    <td>—</td>
+    <td>审核时 <code>ActivityTagService</code> AI 自动打标(失败静默降级),<code>getRelatedActivitiesByTags</code> 相关推荐</td>
+    <td>已实现,超出基础需求</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+</table>
+
+<h3>③ 报名</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>名额控制</td>
+    <td>"最大人数 / 名额",隐含报名应受名额限制、满员截止</td>
+    <td><code>register</code>(:75)、<code>batchRegister</code>(:39)、<code>checkRegistrationGate</code>(:318) 仅校验 published / 是否需要报名 / 截止时间 / 付费闸门,<b>从不读 maxParticipants</b></td>
+    <td><b>🔴 名额失控</b>:允许超额报名,无任何阻塞逻辑;<code>maxParticipants</code> 仅用于"剩余名额"展示(<code>getRegistrationRoster</code> :225 用 <code>Math.max(0,…)</code> 兜底)</td>
+    <td><span class="sev s-high">高</span></td>
+  </tr>
+  <tr>
+    <td>免费报名</td>
+    <td>提交报名 → 待审核(pending) → 管理员通过</td>
+    <td><code>register</code> 写 <code>pending</code>;管理员 <code>approve</code>(:105)/<code>reject</code>(:121) 仅 pending 可审</td>
+    <td>流程完整;但免费活动报名后<b>必须管理员审核</b>才能签到,付费活动支付即 auto-approved,体验不一致</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>付费报名</td>
+    <td>—(需求未细化收费)</td>
+    <td><code>ActivityOrderService</code>:订单→微信预支付→回调→写 approved 全链路,金额=0 时免支付直接通过</td>
+    <td>已实现且完整(含回调解密)</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>计数口径</td>
+    <td>当前参与人数</td>
+    <td><code>currentParticipants</code> 只在<b>签到时 +1</b>(见⑤),并非报名人数</td>
+    <td><b>语义错位</b>:与 <code>maxParticipants</code> 不是同一口径,满员判断失真</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+</table>
+
+<h3>④ 取消报名</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>可取消状态</td>
+    <td>用户应能取消自己的报名</td>
+    <td><code>cancel</code>(:97) 用 <code>findRegistration(...,"pending")</code> 过滤</td>
+    <td><b>🔴 主路径断裂</b>:已审核通过(approved)/已支付报名无法取消,调用即返回"报名记录不存在"</td>
+    <td><span class="sev s-high">高</span></td>
+  </tr>
+  <tr>
+    <td>退款/回滚</td>
+    <td>取消应释放名额、付费应退款</td>
+    <td>取消仅改状态;主活动无支付绑定、无退款逻辑;名额/计数均无回滚</td>
+    <td><b>🔴 无退款、无名额回滚</b>(付费活动取消无退款;退款逻辑仅存在于商品履约支线 ActivityProvisionService.cancel)</td>
+    <td><span class="sev s-high">高</span></td>
+  </tr>
+  <tr>
+    <td>取消记录</td>
+    <td>—</td>
+    <td>无"取消原因/取消时间"字段</td>
+    <td>可观测性弱</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+</table>
+
+<h3>⑤ 签到</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>签到主流程</td>
+    <td>扫码/按钮签到,发放积分、扣能量(未报名)、更新参与人数、发放五维能量</td>
+    <td><code>ActivityService.checkin</code>(:198) 完整:时间窗校验、本人/同家庭代签、未报名能量惩罚、去重、建 Task(completed)、加积分、写 PointsLog、发能量、发 CF 值</td>
+    <td>功能完整,超出基础需求</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>审核签到</td>
+    <td>实体含 <code>requireCheckinReview</code>(签到需审核)</td>
+    <td><code>checkin</code> 逻辑<b>从不读取</b>该字段,一律直接 <code>Task.status="completed"</code></td>
+    <td><b>🔴 虚假字段</b>:配置项形同虚设,无任何"待审核"中间态</td>
+    <td><span class="sev s-high">高</span></td>
+  </tr>
+  <tr>
+    <td>强制报名</td>
+    <td>先报名后签到</td>
+    <td>除 <code>requireRegistration=1</code> 且未通过审核时拒绝外,并未阻止未报名者签到(无手机号孩子可代签)</td>
+    <td>与"先报名后签到"假设不符,但属业务取舍</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+  <tr>
+    <td>平行签到机制</td>
+    <td>—</td>
+    <td>商品履约 <code>activity_provisions</code> 另有一套签到/核销(provision→signIn→complete),与主活动 Task 签到互不关联</td>
+    <td>同一活动可能两种签到并存,统计口径混乱(见第五节)</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+</table>
+
+<h3>⑥ 活动总结</h3>
+<table>
+  <tr><th style="width:110px">维度</th><th>需求要点</th><th>实现情况</th><th>差异 / 缺口</th><th>严重度</th></tr>
+  <tr>
+    <td>评价</td>
+    <td>流程阶段七"活动评价":提交评分/评论、查看评价列表、我的评价</td>
+    <td><code>ActivityFeedbackService.submit</code>(:20) 直接 insert,无去重、无"是否已结束/已报名/已签到"门槛;任何人可评;<code>status</code> 恒 0 无意义</td>
+    <td>基础可用但<b>无门槛、无去重、状态字段无效</b></td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+  <tr>
+    <td>总结资料</td>
+    <td>—(需求未定义"活动总结"独立阶段)</td>
+    <td><code>ActivityMaterialService.upload</code>(:26) 限定 <code>status=published</code> 且 <code>now.after(endTime)</code>,即结束后才能上传资料</td>
+    <td>已实现"结束后的资料上传"</td>
+    <td><span class="sev s-low">低</span></td>
+  </tr>
+  <tr>
+    <td>聚合/AI 总结</td>
+    <td>用户表述中的"活动总结"隐含<b>参与数据汇总 / 复盘报告</b></td>
+    <td><b>无任何服务端聚合、AI 总结文本生成或参与数据汇总报告</b>。<code>ActivityDTO</code> 注释声明了 <code>summarizing</code>/总结中展示态,但 <code>applyDisplayState</code>(:527) <b>从不产出该值</b>(死代码)</td>
+    <td><b>🟡 需求未定义 + 实现未落地</b>:若业务需要"自动总结",目前完全缺失;DTO 里的"总结中"标签前端若依赖将无法命中</td>
+    <td><span class="sev s-mid">中</span></td>
+  </tr>
+</table>
+
+<h2>三、差异汇总(按严重度)</h2>
+<table class="matrix">
+  <tr><th>级别</th><th>差异项</th><th>影响</th></tr>
+  <tr><td><span class="sev s-high">高</span></td><td>名额(quota)报名时不校验,允许超额报名</td><td>满员控制失效,可能超售;业务正确性风险</td></tr>
+  <tr><td><span class="sev s-high">高</span></td><td>取消报名仅支持 pending,已通过/已支付报名无法取消</td><td>用户正常"报名后取消"主路径断裂</td></tr>
+  <tr><td><span class="sev s-high">高</span></td><td>取消报名无退款、无名额/计数回滚</td><td>付费活动取消无退款,财务与名额失准</td></tr>
+  <tr><td><span class="sev s-high">高</span></td><td>签到审核字段 requireCheckinReview 形同虚设</td><td>配置项不产生任何效果,误导运营</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>两套发布范式不一致(直发不校验维度 vs 审核强制权重100%)</td><td>状态机歧义、数据质量参差</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>currentParticipants 语义错位(实为签到计数)</td><td>满员/参与统计失真</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>评价无门槛、无去重、status 恒 0</td><td>刷评/误评无防护</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>活动总结无聚合/AI 生成;DTO summarizing 死值</td><td>"总结"能力缺失,前端标签失效</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>过期自动结束弱实现(查询触发+延迟7天)</td><td>过期活动未及时结束</td></tr>
+  <tr><td><span class="sev s-mid">中</span></td><td>未报名者仍可签到(除需审核场景)</td><td>与"先报名后签到"假设不符</td></tr>
+  <tr><td><span class="sev s-low">低</span></td><td>三套活动体系并存且互不相通(见第五节)</td><td>架构/维护风险</td></tr>
+  <tr><td><span class="sev s-low">低</span></td><td>主活动无管理后台"新建"入口</td><td>职责割裂</td></tr>
+  <tr><td><span class="sev s-low">低</span></td><td>创建接口零字段校验</td><td>依赖前端兜底</td></tr>
+</table>
+
+<h2>四、关键缺口详解</h2>
+
+<div class="card gap">
+  <h3 style="margin-top:4px">🔴 名额失控(最严重)</h3>
+  <p>报名入口 <code>register</code> / <code>batchRegister</code> 与闸门 <code>checkRegistrationGate</code> 均未对 <code>maxParticipants</code> 做判断。系统只在"报名情况"接口用 <code>max - approvedCount</code> 计算剩余名额并 <code>Math.max(0, …)</code> 兜底——意味着即使报名人数远超上限,剩余名额只显示 0,但<b>报名依旧成功</b>。对线下有场地/物料限制的活动,这是直接业务风险。</p>
+</div>
+
+<div class="card gap">
+  <h3 style="margin-top:4px">🔴 取消报名主路径断裂 + 无退款</h3>
+  <p><code>cancel</code> 用 <code>findRegistration(activityId, memberId, userId, "pending")</code> 限定只查 pending。而免费活动需管理员 approve、付费活动支付成功即 auto-approved——两者到达"可签到"状态后都不再是 pending,因此用户在前端点"取消报名"会收到"报名记录不存在"。此外,主活动报名与支付(ActivityOrder)之间缺乏取消→退款的回路,付费用户取消后无退款。</p>
+</div>
+
+<div class="card gap">
+  <h3 style="margin-top:4px">🔴 签到审核字段失效</h3>
+  <p>实体 <code>Activity</code> 设计了 <code>requireCheckinReview</code>(是否需要审核签到),但 <code>checkin</code> 全程未读取该字段,签到一律直接生成 <code>Task.status="completed"</code>。运营在后台开启"签到需审核"后不会得到任何预期行为。</p>
+</div>
+
+<h2>五、架构风险提示:三套互不相通的活动子系统</h2>
+<div class="card note">
+  <p>当前后端并存三条"活动"数据/流程线,做需求对照时切勿混为一谈:</p>
+  <ul>
+    <li><b>主活动 activities</b>:C 端活动中心,承载本报告六环节(报名/签到/评价)。状态机 <code>draft/pending/rejected/published/withdrawn/ended</code>。</li>
+    <li><b>规划师活动 guide_activities</b>:规划师端,仅 create/update/publish/cancel/delete,<b>不参与报名/签到/评价</b>,状态机 <code>draft/published/cancelled/completed</code>,与主活动字段、状态完全独立。</li>
+    <li><b>商品履约活动 activity_provisions</b>:挂在 product_orders 上,"购买活动商品→核销码→签到→完成→退款",状态机 <code>unprovisioned→provisioned→signed_in→completed</code>,与主活动报名/签到<b>平行且互不相通</b>。</li>
+  </ul>
+  <p>其中"规划师活动"与"主活动"的报名/签到/评价能力无法复用;"商品履约签到"与"主活动 Task 签到"是两套独立的签到实现。这意味着同一业务概念(活动、报名、签到)在代码里有多套定义,需求对齐与后续迭代成本显著。</p>
+</div>
+
+<h2>六、修复优先级建议</h2>
+<table>
+  <tr><th style="width:60px">优先级</th><th>事项</th><th>建议动作</th></tr>
+  <tr><td><span class="sev s-high">P0</span></td><td>名额校验</td><td>在 <code>register/batchRegister</code> 中于写入前校验 <code>approvedCount &lt; maxParticipants</code>(含并发悲观锁/唯一约束),满员返回"名额已满"</td></tr>
+  <tr><td><span class="sev s-high">P0</span></td><td>取消报名链路</td><td><code>cancel</code> 支持 approved 状态;对付费活动联动 <code>ActivityOrderService</code> 走退款;回滚名额计数</td></tr>
+  <tr><td><span class="sev s-high">P0</span></td><td>签到审核字段</td><td>若保留该能力则在 <code>checkin</code> 读取 <code>requireCheckinReview</code> 生成待审核 Task;否则从实体/后台配置中移除,避免误导</td></tr>
+  <tr><td><span class="sev s-mid">P1</span></td><td>currentParticipants 口径</td><td>拆分为"报名人数"与"签到人数"两个字段,或统一以 approved 计数,消除语义错位</td></tr>
+  <tr><td><span class="sev s-mid">P1</span></td><td>发布范式统一</td><td>明确"直发"与"审核流"的适用场景,统一维度/权重校验规则,避免状态歧义</td></tr>
+  <tr><td><span class="sev s-mid">P1</span></td><td>活动总结能力</td><td>若业务需要"总结",补充服务端聚合(参与率/签到率/评价汇总)或 AI 总结,并接线 DTO 的 summarizing 态;否则清理死代码</td></tr>
+  <tr><td><span class="sev s-mid">P1</span></td><td>评价门槛</td><td>评价前校验"已报名且已签到",并去重(每 child 一条)</td></tr>
+  <tr><td><span class="sev s-low">P2</span></td><td>自动结束</td><td>改为定时任务(如每日 cron),消除"查询才触发"与 7 天延迟</td></tr>
+  <tr><td><span class="sev s-low">P2</span></td><td>子系统整合决策</td><td>明确主活动 / 规划师活动 / 商品履约三者边界,决定是否需要统一活动内核,避免长期分裂</td></tr>
+</table>
+
+<footer>
+  本报告基于代码静态核查(已逐行核验 register / cancel / checkin / applyDisplayState / autoEndExpiredActivities 等关键方法)与 <code>docs/flows/activity-flow.md</code> 流程需求对照生成。
+  差异判定以"流程需求隐含约束"与"代码实际行为"为准;标注 🔴 的项建议在上线前优先修复。
+</footer>
+</div>
+</body>
+</html>