Explorar el Código

docs: 活动创建活动页体验修正 + 人群/费用重构方案(A1/A2/A3/G1/G2)

- 新增方案文档 docs/superpowers/plans/2026-10-26-activity-create-page-ux-and-audience-design.md,覆盖:
  · A1 时间选择器改为原生 multiSelector 双列滚动,选择时即 5 分钟粒度,删除三处提示文案
  · A2 audienceScope/audienceType 两列 + 结算按参与者身份分类 + 仅会员硬门禁
  · A3 活动费用展示层统一(详情页/列表/海报/报名弹窗/后台)
  · G1 L1 文案层家长/孩子中性化
  · G2 L2/L3 契约与 DB 列名中性化(依赖 T25 收口后执行)
- 同步更新 tools/xaf_agent_hub/tasks_backlog.json:
  · updated 切至 2026-10-26
  · discipline 写入单一事实源(370 字符)
  · 新增 A1/A2/A3/G1/G2 五条任务(pending)
  · 保留 T15/T16 等已完成 session_id/done_at 元信息

cfclub 基线 164c9fbc5 → 新增提交(2 文件 +500 / -18)
zxyj-web hace 2 semanas
padre
commit
3582e35801

+ 306 - 0
docs/superpowers/plans/2026-10-26-activity-create-page-ux-and-audience-design.md

@@ -0,0 +1,306 @@
+# 小程序「创建活动」页体验修正 + 活动人群/费用口径重构(设计方案)
+
+- 日期:2026-10-26
+- 状态:设计完成,待派发
+- 来源:用户 2026-10-25 对小程序创建活动页的 4 条反馈
+- 任务编号:A1 / A2 / A3 / G1 / G2(见 §四)
+
+---
+
+## §零 事实基线(代码证据,均以 cfclub 当前 tip `164c9fbc5` 为准)
+
+### 0.1 时间选择器现状(对应反馈 1、2)
+
+| 事实 | 位置 |
+|---|---|
+| 3 处时间字段全部使用微信原生 `<picker mode="time">`,原生时间选择器**不支持步长**,只能按 60 分钟逐项滚动 | `cfc-frontend/pages/activity/create.vue:24,36,48` |
+| 选中后由 `onStartTimeChange` / `onEndTimeChange` / `onRegTimeChange` 调 `normalizeTimeTo5Min()` 把分钟**向下取整**到 5 的倍数 | `create.vue:340-362` |
+| `normalizeTimeTo5Min`:`Math.floor(mi / 5) * 5`,14:07 → 14:05 | `create.vue:363-373` |
+| 历史数据回填 `fmtTime()` 也做同样归一化 | `create.vue:593-599` |
+| 提示文案「以 5 分钟为一个单元(如 14:00、14:05)」**重复 3 处** | `create.vue:27, 39, 51` |
+| 同名提示还留了一条 CSS 注释 | `create.vue:677` |
+| 列表页用 4 个 child/parent 费用的最小值覆盖 `act.price` | `cfc-frontend/pages/activity/index.vue:852-857` |
+| 上一轮修复(commit `707ea09ad` + Round 2 复核)**已把「用户可短暂看到 14:07 再收敛」记为已知遗留项**,未解决 | `docs/superpowers/plans/verify/verify-activity-time-5min-round2.md` ⑤ 遗留项 3 |
+
+**结论**:用户反馈 2 指出的「选择时仍按每分钟显示、选完才被改写」是上一轮**明确知情且接受的取舍**,本次要求从源头消除。
+
+### 0.2 费用模型现状(对应反馈 3、4)
+
+| 事实 | 位置 |
+|---|---|
+| 小程序创建页**恒显 4 个费用输入**:孩子费用、孩子会员费、家长费用、家长会员费 | `create.vue:67-85` |
+| 小程序创建页**没有** `price` / `memberPrice` 输入项 | `create.vue`(全文无 `priceYuan`/`memberPrice`) |
+| 后台管理端恰恰是**「价格(元)」+「会员价(元)」= `price` / `memberPrice`**,**没有** child/parent 费用字段 | `cfc-web/src/views/admin/ActivityEdit.vue:140-153` |
+| 后端实体 `price` / `memberPrice` 存在,`create.vue` 从不填它们 | `cfc-backend/.../entity/Activity.java:67,70` |
+| `childFee` / `childMemberFee` / `parentFee` / `parentMemberFee` 在实体 + DTO 中存在 | `Activity.java:76-85`、`dto/ActivityDTO.java:50-59` |
+| **结算按「有无 memberId」判 child/parent**:`boolean isChild = memberId != null` → 有 memberId 收 childFee、无 memberId 收 parentFee;快照 `type` 写死 `"child"`/`"parent"` | `ActivityOrderService.java:81-84,91` |
+| 报名表 `ActivityRegistration.childId` 实际装的是 `memberId`(统一成员选择器语义) | `entity/ActivityRegistration.java:16`、`service/ActivityRegistrationService.java:57-71` |
+| 全系统**没有**活动级「仅会员参加」概念;`memberOnly` 只存在于任务域 `Task.java:119` | 全库 grep |
+| 实体已有 `visibility` / `visibleScope(family/role/user)` / `visibleTo` / `familyScope`,但语义是**可见范围**,不等于「谁能参加」,**不可复用** | `Activity.java:91,112-118` |
+| 后台同样**没有适用人群**字段 | `cfc-web/src/views/admin/ActivityEdit.vue`(全文无) |
+| 数据模型已是中性口径:`FamilyMember` 有 `generation` / `isSpouse` / `age`,**无需新增角色字段** | `entity/FamilyMember.java:54,56,67` |
+| 任务域已存在可复用的会员硬门禁范式:`task.memberOnly==1 且非会员 → 拒绝` | `TaskController.java:48` |
+
+**两个必须点明的结论**:
+
+1. **两端口径分裂**:后台设置的「价格 / 会员价」在小程序创建页填不了;小程序填的「孩子/家长费用」后台改不了。`price`/`memberPrice` 列与 `childFee`/`parentFee` 列是**两套并行且互不互通**的费用体系。
+2. **`isChild = memberId != null` 是语义错误**:在统一成员选择器(T25)之后,`memberId` 可以是儿童、长者、配偶等任意家庭成员,不能一律当「孩子」收费。这是隐藏的费用算错风险,且当前**没有结算用例**覆盖它(`childFee`/`parentFee` 在后端仅出现于实体、DTO、`ActivityOrderService` 三处)。
+
+---
+
+## §一 A1:时间选择器收敛为「选择时即 5 分钟粒度」
+
+### 1.1 方案:原生 `<picker mode="multiSelector">` 双列滚动
+
+把 3 处 `mode="time"` 换成 `mode="multiSelector"`,两列分别为:
+
+- 列 1 时:`['00','01',…,'23']`(24 项)
+- 列 2 分:`['00','05','10','15','20','25','30','35','40','45','50','55']`(**12 项**)
+
+效果:
+
+- 用户在选择过程中**只能看到 0/5/10…55**,从源头消除「选了 07 分却变 05 分」;
+- 因为界面本身就是 5 分钟粒度,**无需任何文字解释**,3 处 `picker-hint` 可整行删除;
+- 微信原生能力,**不引入新组件、不依赖第三方**;
+- `normalizeTimeTo5Min` **保留作为兜底**(历史脏数据、后台或接口写入非 5 倍数时仍能收敛),但正常路径不再触发取整。
+
+### 1.2 实施清单(仅前端,单文件)
+
+1. `data` 新增:`hourOptions`、`minuteOptions`、`hourIdx`、`minuteIdx`、`endHourIdx`、`endMinuteIdx`、`regHourIdx`、`regMinuteIdx`(3 组时间各一套索引)。
+2. 模板 3 处 `<picker mode="time">` → `<picker mode="multiSelector" :range="[hourOptions, minuteOptions]" :value="[hourIdx, minuteIdx]" @change="onStartTimeColChange">`。
+3. 新增 `onStartTimeColChange(e)`:取 `e.detail.value`(列索引数组)→ 拼 `HH:MM` → 写入 `timePart` → 调既有 `syncStartTime()`;日期列逻辑(`onStartDateChange` → `syncStartTime`)**完全不变**。
+4. `delete` 3 处 `<text class="picker-hint">…</text>`(行 27/39/51)与对应 CSS 注释(行 677)。
+5. **索引与字符串双向同步**:`timePart` 变化时(含回填)必须能反算 `hourIdx/minuteIdx`,否则 picker 会显示默认值。建议写一个 `applyTimeParts(timePart, hourIdxRef, minuteIdxRef)` 供 `data` 初始化与回填共用。
+6. `fmtTime()` 回填路径:`normalizeTimeTo5Min` 后 `mi / 5` 反算分钟索引。
+
+### 1.3 边界与回归要点
+
+- 结束时间 ≥ 开始时间、报名截止时间 ≤ 开始时间:**现状没有校验**,本次不做(避免范围膨胀),但 A1 完成后 picker 的 `value` 变成索引数组,**不要顺手引入校验**,以免与既有 `syncXxx` 拼装逻辑冲突。
+- 提交串格式必须仍为 `YYYY-MM-DDTHH:mm:00`,秒恒 `00`(与既有契约一致,`docs/.../verify-activity-time-5min-round2.md` ③ 已锁定)。
+- 回填 1440 分钟全量扫描测试沿用上一轮方法(`normalizeTimeTo5Min` / `fmtTime` 提取为 `.mjs` 跑 `node --check` + 全量灌入)。
+- **同源缺陷(建议一并修,可选)**:`cfc-web/src/views/admin/ActivityEdit.vue` 的 element-ui datetime 面板分钟列 60 项全可点、靠 `@change` 兜底收敛(Round 2 遗留项 3 同源)。如需两端一致,做法相同:改为 `type="datetime" :clearable` + `:format` 仍 `yyyy-MM-dd HH:mm`,并在面板用 `disabledDate`/自定义 `time` 列不可行(element-ui 2.x 不支持步长),实际最小改动是**保留收敛 + 面板上不做 5 分钟限制**,即后台维持现状。**建议:A1 只做小程序端,后台不改**,理由写入验收说明。
+
+---
+
+## §二 A2 + A3:适用人群 + 费用模型重构
+
+### 2.1 新模型:两个活动级字段
+
+| 字段 | 取值 | 含义 |
+|---|---|---|
+| `audienceScope` | `all`(默认) / `member_only` | 是否**仅会员可参加** |
+| `audienceType` | `adult`(默认) / `include_child` / `include_elder` / `include_child_elder` | 参与人群构成,决定是否出现儿童/长者费用档 |
+
+费用档位(**会员/非会员为主口径**,儿童/长者为附加口径):
+
+| 档位 | 字段 | 来源 | 何时出现 |
+|---|---|---|---|
+| 成人·非会员 | `price` | **复用已有列,无迁移** | 恒显(默认口径) |
+| 成人·会员 | `memberPrice` | **复用已有列,无迁移** | 恒显(留空 = 同非会员价) |
+| 儿童·非会员 | `childFee` | 复用已有列 | `audienceType` 含 child |
+| 儿童·会员 | `childMemberFee` | 复用已有列 | `audienceType` 含 child |
+| 长者·非会员 | `elderFee` | **新增 2 列(迁移)** | `audienceType` 含 elder |
+| 长者·会员 | `elderMemberFee` | **新增 2 列(迁移)** | `audienceType` 含 elder |
+
+> **待确认 D1**:长者档是否真的需要独立价格?若长者一律复用成人价,可**不新增字段、零迁移**,只在 `audienceType` 上留长者标签用于展示与筛选。**建议默认:不新增长者价格列**(见 §五 D1)。
+
+### 2.2 表单联动规则(小程序 + 后台一致)
+
+- `audienceScope = member_only` → 非会员在详情页看到「仅会员可参加」,提交报名被**后端拒绝**(硬门禁)。
+- `audienceType = adult` → **隐藏**儿童/长者费用输入;提交时把 `childFee/childMemberFee/elderFee/elderMemberFee` 置 `0`(不清空列,仅写 0)。
+- `audienceType` 含 child → 显示儿童两档;含 elder → 显示长者两档(若已按 D1 决策新增长者列)。
+- 成人两档(`price` / `memberPrice`)**始终可见**,这就是「正常情况下只有会员、非会员」的落地。
+- 小程序创建页必须**新增 `priceYuan` / `memberPriceYuan` 输入**(当前缺失,与后台对齐)。
+
+### 2.3 数据模型与迁移
+
+- 新增列(迁移脚本,沿用 `DatabaseInitializer` 的 `information_schema.COLUMNS` 存在性检查范式,参考行 2254-2266 的 `visible_scope` 迁移写法):
+  - `audience_scope VARCHAR(20) DEFAULT 'all'`
+  - `audience_type VARCHAR(20) DEFAULT 'adult'`
+  - (仅当 D1 选「要长者价格」时)`elder_fee INT`、`elder_member_fee INT`
+- 实体 `Activity` + `ActivityDTO` 同步新增字段;**DTO 与接口无需变更签名**(新增字段向后兼容)。
+- `ALTER TABLE ... AFTER visible_to` 定位放置,保证列顺序可读。
+
+### 2.4 结算链路改造(`ActivityOrderService`,关键)
+
+替换现有 `isChild = memberId != null` 逻辑(`ActivityOrderService.java:81-84,91`),改为**按参与者身份分类**:
+
+```
+participantType:
+  self   → memberId 为空(本人/登录用户本人)        → price / memberPrice
+  child  → 家庭成员且 age < 18(或 generation 为子代) → childFee / childMemberFee
+  elder  → 家庭成员且 age >= 60                       → elderFee / elderMemberFee(或按 D1 回落成人价)
+  adult  → 其他成年家庭成员                            → price / memberPrice
+```
+
+依据全部落在 `FamilyMember` 现有字段(`age` / `generation` / `isSpouse`),**不新增任何角色字段**。
+
+**兼容存量数据(必须实现)**:老活动 `audience_type` 为 NULL/空 → 视为 `adult`,但成人档取值按**优先新列、回退旧列**:
+
+- 非会员价:`price > 0 ? price : parentFee`
+- 会员价:`memberPrice > 0 ? memberPrice : parentMemberFee`
+- 儿童档:`childFee` / `childMemberFee` 保持不变
+
+即 `parentFee` 从「家长」语义**降级为「成人非会员价」的历史别名**,保留 1–2 个版本后再评估物理删除。**不做数据回迁**,避免动存量活动收费。
+
+### 2.5 仅会员参加的硬门禁
+
+在 `ActivityOrderService` 创建订单入口(现有校验块 `ActivityOrderService.java:55-64` 之后)追加:
+
+```
+if ("member_only".equals(activity.getAudienceScope())) {
+    String level = membershipService.getMemberLevel(userId);
+    boolean isMember = "FAMILY".equals(level) || "PREMIUM".equals(level);
+    if (!isMember) return Result.error("该活动仅会员可参加");
+}
+```
+
+范式与 `TaskController.java:48` 的 `task.memberOnly` 一致(后端硬门禁,前端提示仅作体验优化,**不以前端为准**)。
+
+> **注意**:`memberLevel` 查询现有代码 `ActivityOrderService.java:66-68` 已存在,且 `userId` 可能为空(非登录用户)。**`audienceScope=member_only` 时非登录用户必须拒绝**(返回需登录错误,与既有「未登录则跳转登录」逻辑衔接)。
+
+### 2.6 展示层改造(A3)
+
+| 位置 | 现状 | 改为 |
+|---|---|---|
+| `activity-detail.vue:61-62` | 「孩子:¥…」「家长:¥…」恒显 | 按 `audienceType` 条件渲染:成人档恒显;儿童/长者档仅当 `audienceType` 含该类别且价格 > 0 |
+| `activity-detail.vue:827,829` | 海报价格文案「(家长)」「(孩子)」 | 「(成人)」「(儿童)」「(长者)」,按 `audienceType` 选择 |
+| `activity-detail.vue:977-985` `hasFee` / `hasMemberPrice` | 只读 4 个 child/parent 字段 | 纳入 `price` / `memberPrice`(否则只填成人价的付费活动会显示「免费」) |
+| `activity-detail.vue:1008-1015` `minFeeText` | 取 4 费最小值 | 取**当前用户可用档位**的最小值 |
+| `activity/index.vue:852-857` `normalizeActivity` | 用 4 费最小值覆盖 `price` | 不再覆盖 `price`;仅在 `price` 为空时回落 `parentFee` |
+| 报名弹窗 `activity-detail.vue:246-300` | 报名人只有姓名+手机+家庭成员勾选,无身份分类 | 展示每个报名人的身份标签(成人/儿童/长者)与对应单价,避免用户不知道为何收这个价 |
+| `cfc-web/.../ActivityEdit.vue:140-153` | 价格 + 会员价,**无适用人群** | 追加 `audienceScope`(单选)+ `audienceType`(多选)+ 儿童/长者费用条件显示,与小程序一致 |
+
+### 2.7 风险与回滚
+
+- **结算金额变化风险**:`participantType` 判定依赖 `FamilyMember.age`,存量成员若 `age` 为空则一律回落 `adult`(**不可**按 child 处理,否则会多收费)。这是本次最重要的回归点。
+- **免费活动**:`totalAmount == 0` 走免报名快照分支(`ActivityOrderService.java:96-104`),改造后必须保持该分支不受影响。
+- **回滚**:新增列均有 DEFAULT,回滚只需前端不传新字段;结算代码用 `audienceType` 为空即走旧映射,天然向后兼容。
+
+---
+
+## §三 G1 / G2:全系统「家长/孩子」措辞清查
+
+### 3.1 现状分布(已扫描 cfclub 当前 tip)
+
+**小程序前端 `.vue`(18+ 文件命中,前 8)**
+
+| 文件 | 命中 |
+|---|---|
+| `pages/body-detail/dimension-questionnaire.vue` | 17 |
+| `pages/home-pages/parent-index.vue` | 11 |
+| `pages/index-home/index.vue` | 9 |
+| `pages/rewards/rewards.vue` | 8 |
+| `pages/activity/activity-detail/activity-detail.vue` | 8 |
+| `pages/wealth/index.vue` | 7 |
+| `pages/parent/child-detail.vue` | 6 |
+| `pages/child/tasks.vue` | 6 |
+
+其后还有 `member-detail`、`child/rewards`、`teacher-profile`、`policy/privacy`、`mind-detail`、`wishes/detail`、`tasks/review`、`profile/components/ProfileHeader` 等。
+
+**后端 `.java`(前 8)**
+
+| 文件 | 命中 |
+|---|---|
+| `config/DatabaseInitializer.java` | 66 |
+| `service/EnergyService.java` | 57 |
+| `service/WishService.java` | 28 |
+| `service/TaskService.java` | 25 |
+| `service/RewardService.java` | 25 |
+| `service/FamilyMemberService.java` | 24 |
+| `service/DataMigrationService.java` | 20 |
+| `service/UserService.java` | 16 |
+
+**API 契约层遗留最重**(`cfc-frontend/utils/api.js` 仍在导出):`getChildren`、`switchToChild`、`switchBackToParent`、`getParentDashboard`、`getChildOverview`、`createChild`、`updateChild`、`getParentWishlist`、`createParentWishlist`、`generateParentInvite`、`getChildGuides`、`getChildNutritionProfile`、`getChildCompletionStats`、`getChildBadges` 等,且 `dist/` 产物同构存在。
+
+**页面目录级命名**:`pages/parent/`、`pages/child/`、`pages/home-pages/parent-index.vue`、`pages/home-pages/child-index.vue`。
+
+**重要正面事实**:`FamilyMember` 数据模型本身已是中性口径(`nickname` / `age` / `generation` / `isSpouse`),`ActivityRegistration` 也已统一为 `memberId` 语义(仅 DB 列名仍叫 `child_id`)。**遗留集中在文案、API 名、页面目录名、后端方法名与日志文案**,而非数据模型。
+
+### 3.2 分层策略(按风险分批,不做一次性大改名)
+
+| 层 | 范围 | 风险 | 批次 |
+|---|---|---|---|
+| **L1 文案层** | 用户可见文字:前端 `.vue` 模板文案 + 后端返回给前端的 toast / 提示语 / 消息模板。「家长/孩子」→「本人/家庭成员」「儿童/长者」「成人/儿童」 | 低(纯字符串,不动列名不动路径) | **G1,本批** |
+| **L2 前端契约与页面层** | `utils/api.js` 加**新别名**(保留旧名过渡);`pages/parent` → `pages/family`、`parent-index.vue` / `child-index.vue` → 中性名,`pages.json` 同步;**旧路径保留跳转壳** | 中(影响已发布小程序路径) | G2 |
+| **L3 后端内部层** | Java 方法名/变量名 `childId`→`memberId`、`ParentController`→`FamilyMemberController`、`isChild`→`isMinorMember`;DB 列 `child_id`→`member_id`(迁移 + 双写过渡) | 中高(外键迁移、与 T25 重叠) | G2 |
+| **L4 领域语义层** | `Activity.childFee` / `parentFee` 语义降级(见 §2.4) | 中 | 已并入 A2 |
+
+### 3.3 冲突面提示(派发前必看)
+
+- **G2 的 L3 与 T25(统一成员选择器)高度重叠**:`MemberSelector.vue`、`memberNormalize.js`、`activity-detail.vue`、`utils/api.js` 均已在 T25 分支上被改过。`member_id` 统一已在 T25 语义层完成,G2 只应做**命名与列名**,**不得**重做成员去重逻辑。
+- **必须串行**:G1 只改字符串可安全与 A 系列并行;G2 必须等 T25 全部收口后再派。
+- **L1 与 A3 有交集**:`activity-detail.vue` 的「孩子/家长」展示文案在 A3 里已要改(受 `audienceType` 条件驱动)。为避免两个会话改同一文件,**A3 内含 `activity-detail.vue` 的文案改造,G1 明确排除 `activity-detail.vue` 与 `create.vue`**。
+
+---
+
+## §四 任务拆分与依赖
+
+| ID | 标题 | 优先级 | 模块 | 依赖 | 说明 |
+|---|---|---|---|---|---|
+| **A1** | 活动时间选择器:选择时即只显示 5 分钟粒度,删除提示文案 | P1 | frontend | 无 | §一,单文件 `create.vue` |
+| **A2** | 活动适用人群 + 费用模型(DB 字段 + 创建表单 + 结算分类 + 仅会员硬门禁) | P1 | fullstack | D1/D2 拍板 | §二 2.1–2.5,含迁移脚本 |
+| **A3** | 活动费用展示层口径统一(小程序详情/列表/海报/报名弹窗 + 后台管理端) | P1 | fullstack | A2 | §二 2.6;**内含 `activity-detail.vue` 文案改造** |
+| **G1** | 全系统「家长/孩子」措辞清查(L1 文案层) | P2 | fullstack | A3 完成(避免同文件冲突) | §三 3.2;**排除 `activity-detail.vue`、`create.vue`** |
+| **G2** | 契约/目录/DB 列名中性化(L2 + L3) | P3 | fullstack | T25 全收口 + G1 | §三 3.2/3.3,风险最高,需单独评审 |
+
+派发顺序建议:**A1 与 A2 并行** → A3 → G1 → G2。
+
+---
+
+## §五 待确认决策点
+
+| 编号 | 决策 | 建议默认 | 影响 |
+|---|---|---|---|
+| **D1** | 长者是否需要独立价格列(`elder_fee` / `elder_member_fee`)? | **不新增**,长者复用成人价,仅在 `audienceType` 留长者标签 | 决定是否多 2 列迁移 |
+| **D2** | 「仅会员参加」硬门禁是否放后端下单入口? | **是**(与 `Task.memberOnly` 同范式,前端提示仅作体验) | 影响非登录用户报名路径 |
+| **D3** | 老活动 `parentFee` 语义降级为「成人非会员价别名」,是否接受? | **接受**(不改数据、不改存量收费) | 否则需一次性回迁存量活动 |
+| **D4** | 儿童/长者身份判定用 `FamilyMember.age`(<18 / ≥60)还是新增关系枚举? | **用 `age`**,`age` 为空一律回落 `adult` | 影响结算金额准确性 |
+| **D5** | G1 是否本轮就做? | **做**,但与 A3 串行(避免同文件冲突) | 影响排期 |
+| **D6** | 后台 `cfc-web/ActivityEdit.vue` 是否同批改? | **是**(纳入 A3,保证两端口径一致) | 否则两端继续分裂 |
+| **D7** | 后台 datetime 面板分钟列是否也限制为 5 倍数? | **不改**(element-ui 2.x 不支持步长,维持 `@change` 收敛) | 见 §一 1.3 |
+
+---
+
+## §六 验收标准
+
+**A1**
+1. 3 处时间选择器均为 `mode="multiSelector"`,分列恰好 12 项 `['00','05',…,'55']`,**选择过程中无法选中非 5 倍数分钟**。
+2. `grep "以 5 分钟为一个单元" cfc-frontend/pages/activity/create.vue` **零命中**(含 CSS 注释行 677)。
+3. `normalizeTimeTo5Min` 仍存在且 1440 分钟全量扫描 PASS(兜底不删)。
+4. 提交串恒为 `YYYY-MM-DDTHH:mm:00`,秒恒 `00`。
+5. 编辑既有活动(含历史非 5 倍数时间,如 14:07)回填后 picker 显示与提交值一致(14:05)。
+6. `node --check` 通过;模板 `vue-template-compiler` 零 error。
+
+**A2**
+1. `information_schema` 确认 `activities` 新增 `audience_scope` / `audience_type`(及长者列,若 D1 选要)。
+2. `mvn -pl cfc-backend -am compile` 通过。
+3. `audienceScope=member_only` + 非会员下单 → 返回「该活动仅会员可参加」;非登录用户下单 → 引导登录。
+4. 报名人 `age` 为空 → 一律按成人档计费(**不得**误判为儿童)。
+5. 老活动(`audience_type` 为空)结算金额与改造前**逐分一致**(用 `price`/`parentFee` 各非空 4 种组合各测一例)。
+6. 免费活动(`totalAmount==0`)免报名快照分支行为不变。
+
+**A3**
+1. `audienceType=adult` 的活动详情页不出现「儿童」「长者」「孩子」「家长」字样。
+2. 只填 `price`/`memberPrice`(`childFee`/`parentFee` 全 0)的付费活动,详情页正确显示价格,**不再误显「免费」**(回归 `hasFee` / `minFeeText` / `normalizeActivity`)。
+3. 报名弹窗按报名人身份显示对应单价。
+4. 后台管理端可设置 `audienceScope` / `audienceType` 并正确回显;两端保存的活动数据一致。
+5. 海报价格文案无「家长」「孩子」字样。
+6. 小程序 `node --check` + `cfc-web` `vue-cli-service build` 通过。
+
+**G1**
+1. 小程序用户可见文案无「家长」「孩子」(`grep -rl "家长\|孩子" cfc-frontend/pages cfc-frontend/components --include="*.vue"` 结果为空,或仅剩白名单文件并注明理由)。
+2. 后端返回给前端的文案(toast / 提示 / 消息模板 / 订阅消息)无「家长」「孩子」。
+3. **不改** DB 列名、API 路径、页面目录、Java 方法名。
+4. **排除** `activity-detail.vue` 与 `create.vue`(已由 A2/A3 覆盖)。
+5. 编译/构建全部通过。
+
+---
+
+## §七 不做的事(明确排除,避免范围膨胀)
+
+- 不做时间字段的业务校验(结束时间 ≥ 开始时间等)——现状本来就没有,不在本次范围。
+- 不重做统一成员选择器(T25 已完成,`ActivityRegistration.childId` 装 `memberId` 的现状保留)。
+- 不做物理删除 `childFee` / `parentFee` 列(降级为别名保留 1–2 个版本)。
+- 不做 `child_id` → `member_id` 外键迁移(属 G2,需单独评审)。
+- 不改小程序 `dist/` / `unpackage/` 产物(构建产物不入库)。

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 100 - 17
tools/xaf_agent_hub/tasks_backlog.json


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio