فهرست منبع

docs: 家庭挑战参与机制实现计划

asus 1 ماه پیش
والد
کامیت
dc0a74cc8f
2فایلهای تغییر یافته به همراه2167 افزوده شده و 0 حذف شده
  1. 2 0
      docs/superpowers/PROJECT-OVERVIEW.md
  2. 2165 0
      docs/superpowers/plans/2026-08-08-family-challenge-participation.md

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

@@ -339,6 +339,7 @@
 | `2026-08-06-family-challenge-flow-design.md` | 🟢 现状基线(含断点清单) | 家庭挑战使用流程梳理(7 断点,含实现状态标注) |
 | `2026-08-07-xaf-partner-ppt-redesign-design.md` | ✅ 已实施(16 页问题驱动式重构,slide-1~16 全量交付并渲染验证通过) | 浠艾福合作伙伴版 PPT 重构设计(困境→身份→成果→理念→平台→壁垒→未来→合作;封面标题"让每个家庭的健康,都被提前看见") |
 | `2026-08-07-xaf-internal-communication-design.md` | ✅ 已实施(16 页四层作战手册:一句话共识→标准话术→沟通红线→常见问答;独立目录 `ppt-output/internal/`,渲染验证通过) | 浠艾福对外沟通·统一口径(内部版)PPT 设计(战略对齐·统一口径,每页自包含可查阅) |
+| `2026-08-08-family-challenge-participation-design.md` | ✅ 设计已确认(2026-08-08 拍板:C3 只扣沟通,其余按默认) | 家庭挑战参与机制重构(任意成员发起/可选参与/全体同意后开始/方向性关系评分) |
 
 ### 实施计划(plans/)
 
@@ -401,6 +402,7 @@
 | `2026-08-04-registration-profile-backfill.md` | ✅ 已实施(12 Tasks:注册解耦最小集 + 个人中心卡片 + 功能场景就地补充自动回写 + 无家庭引导组件) | 新用户注册引导实施计划 |
 | `2026-08-04-report-confirm-member-selector.md` | ✅ 已完成 | 被检测人选择面板整合实现计划(report-confirm.vue 单文件) |
 | `2026-08-04-family-challenge-custom.md` | 🟢 已实施 | 家庭挑战自定义管理(创建/编辑/删除/模板) |
+| `2026-08-08-family-challenge-participation.md` | ✅ 已实施(9 commits:迁移182-185/3实体/ChallengeScoreService/respond端点/评分配置页/小程序同意拒绝+成员多选) | 家庭挑战参与机制重构(任意成员发起/可选参与/同意后开始/方向性评分) |
 
 ### 计划与设计文档(specs/)
 

+ 2165 - 0
docs/superpowers/plans/2026-08-08-family-challenge-participation.md

@@ -0,0 +1,2165 @@
+# 家庭挑战参与机制实现计划(含方向性关系评分)
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
+
+**Goal:** 家庭挑战从"家长创建即全员参与"重构为"任意成员发起 → 可选参与成员 → pending 待同意 → 全体同意后 active → 方向性关系评分即时结算",并配套后台评分配置页。
+
+**Architecture:** 后端新增 3 张表(participant 参与成员表、relationship_scores 方向分数表、score_config 评分配置表)与 3 个实体/Mapper;新建 `ChallengeScoreService` 统一结算方向分数+能量;改造 `FamilyChallengeService`(create 改 pending + respondChallenge + 打卡/结算口径收紧);控制器去家长门控并新增 respond 端点;cfc-web 新增评分配置管理页;小程序首页卡片支持同意/拒绝、管理页支持参与成员多选。
+
+**Tech Stack:** Spring Boot 2.7.18 + MyBatis-Plus + Java 8 / uni-app Vue 2 小程序 / Vue 2 + Element UI
+
+## Global Constraints
+
+- **接口统一 `@PostMapping`**,禁止 `@GetMapping/@PutMapping/@DeleteMapping`
+- **DI 用 `@Resource`**,字段名必须与类型默认 Bean Name 一致
+- **响应统一 `Result<T>`** (code/message/data)
+- **数据库迁移唯一入口**:`DatabaseInitializer.runMigrations()`;所有迁移幂等(try-catch 忽略已存在);`schema.sql` 必须同步新增表 DDL
+- **迁移编号递增**:当前最大 `迁移181` + `迁移20240709-01`,本次从 `迁移182` 起
+- **评分矩阵(用户 2026-08-08 拍板)**:
+  - 发起 initiate:I→每个其他参与成员 trust+0/intimacy+1/comm+1;其他成员→I 不动;能量仅发起人 +1
+  - 同意 agree:X→每个其他参与成员 trust+1/intimacy+1/comm+1;反向不动;能量 X 本人 +1
+  - 拒绝 reject:I→X trust-1/intimacy-1/comm-1;X→I 仅 comm+1(trust/intimacy 不动);已同意成员 A→X **仅 comm-1(C3)**;能量 I+A 每人 -1
+  - 方向分数落点在 actor 的"出向"行(A→B 表示 A 对 B 的分数)
+- **评分配置默认值**:initiate(energy+1,trust+0,intimacy+1,comm+1) / agree(全+1) / reject(energy-1,trust-1,intimacy-1,comm 幅度+1 方向代码固定);数值后台可配置,方向由代码固定
+- **方向分数不设上下限 clamp**(spec 未定义;初始基线 = 该成员在 `family_members` 的成员级分数,缺失默认 60.00)
+- **结算/打卡口径**:只统计 status=`agreed` 的参与成员,排除 rejected
+- **Java 8 语法**(无 var/switch 表达式),MyBatis-Plus LambdaQueryWrapper
+- **小程序限制**:禁止可选链 `?.`、禁止 CSS Grid、禁止 `:key` 表达式、禁止 `new Date(string)`(用 `parseDate()`)
+- **验证**:后端 `mvn clean compile` 必须通过(唯一验证方式);小程序只做语法/结构校验,禁止自行打包
+
+---
+
+## File Structure
+
+| 文件 | 动作 | 职责 |
+|------|------|------|
+| `cfc-backend/.../entity/FamilyChallengeParticipant.java` | 新建 | 挑战参与成员实体(pending/agreed/rejected) |
+| `cfc-backend/.../entity/FamilyRelationshipScore.java` | 新建 | 方向性关系分数实体(A→B 一行) |
+| `cfc-backend/.../entity/FamilyChallengeScoreConfig.java` | 新建 | 评分配置实体(action+scoreType) |
+| `cfc-backend/.../mapper/FamilyChallengeParticipantMapper.java` | 新建 | BaseMapper |
+| `cfc-backend/.../mapper/FamilyRelationshipScoreMapper.java` | 新建 | BaseMapper |
+| `cfc-backend/.../mapper/FamilyChallengeScoreConfigMapper.java` | 新建 | BaseMapper |
+| `cfc-backend/.../service/ChallengeScoreService.java` | 新建 | 方向分数读取/初始化/增减 + 能量发放/扣减(核心结算引擎) |
+| `cfc-backend/.../service/FamilyChallengeService.java` | 修改 | createChallenge 改 pending+participant+发起结算;新增 respondChallenge;updateProgress/结算/列表口径收紧 |
+| `cfc-backend/.../controller/FamilyChallengeController.java` | 修改 | 新增 respond 端点;create 去家长门控+memberId;list 返回 myStatus |
+| `cfc-backend/.../controller/admin/AdminChallengeScoreConfigController.java` | 新建 | 评分配置 CRUD(list/update) |
+| `cfc-backend/.../service/EnergyService.java` | 修改 | 新增 `awardEnergyByCode`(对称于 deductEnergyByCode) |
+| `cfc-backend/.../config/DatabaseInitializer.java` | 修改 | 迁移182-185:3 新表 + 默认配置 + 存量挑战补 participant |
+| `cfc-backend/src/main/resources/schema.sql` | 修改 | 同步 3 张新表 DDL |
+| `cfc-backend/src/test/.../service/FamilyChallengeServiceTest.java` | 修改 | 更新 mock 依赖 + 新增 respondChallenge/createChallenge 校验测试 |
+| `cfc-backend/src/test/.../service/ChallengeScoreServiceTest.java` | 新建 | 方向分数/能量结算矩阵测试 |
+| `cfc-web/src/api/challengeScore.js` | 新建 | 管理端 API 封装 |
+| `cfc-web/src/views/admin/ChallengeScoreConfig.vue` | 新建 | 评分配置管理页 |
+| `cfc-web/src/router/index.js` | 修改 | 注册配置页路由 |
+| `cfc-frontend/utils/api.js` | 修改 | 新增 `respondChallenge` |
+| `cfc-frontend/components/FamilyChallengeCard.vue` | 修改 | pending 渲染 + 同意/拒绝按钮 + rejected 徽标 |
+| `cfc-frontend/pages/health/challenge-manage.vue` | 修改 | 移除角色门控 + 参与成员多选 + pending 展示 |
+| `cfc-frontend/pages/index-home/index.vue` | 修改 | 处理 respond 事件 |
+| `docs/superpowers/PROJECT-OVERVIEW.md` | 修改 | 同步计划条目 |
+
+---
+
+### Task 1: 数据库迁移 + schema.sql 同步
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(runMigrations() 末尾,`迁移181` 之后、类闭合 `}` 之前)
+- Modify: `cfc-backend/src/main/resources/schema.sql`(family_challenge_progress 定义之后追加 3 张表)
+
+**Interfaces:**
+- Produces: 表 `family_challenge_participant` / `family_relationship_scores` / `family_challenge_score_config`(含 12 行默认配置数据);存量 family_challenge 补齐 participant 行
+
+- [x] **Step 1: DatabaseInitializer 追加迁移182-185**
+
+在 `DatabaseInitializer.java` 的 `// 迁移181: ...` ensureColumn 行之后(8120 行 `}` 之前)追加:
+
+```java
+		// 迁移182: 创建 family_challenge_participant 表(挑战参与成员,任意成员发起/可选参与)
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_challenge_participant (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"challenge_id BIGINT NOT NULL COMMENT '挑战ID', " +
+					"member_id BIGINT NOT NULL COMMENT '家庭成员ID', " +
+					"status VARCHAR(20) NOT NULL COMMENT 'pending/agreed/rejected', " +
+					"agreed_at DATETIME NULL COMMENT '同意/拒绝时间', " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+					"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+					"UNIQUE KEY uk_challenge_member (challenge_id, member_id), " +
+					"INDEX idx_member (member_id)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭挑战参与成员'");
+			log.info("已创建family_challenge_participant表");
+		} catch (Exception e) {
+			log.warn("创建 family_challenge_participant 表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移183: 创建 family_relationship_scores 表(方向性关系分数,A对B ≠ B对A)
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_relationship_scores (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+					"from_member_id BIGINT NOT NULL COMMENT '分数持有方(A对B的行) ', " +
+					"to_member_id BIGINT NOT NULL COMMENT '分数对象方', " +
+					"trust_score DECIMAL(5,2) DEFAULT 0 COMMENT 'A对B的信任', " +
+					"intimacy_score DECIMAL(5,2) DEFAULT 0 COMMENT 'A对B的亲近', " +
+					"communication_score DECIMAL(5,2) DEFAULT 0 COMMENT 'A对B的沟通', " +
+					"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+					"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+					"UNIQUE KEY uk_from_to (from_member_id, to_member_id), " +
+					"INDEX idx_family (family_id)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='方向性关系分数'");
+			log.info("已创建family_relationship_scores表");
+		} catch (Exception e) {
+			log.warn("创建 family_relationship_scores 表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移184: 创建 family_challenge_score_config 表(挑战评分配置,后台可配置)+ 默认数据
+		try {
+			jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_challenge_score_config (" +
+					"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+					"action VARCHAR(20) NOT NULL COMMENT 'initiate/agree/reject', " +
+					"score_type VARCHAR(20) NOT NULL COMMENT 'energy/trust/intimacy/communication', " +
+					"value INT NOT NULL DEFAULT 0 COMMENT '增减值(可正可负,默认±1)', " +
+					"enabled TINYINT DEFAULT 1, " +
+					"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+					"UNIQUE KEY uk_action_type (action, score_type)" +
+					") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭挑战评分配置'");
+			jdbcTemplate.execute("INSERT IGNORE INTO family_challenge_score_config (action, score_type, value, enabled) VALUES " +
+					"('initiate','energy',1,1),('initiate','trust',0,1),('initiate','intimacy',1,1),('initiate','communication',1,1)," +
+					"('agree','energy',1,1),('agree','trust',1,1),('agree','intimacy',1,1),('agree','communication',1,1)," +
+					"('reject','energy',-1,1),('reject','trust',-1,1),('reject','intimacy',-1,1),('reject','communication',1,1)");
+			log.info("已创建family_challenge_score_config表并写入默认配置");
+		} catch (Exception e) {
+			log.warn("创建 family_challenge_score_config 表失败(可能已存在): " + e.getMessage());
+		}
+
+		// 迁移185: 存量挑战补全 participant 行(全员 agreed,兼容旧数据,不破坏)
+		try {
+			jdbcTemplate.execute("INSERT IGNORE INTO family_challenge_participant (challenge_id, member_id, status, agreed_at) " +
+					"SELECT fc.id, fm.id, 'agreed', NOW() FROM family_challenge fc " +
+					"JOIN family_members fm ON fm.family_id = fc.family_id " +
+					"WHERE NOT EXISTS (SELECT 1 FROM family_challenge_participant fcp WHERE fcp.challenge_id = fc.id)");
+			log.info("已为存量挑战补全participant行");
+		} catch (Exception e) {
+			log.warn("存量挑战补全participant失败(可能已处理): " + e.getMessage());
+		}
+```
+
+- [x] **Step 2: schema.sql 追加 3 张新表 DDL**
+
+在 `schema.sql` 中 `family_challenge_progress` 表定义(3425 行 `)` 之后、`health_interaction` 之前插入:
+
+```sql
+-- 家庭挑战参与成员表(参与机制重构:任意成员发起/可选参与/同意后开始)
+CREATE TABLE IF NOT EXISTS family_challenge_participant (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    challenge_id BIGINT NOT NULL COMMENT '挑战ID',
+    member_id BIGINT NOT NULL COMMENT '家庭成员ID',
+    status VARCHAR(20) NOT NULL COMMENT 'pending/agreed/rejected',
+    agreed_at DATETIME NULL COMMENT '同意/拒绝时间',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_challenge_member (challenge_id, member_id),
+    INDEX idx_member (member_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭挑战参与成员';
+
+-- 方向性关系分数表(A对B ≠ B对A)
+CREATE TABLE IF NOT EXISTS family_relationship_scores (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    family_id BIGINT NOT NULL COMMENT '家庭ID',
+    from_member_id BIGINT NOT NULL COMMENT '分数持有方(A对B的行)',
+    to_member_id BIGINT NOT NULL COMMENT '分数对象方',
+    trust_score DECIMAL(5,2) DEFAULT 0 COMMENT 'A对B的信任',
+    intimacy_score DECIMAL(5,2) DEFAULT 0 COMMENT 'A对B的亲近',
+    communication_score DECIMAL(5,2) DEFAULT 0 COMMENT 'A对B的沟通',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_from_to (from_member_id, to_member_id),
+    INDEX idx_family (family_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='方向性关系分数';
+
+-- 家庭挑战评分配置表(后台可配置)
+CREATE TABLE IF NOT EXISTS family_challenge_score_config (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    action VARCHAR(20) NOT NULL COMMENT 'initiate/agree/reject',
+    score_type VARCHAR(20) NOT NULL COMMENT 'energy/trust/intimacy/communication',
+    value INT NOT NULL DEFAULT 0 COMMENT '增减值(可正可负,默认±1)',
+    enabled TINYINT DEFAULT 1,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    UNIQUE KEY uk_action_type (action, score_type)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭挑战评分配置';
+```
+
+- [x] **Step 3: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [x] **Step 4: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java cfc-backend/src/main/resources/schema.sql
+git commit -m "feat(challenge): 参与机制迁移-新增participant/方向分数/评分配置表"
+```
+
+---
+
+### Task 2: 后端实体 + Mapper(3 个实体 + 3 个 Mapper)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallengeParticipant.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyRelationshipScore.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallengeScoreConfig.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyChallengeParticipantMapper.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyRelationshipScoreMapper.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyChallengeScoreConfigMapper.java`
+
+**Interfaces:**
+- Produces: 实体 `FamilyChallengeParticipant`(challengeId/memberId/status/agreedAt)、`FamilyRelationshipScore`(familyId/fromMemberId/toMemberId/trustScore/intimacyScore/communicationScore,BigDecimal)、`FamilyChallengeScoreConfig`(action/scoreType/value/enabled)
+
+- [x] **Step 1: 创建 FamilyChallengeParticipant 实体**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("family_challenge_participant")
+public class FamilyChallengeParticipant implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long challengeId;
+
+    private Long memberId;
+
+    /** 状态: pending/agreed/rejected */
+    private String status;
+
+    /** 同意/拒绝时间 */
+    private Date agreedAt;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}
+```
+
+- [x] **Step 2: 创建 FamilyRelationshipScore 实体**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("family_relationship_scores")
+public class FamilyRelationshipScore implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long familyId;
+
+    /** 分数持有方(A对B的行) */
+    private Long fromMemberId;
+
+    /** 分数对象方 */
+    private Long toMemberId;
+
+    /** A对B的信任 */
+    private BigDecimal trustScore;
+
+    /** A对B的亲近 */
+    private BigDecimal intimacyScore;
+
+    /** A对B的沟通 */
+    private BigDecimal communicationScore;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}
+```
+
+- [x] **Step 3: 创建 FamilyChallengeScoreConfig 实体**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("family_challenge_score_config")
+public class FamilyChallengeScoreConfig implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 动作: initiate/agree/reject */
+    private String action;
+
+    /** 分数类型: energy/trust/intimacy/communication */
+    private String scoreType;
+
+    /** 增减值(可正可负,默认±1) */
+    private Integer value;
+
+    /** 是否启用: 1=启用 */
+    private Integer enabled;
+
+    private Date updatedAt;
+}
+```
+
+- [x] **Step 4: 创建 3 个 Mapper**
+
+每个文件独立创建(与现有 `FamilyChallengeProgressMapper` 完全同构):
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FamilyChallengeParticipant;
+
+public interface FamilyChallengeParticipantMapper extends BaseMapper<FamilyChallengeParticipant> {
+}
+```
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FamilyRelationshipScore;
+
+public interface FamilyRelationshipScoreMapper extends BaseMapper<FamilyRelationshipScore> {
+}
+```
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FamilyChallengeScoreConfig;
+
+public interface FamilyChallengeScoreConfigMapper extends BaseMapper<FamilyChallengeScoreConfig> {
+}
+```
+
+- [x] **Step 5: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [x] **Step 6: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallengeParticipant.java cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyRelationshipScore.java cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallengeScoreConfig.java cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyChallengeParticipantMapper.java cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyRelationshipScoreMapper.java cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyChallengeScoreConfigMapper.java
+git commit -m "feat(challenge): 新增participant/方向分数/评分配置实体与Mapper"
+```
+
+---
+
+### Task 3: EnergyService 新增 awardEnergyByCode
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java`(`deductEnergyByCode` 方法之后,约 1381 行)
+
+**Interfaces:**
+- Produces: `public Map<String, Integer> awardEnergyByCode(Long memberId, String dimensionCode, Integer amount, String reason)` — 按维度 code 发放能量(对称于 `deductEnergyByCode`),返回发放结果 map(空=未发放)
+
+**Context:** 现有 `awardEnergy(memberId, sourceType, sourceId, ...)` 依赖 energy_source_config / behavior_config 配置,挑战 ±1 能量直接按维度 code 发放最可靠。
+
+- [x] **Step 1: 在 deductEnergyByCode 后新增 awardEnergyByCode**
+
+在 `deductEnergyByCode` 方法(1373-1381 行)的 `}` 之后追加:
+
+```java
+	/**
+	 * 发放能量 — 通过维度code(自动查ID),无日上限/配置约束,供低频行为直接发放
+	 *
+	 * @return 发放结果 Map<dimensionCode, amount>(空 = 维度不存在未发放)
+	 */
+	@Transactional
+	public Map<String, Integer> awardEnergyByCode(Long memberId, String dimensionCode, Integer amount, String reason) {
+		if (amount == null || amount <= 0) return new HashMap<>();
+		EnergyDimension dim = getDimByCode(dimensionCode);
+		if (dim == null) {
+			log.warn("EnergyService.awardEnergyByCode: dimension not found, code={}", dimensionCode);
+			return new HashMap<>();
+		}
+		Map<Long, Integer> allocations = new LinkedHashMap<>();
+		allocations.put(dim.getId(), amount);
+		return executeAward(memberId, "challenge", null, amount, reason, null, allocations);
+	}
+```
+
+**注意**:`executeAward` 已是 private 方法(1229 行),签名 `(Long memberId, String sourceType, Long sourceId, Integer totalAmount, String description, Integer expireDays, Map<Long,Integer> allocations)`,可直接调用。需确认 `HashMap`/`LinkedHashMap` 已 import(文件头部已有 `java.util.*` 或单个 import——如缺失则补 import)。
+
+- [x] **Step 2: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [x] **Step 3: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java
+git commit -m "feat(energy): EnergyService新增awardEnergyByCode按维度code发放"
+```
+
+---
+
+### Task 4: ChallengeScoreService(方向分数 + 能量结算核心)
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeScoreService.java`
+
+**Interfaces:**
+- Consumes: `FamilyRelationshipScoreMapper` / `FamilyChallengeScoreConfigMapper` / `FamilyMemberMapper` / `EnergyService`(`awardEnergyByCode` + `deductEnergyByCode`)
+- Produces:
+  - `public void applyInitiate(Long familyId, Long initiatorId, List<Long> participantIds, Long challengeId)`
+  - `public void applyAgree(Long familyId, Long memberId, List<Long> participantIds, Long challengeId)`
+  - `public void applyReject(Long familyId, Long rejectorId, Long initiatorId, List<Long> agreedMemberIds, Long challengeId)`
+
+**评分矩阵(用户拍板,必须逐格对齐):**
+
+| 动作 | 落点 | trust | intimacy | communication | energy |
+|------|------|:---:|:---:|:---:|:---:|
+| initiate | I→每个其他参与成员 | +0 | +1 | +1 | — |
+| initiate | 其他成员→I | 0 | 0 | 0 | — |
+| initiate | I 本人 | — | — | — | +1 |
+| agree | X→每个其他参与成员 | +1 | +1 | +1 | — |
+| agree | 其他成员→X | 0 | 0 | 0 | — |
+| agree | X 本人 | — | — | — | +1 |
+| reject | I→X | -1 | -1 | -1 | — |
+| reject | X→I | 0 | 0 | +1 | — |
+| reject | 已同意成员 A→X(C3) | 0 | 0 | -1 | — |
+| reject | I + A 每人 | — | — | — | -1 |
+
+- [x] **Step 1: 创建 ChallengeScoreService**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.FamilyChallengeScoreConfig;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.FamilyRelationshipScore;
+import com.etotem.cfc.mapper.FamilyChallengeScoreConfigMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.FamilyRelationshipScoreMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 家庭挑战方向性关系评分结算引擎。
+ *
+ * 方向性语义:A→B 表示"A 对 B 的分数",存于 family_relationship_scores 行 (A→B)。
+ * 动作执行者(actor)永远是被加减的主体,落点在其"出向"行上;反向行不动。
+ * 数值从 family_challenge_score_config 读取(后台可配置),方向由本类代码固定。
+ */
+@Slf4j
+@Service
+public class ChallengeScoreService {
+
+    /** 配置默认值兜底(与 spec §3.4 一致):action -> scoreType -> value */
+    private static final Map<String, Map<String, Integer>> DEFAULT_CONFIG = new HashMap<>();
+
+    static {
+        Map<String, Integer> initiate = new HashMap<>();
+        initiate.put("energy", 1);
+        initiate.put("trust", 0);
+        initiate.put("intimacy", 1);
+        initiate.put("communication", 1);
+        DEFAULT_CONFIG.put("initiate", initiate);
+
+        Map<String, Integer> agree = new HashMap<>();
+        agree.put("energy", 1);
+        agree.put("trust", 1);
+        agree.put("intimacy", 1);
+        agree.put("communication", 1);
+        DEFAULT_CONFIG.put("agree", agree);
+
+        Map<String, Integer> reject = new HashMap<>();
+        reject.put("energy", -1);
+        reject.put("trust", -1);
+        reject.put("intimacy", -1);
+        reject.put("communication", 1); // 幅度,方向由代码固定
+        DEFAULT_CONFIG.put("reject", reject);
+    }
+
+    @Resource
+    private FamilyRelationshipScoreMapper relationshipScoreMapper;
+
+    @Resource
+    private FamilyChallengeScoreConfigMapper scoreConfigMapper;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
+    @Resource
+    private EnergyService energyService;
+
+    /** 发起结算:I→每个其他参与成员 trust+0/intimacy+1/comm+1;能量仅发起人 +1 */
+    @Transactional
+    public void applyInitiate(Long familyId, Long initiatorId, List<Long> participantIds, Long challengeId) {
+        if (familyId == null || initiatorId == null || participantIds == null) return;
+        int intimacy = configValue("initiate", "intimacy");
+        int communication = configValue("initiate", "communication");
+        for (Long otherId : participantIds) {
+            if (otherId == null || otherId.equals(initiatorId)) continue;
+            adjustScore(familyId, initiatorId, otherId,
+                    configValue("initiate", "trust"), intimacy, communication);
+        }
+        awardEnergy(initiatorId, challengeId, configValue("initiate", "energy"), "发起家庭挑战");
+    }
+
+    /** 同意结算:X→每个其他参与成员 trust+1/intimacy+1/comm+1;能量 X 本人 +1 */
+    @Transactional
+    public void applyAgree(Long familyId, Long memberId, List<Long> participantIds, Long challengeId) {
+        if (familyId == null || memberId == null || participantIds == null) return;
+        int trust = configValue("agree", "trust");
+        int intimacy = configValue("agree", "intimacy");
+        int communication = configValue("agree", "communication");
+        for (Long otherId : participantIds) {
+            if (otherId == null || otherId.equals(memberId)) continue;
+            adjustScore(familyId, memberId, otherId, trust, intimacy, communication);
+        }
+        awardEnergy(memberId, challengeId, configValue("agree", "energy"), "同意家庭挑战");
+    }
+
+    /**
+     * 拒绝结算(矩阵见类注释):
+     * I→X: trust-1/intimacy-1/comm-1;X→I: 仅 comm+1;已同意成员A→X: 仅 comm-1(C3);能量 I+A 每人 -1
+     */
+    @Transactional
+    public void applyReject(Long familyId, Long rejectorId, Long initiatorId, List<Long> agreedMemberIds, Long challengeId) {
+        if (familyId == null || rejectorId == null || initiatorId == null) return;
+        int commAbs = Math.abs(configValue("reject", "communication"));
+
+        // I→X: trust/intimacy/communication 全扣
+        adjustScore(familyId, initiatorId, rejectorId,
+                configValue("reject", "trust"),
+                configValue("reject", "intimacy"),
+                -commAbs);
+
+        // X→I: 仅 communication +1(trust/intimacy 不动)
+        adjustScore(familyId, rejectorId, initiatorId, 0, 0, commAbs);
+
+        // 已同意成员 A→X: 仅 communication -1(C3,trust/intimacy 不扣)
+        if (agreedMemberIds != null) {
+            for (Long a : agreedMemberIds) {
+                if (a == null || a.equals(rejectorId)) continue;
+                adjustScore(familyId, a, rejectorId, 0, 0, -commAbs);
+            }
+        }
+
+        // 能量:发起人 + 已同意成员 每人 -1
+        int energyVal = configValue("reject", "energy");
+        deductEnergy(initiatorId, challengeId, energyVal, "成员拒绝家庭挑战");
+        if (agreedMemberIds != null) {
+            for (Long a : agreedMemberIds) {
+                if (a == null || a.equals(rejectorId)) continue;
+                deductEnergy(a, challengeId, energyVal, "成员拒绝家庭挑战");
+            }
+        }
+    }
+
+    // ==================== 私有辅助 ====================
+
+    /** 读取配置值;无配置行/禁用时用内置默认 */
+    private int configValue(String action, String scoreType) {
+        try {
+            FamilyChallengeScoreConfig cfg = scoreConfigMapper.selectOne(
+                    new LambdaQueryWrapper<FamilyChallengeScoreConfig>()
+                            .eq(FamilyChallengeScoreConfig::getAction, action)
+                            .eq(FamilyChallengeScoreConfig::getScoreType, scoreType));
+            if (cfg != null && cfg.getValue() != null && (cfg.getEnabled() == null || cfg.getEnabled() == 1)) {
+                return cfg.getValue();
+            }
+        } catch (Exception e) {
+            log.warn("查询挑战评分配置失败: action={}, scoreType={}, err={}", action, scoreType, e.getMessage());
+        }
+        Map<String, Integer> actionCfg = DEFAULT_CONFIG.get(action);
+        Integer v = actionCfg != null ? actionCfg.get(scoreType) : null;
+        return v != null ? v : 0;
+    }
+
+    /** 对 (from→to) 方向行增减三项分数(delta 为 0 时跳过该列) */
+    private void adjustScore(Long familyId, Long fromId, Long toId,
+                             int trustDelta, int intimacyDelta, int communicationDelta) {
+        FamilyRelationshipScore score = getOrCreateScore(familyId, fromId, toId);
+        if (trustDelta != 0) {
+            score.setTrustScore(safeAdd(score.getTrustScore(), trustDelta));
+        }
+        if (intimacyDelta != 0) {
+            score.setIntimacyScore(safeAdd(score.getIntimacyScore(), intimacyDelta));
+        }
+        if (communicationDelta != 0) {
+            score.setCommunicationScore(safeAdd(score.getCommunicationScore(), communicationDelta));
+        }
+        relationshipScoreMapper.updateById(score);
+    }
+
+    /**
+     * 获取 (from→to) 方向行;不存在则创建。
+     * 初始基线:取 from 成员在 family_members 的成员级分数(无方向),缺失默认 60.00。
+     * 并发兜底:insert 撞唯一键时重查返回已有行。
+     */
+    private FamilyRelationshipScore getOrCreateScore(Long familyId, Long fromId, Long toId) {
+        FamilyRelationshipScore existing = relationshipScoreMapper.selectOne(
+                new LambdaQueryWrapper<FamilyRelationshipScore>()
+                        .eq(FamilyRelationshipScore::getFromMemberId, fromId)
+                        .eq(FamilyRelationshipScore::getToMemberId, toId));
+        if (existing != null) return existing;
+
+        FamilyRelationshipScore score = new FamilyRelationshipScore();
+        score.setFamilyId(familyId);
+        score.setFromMemberId(fromId);
+        score.setToMemberId(toId);
+        FamilyMember from = familyMemberMapper.selectById(fromId);
+        score.setTrustScore(from != null && from.getTrustScore() != null
+                ? from.getTrustScore() : new BigDecimal("60.00"));
+        score.setIntimacyScore(from != null && from.getIntimacyScore() != null
+                ? from.getIntimacyScore() : new BigDecimal("60.00"));
+        score.setCommunicationScore(from != null && from.getCommunicationScore() != null
+                ? from.getCommunicationScore() : new BigDecimal("60.00"));
+        try {
+            relationshipScoreMapper.insert(score);
+            return score;
+        } catch (Exception e) {
+            FamilyRelationshipScore retry = relationshipScoreMapper.selectOne(
+                    new LambdaQueryWrapper<FamilyRelationshipScore>()
+                            .eq(FamilyRelationshipScore::getFromMemberId, fromId)
+                            .eq(FamilyRelationshipScore::getToMemberId, toId));
+            if (retry != null) return retry;
+            throw e;
+        }
+    }
+
+    private BigDecimal safeAdd(BigDecimal base, int delta) {
+        BigDecimal v = base != null ? base : BigDecimal.ZERO;
+        return v.add(BigDecimal.valueOf(delta));
+    }
+
+    private void awardEnergy(Long memberId, Long challengeId, int amount, String reason) {
+        if (amount <= 0) return;
+        try {
+            energyService.awardEnergyByCode(memberId, "action", amount, reason + "(挑战ID:" + challengeId + ")");
+        } catch (Exception e) {
+            log.error("挑战能量发放失败: memberId={}, challengeId={}", memberId, challengeId, e);
+        }
+    }
+
+    private void deductEnergy(Long memberId, Long challengeId, int amount, String reason) {
+        if (amount >= 0) return;
+        try {
+            int result = energyService.deductEnergyByCode(memberId, "action", -amount, reason + "(挑战ID:" + challengeId + ")");
+            if (result == -1) {
+                log.warn("挑战能量扣减跳过(余额不足): memberId={}, challengeId={}", memberId, challengeId);
+            }
+        } catch (Exception e) {
+            log.error("挑战能量扣减失败: memberId={}, challengeId={}", memberId, challengeId, e);
+        }
+    }
+}
+```
+
+- [x] **Step 2: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [x] **Step 3: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ChallengeScoreService.java
+git commit -m "feat(challenge): ChallengeScoreService方向性评分结算引擎"
+```
+
+---
+
+### Task 5: FamilyChallengeService 改造(create 改 pending + respondChallenge + 口径收紧)
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallenge.java`(新增 myStatus 非表字段)
+
+**Interfaces:**
+- Consumes: `ChallengeScoreService` / `FamilyChallengeParticipantMapper`(@Resource 注入)
+- Produces:
+  - `public Result<Long> createChallenge(Long familyId, Long userId, Long memberId, Map<String,Object> params)`(签名变更:新增 memberId)
+  - `public Result<Void> respondChallenge(Long challengeId, Long memberId, String action)`(新增)
+  - `public List<FamilyChallenge> getActiveChallenges(Long familyId, Long currentMemberId)`(签名变更:新增 currentMemberId)
+  - `public List<FamilyChallenge> getChallengeHistory(Long familyId)`(内部口径调整)
+  - `public Result<Void> updateProgress(Long challengeId, Long childId, int delta)`(新增 participant 校验)
+  - 私有 `listParticipants(Long challengeId)` / `setMyStatus(List<FamilyChallenge>, Long currentMemberId)`
+
+- [x] **Step 1: FamilyChallenge 实体新增 myStatus 字段**
+
+在 `FamilyChallenge.java` 的 `creatorId` 字段之后、类闭合 `}` 之前追加:
+
+```java
+	/** 非表字段:当前登录成员在本挑战的参与状态(pending/agreed/rejected/null=非参与) */
+	@TableField(exist = false)
+	private String myStatus;
+```
+
+- [x] **Step 2: FamilyChallengeService 注入新依赖**
+
+在类顶部(`pointsService` 字段之后)追加:
+
+```java
+	@Resource
+	private ChallengeScoreService challengeScoreService;
+
+	@Resource
+	private FamilyChallengeParticipantMapper familyChallengeParticipantMapper;
+```
+
+并补 import:`com.etotem.cfc.entity.FamilyChallengeParticipant`、`com.etotem.cfc.mapper.FamilyChallengeParticipantMapper`、`java.util.stream.Collectors`(如无)。
+
+- [x] **Step 3: 新增 respondChallenge 方法**
+
+在 `updateProgress` 方法之前(`@Transactional public Result<Void> updateProgress` 上方)插入:
+
+```java
+	/**
+	 * 同意/拒绝挑战(参与成员动作,即时结算方向评分)。
+	 * agree:status→agreed → 结算同意评分 → 除 rejected 外全部 agreed → 挑战转 active
+	 * reject:status→rejected → 结算拒绝评分 → 除发起人外无剩余参与成员 → 挑战自动 cancelled(Q6)
+	 */
+	@Transactional
+	public Result<Void> respondChallenge(Long challengeId, Long memberId, String action) {
+		if (challengeId == null || memberId == null) {
+			return Result.error("参数错误");
+		}
+		if (!"agree".equals(action) && !"reject".equals(action)) {
+			return Result.error("action必须是agree或reject");
+		}
+
+		FamilyChallenge challenge = familyChallengeMapper.selectById(challengeId);
+		if (challenge == null) {
+			return Result.error("挑战不存在");
+		}
+		if (!"pending".equals(challenge.getStatus())) {
+			return Result.error("挑战当前不可操作");
+		}
+
+		// 调用者必须是该挑战 participant 且 status=pending(幂等:已处理过则拒绝)
+		FamilyChallengeParticipant participant = familyChallengeParticipantMapper.selectOne(
+				new LambdaQueryWrapper<FamilyChallengeParticipant>()
+						.eq(FamilyChallengeParticipant::getChallengeId, challengeId)
+						.eq(FamilyChallengeParticipant::getMemberId, memberId));
+		if (participant == null) {
+			return Result.error("你不在该挑战参与名单中");
+		}
+		if (!"pending".equals(participant.getStatus())) {
+			return Result.error("该挑战已处理过,请勿重复操作");
+		}
+
+		List<FamilyChallengeParticipant> allParticipants = listParticipants(challengeId);
+		List<Long> agreedMemberIds = new ArrayList<>();
+		for (FamilyChallengeParticipant p : allParticipants) {
+			if ("agreed".equals(p.getStatus())) agreedMemberIds.add(p.getMemberId());
+		}
+
+		if ("agree".equals(action)) {
+			participant.setStatus("agreed");
+			participant.setAgreedAt(new Date());
+			familyChallengeParticipantMapper.updateById(participant);
+
+			List<Long> participantIds = new ArrayList<>();
+			for (FamilyChallengeParticipant p : allParticipants) {
+				participantIds.add(p.getMemberId());
+			}
+			challengeScoreService.applyAgree(challenge.getFamilyId(), memberId, participantIds, challengeId);
+
+			// 除 rejected 外全部 agreed → active
+			boolean allAgreed = true;
+			for (FamilyChallengeParticipant p : allParticipants) {
+				if ("rejected".equals(p.getStatus())) continue;
+				if (!"agreed".equals(p.getStatus()) && !p.getMemberId().equals(memberId)) {
+					allAgreed = false;
+					break;
+				}
+			}
+			if (allAgreed) {
+				challenge.setStatus("active");
+				familyChallengeMapper.updateById(challenge);
+			}
+		} else {
+			// reject
+			participant.setStatus("rejected");
+			participant.setAgreedAt(new Date());
+			familyChallengeParticipantMapper.updateById(participant);
+
+			Long initiatorId = challenge.getCreatorId() != null
+					? resolveMemberIdByUserId(challenge.getCreatorId()) : null;
+			// 发起人为 null(系统创建)时按 memberId 归属推断:取 agreed 成员中第一个?保守处理:不结算 I→X/X→I,仅结算 A→X 与能量
+			if (initiatorId != null) {
+				challengeScoreService.applyReject(challenge.getFamilyId(), memberId, initiatorId, agreedMemberIds, challengeId);
+			} else {
+				// 系统创建挑战:仅已同意成员→拒绝人 扣沟通 + 能量扣减(无发起人维度)
+				challengeScoreService.applyReject(challenge.getFamilyId(), memberId, memberId, agreedMemberIds, challengeId);
+			}
+
+			// 除发起人外无剩余参与成员 → cancelled(Q6)
+			boolean onlyInitiatorLeft = true;
+			int nonRejected = 0;
+			for (FamilyChallengeParticipant p : allParticipants) {
+				if (!"rejected".equals(p.getStatus())) nonRejected++;
+			}
+			// 非 rejected 的只剩发起人本人(且发起人是 participant)
+			if (nonRejected <= 1) {
+				challenge.setStatus("cancelled");
+				familyChallengeMapper.updateById(challenge);
+			}
+		}
+
+		return Result.success();
+	}
+
+	/** 解析 user_id → 家庭成员ID(发起人) */
+	private Long resolveMemberIdByUserId(Long userId) {
+		if (userId == null) return null;
+		// 发起人 creatorId 存的是 userId;查其家庭成员
+		List<FamilyMember> members = familyMemberMapper.selectList(
+				new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getUserId, userId));
+		return members.isEmpty() ? null : members.get(0).getId();
+	}
+
+	private List<FamilyChallengeParticipant> listParticipants(Long challengeId) {
+		return familyChallengeParticipantMapper.selectList(
+				new LambdaQueryWrapper<FamilyChallengeParticipant>()
+						.eq(FamilyChallengeParticipant::getChallengeId, challengeId)
+						.orderByAsc(FamilyChallengeParticipant::getId));
+	}
+```
+
+- [x] **Step 4: createChallenge 改 pending + participant + 发起结算**
+
+将 `createChallenge` 方法(358-422 行)整体替换为:
+
+```java
+	@Transactional
+	public Result<Long> createChallenge(Long familyId, Long userId, Long memberId, Map<String, Object> params) {
+		if (familyId == null || userId == null || memberId == null) {
+			return Result.error("familyId、userId和memberId不能为空");
+		}
+
+		// 验证title
+		String title = params.get("title") != null ? params.get("title").toString() : null;
+		if (title == null || title.trim().isEmpty()) {
+			return Result.error("title不能为空");
+		}
+		if (title.length() > 200) {
+			return Result.error("title不能超过200个字符");
+		}
+
+		// 验证targetMode
+		String targetMode = params.get("targetMode") != null ? params.get("targetMode").toString() : "all_members";
+		if (!"aggregate".equals(targetMode) && !"all_members".equals(targetMode)) {
+			return Result.error("targetMode必须是aggregate或all_members");
+		}
+
+		// 验证targetValue
+		Integer targetValue = params.get("targetValue") != null ?
+				Integer.parseInt(params.get("targetValue").toString()) : 0;
+		if (targetValue <= 0) {
+			return Result.error("targetValue必须大于0");
+		}
+
+		// 验证rewardPoints
+		Integer rewardPoints = params.get("rewardPoints") != null ?
+				Integer.parseInt(params.get("rewardPoints").toString()) : 0;
+		if (rewardPoints < 0) {
+			return Result.error("rewardPoints不能小于0");
+		}
+
+		// 验证durationDays
+		Integer durationDays = params.get("durationDays") != null ?
+				Integer.parseInt(params.get("durationDays").toString()) : 7;
+		if (durationDays <= 0) {
+			return Result.error("durationDays必须大于0");
+		}
+
+		// 验证description
+		String description = params.get("description") != null ? params.get("description").toString() : "";
+
+		// 参与成员校验:默认全员;必须包含发起人本人且均属本家庭
+		List<Long> participantIds = new ArrayList<>();
+		Object rawIds = params.get("participantMemberIds");
+		if (rawIds == null) {
+			// 未传 → 默认本家庭全部成员
+			List<FamilyMember> members = familyMemberMapper.selectList(
+					new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
+			for (FamilyMember m : members) {
+				participantIds.add(m.getId());
+			}
+		} else if (rawIds instanceof java.util.Collection) {
+			for (Object o : (java.util.Collection<?>) rawIds) {
+				if (o == null) continue;
+				participantIds.add(Long.valueOf(o.toString()));
+			}
+		}
+		if (participantIds.isEmpty() || !participantIds.contains(memberId)) {
+			return Result.error("参与成员必须包含发起人本人");
+		}
+		// 均属本家庭
+		for (Long pid : participantIds) {
+			LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
+					.eq(FamilyMember::getId, pid)
+					.eq(FamilyMember::getFamilyId, familyId);
+			if (familyMemberMapper.selectCount(mw) == 0) {
+				return Result.error("参与成员不属于本家庭");
+			}
+		}
+
+		// 创建挑战(pending 状态,参与成员同意后转 active)
+		FamilyChallenge challenge = new FamilyChallenge();
+		challenge.setFamilyId(familyId);
+		challenge.setCreatorId(userId);
+		challenge.setChallengeType(params.get("challengeType") != null ? params.get("challengeType").toString() : "custom");
+		challenge.setTitle(title);
+		challenge.setDescription(description);
+		challenge.setDurationDays(durationDays);
+		challenge.setRewardPoints(rewardPoints);
+		challenge.setTargetMode(targetMode);
+		challenge.setTargetValue(targetValue);
+		challenge.setStartDate(new Date());
+		challenge.setEndDate(new Date(System.currentTimeMillis() + durationDays * 24L * 60 * 60 * 1000));
+		challenge.setStatus("pending");
+		challenge.setCreatedAt(new Date());
+
+		familyChallengeMapper.insert(challenge);
+
+		// 写 participant 行:发起人 agreed,其余 pending
+		for (Long pid : participantIds) {
+			FamilyChallengeParticipant participant = new FamilyChallengeParticipant();
+			participant.setChallengeId(challenge.getId());
+			participant.setMemberId(pid);
+			if (pid.equals(memberId)) {
+				participant.setStatus("agreed");
+				participant.setAgreedAt(new Date());
+			} else {
+				participant.setStatus("pending");
+			}
+			familyChallengeParticipantMapper.insert(participant);
+		}
+
+		// 立即结算发起评分(§4.4)
+		challengeScoreService.applyInitiate(familyId, memberId, participantIds, challenge.getId());
+
+		return Result.success(challenge.getId());
+	}
+```
+
+- [x] **Step 5: getActiveChallenges 包含 pending + myStatus**
+
+将 `getActiveChallenges` 方法(39-58 行)替换为:
+
+```java
+	public List<FamilyChallenge> getActiveChallenges(Long familyId, Long currentMemberId) {
+		if (familyId == null) return new ArrayList<>();
+
+		LambdaQueryWrapper<FamilyChallenge> wrapper = new LambdaQueryWrapper<FamilyChallenge>()
+				.eq(FamilyChallenge::getFamilyId, familyId)
+				.in(FamilyChallenge::getStatus, "active", "pending")
+				.orderByDesc(FamilyChallenge::getCreatedAt);
+		List<FamilyChallenge> active = familyChallengeMapper.selectList(wrapper);
+
+		for (FamilyChallenge c : active) {
+			// 仅 active 且过期的挑战触发结算
+			if ("active".equals(c.getStatus())
+					&& c.getEndDate() != null && new Date().after(c.getEndDate())) {
+				checkAndSettle(c);
+			}
+		}
+
+		active = familyChallengeMapper.selectList(wrapper);
+
+		fillProgress(active);
+		setMyStatus(active, currentMemberId);
+		return active;
+	}
+
+	/** 为每个挑战设置当前成员参与状态(myStatus) */
+	private void setMyStatus(List<FamilyChallenge> challenges, Long currentMemberId) {
+		if (challenges == null || challenges.isEmpty() || currentMemberId == null) return;
+		for (FamilyChallenge c : challenges) {
+			FamilyChallengeParticipant p = familyChallengeParticipantMapper.selectOne(
+					new LambdaQueryWrapper<FamilyChallengeParticipant>()
+							.eq(FamilyChallengeParticipant::getChallengeId, c.getId())
+							.eq(FamilyChallengeParticipant::getMemberId, currentMemberId));
+			c.setMyStatus(p != null ? p.getStatus() : null);
+		}
+	}
+```
+
+- [x] **Step 6: fillProgress 基于 participant(含 status 字段)**
+
+将 `fillProgress` 方法(271-316 行)中"查全部家庭成员"部分替换为基于 participant:
+
+```java
+	private void fillProgress(List<FamilyChallenge> challenges) {
+		if (challenges == null || challenges.isEmpty()) return;
+
+		Map<Long, List<FamilyMember>> familyMembersCache = new HashMap<>();
+
+		for (FamilyChallenge c : challenges) {
+			normalizeTarget(c);
+
+			LambdaQueryWrapper<FamilyChallengeProgress> pw = new LambdaQueryWrapper<FamilyChallengeProgress>()
+					.eq(FamilyChallengeProgress::getChallengeId, c.getId());
+			List<FamilyChallengeProgress> progresses = familyChallengeProgressMapper.selectList(pw);
+
+			int total = 0;
+			Map<Long, Integer> progressMap = new HashMap<>();
+			for (FamilyChallengeProgress p : progresses) {
+				int v = p.getProgressValue() != null ? p.getProgressValue() : 0;
+				total += v;
+				progressMap.put(p.getChildId(), v);
+			}
+			c.setTotalProgress(total);
+
+			// 参与成员(participant 表,含 status),替代原"全部家庭成员"
+			List<FamilyChallengeParticipant> participants = listParticipants(c.getId());
+
+			List<Map<String, Object>> memberProgress = new ArrayList<>();
+			if (participants != null) {
+				for (FamilyChallengeParticipant part : participants) {
+					FamilyMember m = familyMemberMapper.selectById(part.getMemberId());
+					Integer v = progressMap.get(part.getMemberId());
+					int value = v != null ? v : 0;
+					Map<String, Object> mp = new HashMap<>();
+					mp.put("memberId", part.getMemberId());
+					mp.put("nickname", m != null && m.getNickname() != null ? m.getNickname() : "成员" + part.getMemberId());
+					mp.put("progressValue", value);
+					mp.put("completed", value >= c.getTargetValue());
+					mp.put("status", part.getStatus());
+					memberProgress.add(mp);
+				}
+			}
+			c.setMemberProgress(memberProgress);
+		}
+	}
+```
+
+**注意**:将原 `familyMembersCache` 逻辑一并移除(participant 已含全部需要展示的成员);`familyMembersCache` 变量随之删除。
+
+- [x] **Step 7: checkAndSettle 的 all_members 判定改为 agreed participants**
+
+在 `checkAndSettle`(182-254 行)中,将 all_members 分支的成员查询替换:
+
+```java
+		} else {
+			// 全员个人目标:每个 agreed 参与成员都达到 target_value
+			List<FamilyChallengeParticipant> participants = listParticipants(challenge.getId());
+			List<FamilyMember> members = new ArrayList<>();
+			for (FamilyChallengeParticipant p : participants) {
+				if (!"agreed".equals(p.getStatus())) continue;
+				FamilyMember m = familyMemberMapper.selectById(p.getMemberId());
+				if (m != null) members.add(m);
+			}
+			if (members.isEmpty()) {
+				isCompleted = false;
+			} else {
+				Map<Long, Integer> progressMap = new HashMap<>();
+				for (FamilyChallengeProgress p : progresses) {
+					progressMap.put(p.getChildId(),
+							p.getProgressValue() != null ? p.getProgressValue() : 0);
+				}
+				isCompleted = true;
+				for (FamilyMember m : members) {
+					Integer v = progressMap.get(m.getId());
+					if (v == null || v < targetValue) {
+						isCompleted = false;
+						break;
+					}
+				}
+			}
+		}
+```
+
+- [x] **Step 8: autoCreateChallenges 补写 participant 行**
+
+在 `autoCreateChallenges` 方法(86-100 行)中,`familyChallengeMapper.insert(challenge);` 之后追加:
+
+```java
+			// 系统创建:写 participant 行(全员 agreed,与存量迁移口径一致)
+			LambdaQueryWrapper<FamilyMember> mw = new LambdaQueryWrapper<FamilyMember>()
+					.eq(FamilyMember::getFamilyId, familyId);
+			List<FamilyMember> members = familyMemberMapper.selectList(mw);
+			for (FamilyMember m : members) {
+				FamilyChallengeParticipant participant = new FamilyChallengeParticipant();
+				participant.setChallengeId(challenge.getId());
+				participant.setMemberId(m.getId());
+				participant.setStatus("agreed");
+				participant.setAgreedAt(new Date());
+				familyChallengeParticipantMapper.insert(participant);
+			}
+```
+
+- [x] **Step 9: updateProgress 新增 participant+agreed 校验**
+
+在 `updateProgress`(105-173 行)的成员归属校验(第 3 步)之后插入:
+
+```java
+		// 3.5 参与成员校验:仅 agreed 参与成员可打卡(pending/rejected 拒绝)
+		FamilyChallengeParticipant participant = familyChallengeParticipantMapper.selectOne(
+				new LambdaQueryWrapper<FamilyChallengeParticipant>()
+						.eq(FamilyChallengeParticipant::getChallengeId, challengeId)
+						.eq(FamilyChallengeParticipant::getMemberId, childId));
+		if (participant == null || !"agreed".equals(participant.getStatus())) {
+			return Result.error("仅参与成员可打卡");
+		}
+```
+
+- [x] **Step 10: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS(现有测试类若引用了旧签名 getActiveChallenges/createChallenge,会在 Task 7 一并修正——但编译阶段会先失败,故此处同步把 Task 7 的 mock 调整提前或在 Task 7 修复。**如编译失败因测试类旧签名,立即在 Task 7 Step 1 修复测试类签名**。)
+
+- [x] **Step 11: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/FamilyChallengeService.java cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyChallenge.java
+git commit -m "feat(challenge): create改pending+respondChallenge+打卡结算口径收紧"
+```
+
+---
+
+### Task 6: FamilyChallengeController 改造(respond 端点 + 去门控 + myStatus)
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyChallengeController.java`
+
+**Interfaces:**
+- Consumes: `FamilyChallengeService.respondChallenge` / `getActiveChallenges(familyId, currentMemberId)` / `createChallenge(familyId, userId, memberId, params)`
+- Produces: `POST /api/health/challenge/respond`(body: `{challengeId, action}`)
+
+- [x] **Step 1: list 端点传 currentMemberId**
+
+替换 `getList`(19-25 行):
+
+```java
+	@PostMapping("/list")
+	public Result<List<FamilyChallenge>> getList(@RequestAttribute(value = "familyId", required = false) Long familyId,
+	                                             @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+		if (familyId == null) {
+			return Result.noFamily("请先创建或加入家庭");
+		}
+		return Result.success(familyChallengeService.getActiveChallenges(familyId, currentMemberId));
+	}
+```
+
+- [x] **Step 2: create 去家长门控 + 传 memberId**
+
+替换 `createChallenge`(57-66 行):
+
+```java
+	@PostMapping("/create")
+	public Result<Long> createChallenge(@RequestBody Map<String, Object> params,
+	                                    @RequestAttribute(value = "familyId", required = false) Long familyId,
+	                                    @RequestAttribute(value = "userId", required = false) Long userId,
+	                                    @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+		if (currentMemberId == null) {
+			return Result.error("未识别当前成员");
+		}
+		return familyChallengeService.createChallenge(familyId, userId, currentMemberId, params);
+	}
+```
+
+- [x] **Step 3: 新增 respond 端点**
+
+在 `updateProgress` 端点之后插入:
+
+```java
+	@PostMapping("/respond")
+	public Result<Void> respondChallenge(@RequestBody Map<String, Object> params,
+	                                     @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
+		if (currentMemberId == null) {
+			return Result.error("未识别当前成员");
+		}
+		Long challengeId = params.get("challengeId") != null
+				? Long.valueOf(params.get("challengeId").toString()) : null;
+		String action = params.get("action") != null ? params.get("action").toString() : null;
+		return familyChallengeService.respondChallenge(challengeId, currentMemberId, action);
+	}
+```
+
+- [x] **Step 4: 检查路由重复**
+
+Run:
+```bash
+cd cfc-backend && grep -rn '@Mapping' src/main/java/com/etotem/cfc/controller/ | grep -oP '@\w+Mapping\("\K[^"]*' | sort -u | Select-String -Pattern "challenge"
+```
+Expected: 仅 `challenge/list`、`challenge/history`、`challenge/progress`、`challenge/templates`、`challenge/create`、`challenge/update/{id}`、`challenge/delete/{id}`、`challenge/respond`,无重复。
+
+- [x] **Step 5: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [x] **Step 6: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/FamilyChallengeController.java
+git commit -m "feat(challenge): respond端点+创建去门控+list返回myStatus"
+```
+
+---
+
+### Task 7: 后端单元测试更新与新增
+
+**Files:**
+- Modify: `cfc-backend/src/test/java/com/etotem/cfc/service/FamilyChallengeServiceTest.java`
+- Create: `cfc-backend/src/test/java/com/etotem/cfc/service/ChallengeScoreServiceTest.java`
+
+**Interfaces:**
+- Consumes: `FamilyChallengeService`(新签名 getActiveChallenges/createChallenge、respondChallenge)、`ChallengeScoreService`(applyInitiate/applyAgree/applyReject)
+
+- [x] **Step 1: 修复 FamilyChallengeServiceTest 旧签名 + 新增 mock**
+
+将测试类(26-33 行)mock 区替换为(新增 `ChallengeScoreService` 与 `FamilyChallengeParticipantMapper` mock):
+
+```java
+	public class FamilyChallengeServiceTest {
+
+	    @Mock private FamilyChallengeMapper familyChallengeMapper;
+	    @Mock private FamilyChallengeProgressMapper familyChallengeProgressMapper;
+	    @Mock private FamilyChallengeParticipantMapper familyChallengeParticipantMapper;
+	    @Mock private FamilyMemberMapper familyMemberMapper;
+	    @Mock private HealthScoreService healthScoreService;
+	    @Mock private PointsService pointsService;
+	    @Mock private ChallengeScoreService challengeScoreService;
+	    @InjectMocks private FamilyChallengeService service;
+```
+
+并将 `getActiveChallenges_shouldSettleExpiredChallenges`(115-132 行)调用改为新签名:`service.getActiveChallenges(10L, null);`,同时补 `when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(null);`(getActiveChallenges 中 setMyStatus 会查询 participant;currentMemberId=null 时 setMyStatus 直接 return,无需 mock,但 fillProgress 会调 listParticipants → `selectList`)。
+
+**注意**:`fillProgress` 现在调用 `familyChallengeParticipantMapper.selectList(any())`——需要在每个相关测试补 `when(familyChallengeParticipantMapper.selectList(any())).thenReturn(java.util.Collections.emptyList());`,否则 NPE。在 `setUp()` 中统一补充:
+
+```java
+	    @BeforeEach
+	    void setUp() {
+	        MockitoAnnotations.openMocks(this);
+	        when(familyChallengeParticipantMapper.selectList(any())).thenReturn(java.util.Collections.emptyList());
+	    }
+```
+
+- [x] **Step 2: 新增 respondChallenge 测试**
+
+在测试类末尾追加:
+
+```java
+	    @Test
+	    void respondChallenge_agree_shouldTransitionToActiveWhenAllAgreed() {
+	        FamilyChallenge challenge = new FamilyChallenge();
+	        challenge.setId(1L);
+	        challenge.setFamilyId(10L);
+	        challenge.setStatus("pending");
+	        challenge.setTargetMode("aggregate");
+	        challenge.setTargetValue(300);
+	        challenge.setCreatorId(9L);
+
+	        FamilyChallengeParticipant p = new FamilyChallengeParticipant();
+	        p.setId(1L);
+	        p.setChallengeId(1L);
+	        p.setMemberId(100L);
+	        p.setStatus("pending");
+
+	        FamilyChallengeParticipant initiator = new FamilyChallengeParticipant();
+	        initiator.setId(2L);
+	        initiator.setChallengeId(1L);
+	        initiator.setMemberId(200L);
+	        initiator.setStatus("agreed");
+
+	        FamilyMember member = new FamilyMember();
+	        member.setId(200L);
+	        member.setUserId(9L);
+
+	        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
+	        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(p);
+	        when(familyChallengeParticipantMapper.selectList(any()))
+	                .thenReturn(java.util.Arrays.asList(initiator, p));
+
+	        Result<Void> result = service.respondChallenge(1L, 100L, "agree");
+
+	        assertEquals(Integer.valueOf(200), result.getCode());
+	        verify(challengeScoreService).applyAgree(eq(10L), eq(100L), anyList(), eq(1L));
+	        verify(familyChallengeParticipantMapper).updateById(argThat(pp -> "agreed".equals(pp.getStatus())));
+	        verify(familyChallengeMapper).updateById(argThat(c -> "active".equals(c.getStatus())));
+	    }
+
+	    @Test
+	    void respondChallenge_reject_shouldCancelWhenOnlyInitiatorLeft() {
+	        FamilyChallenge challenge = new FamilyChallenge();
+	        challenge.setId(1L);
+	        challenge.setFamilyId(10L);
+	        challenge.setStatus("pending");
+	        challenge.setTargetMode("aggregate");
+	        challenge.setTargetValue(300);
+	        challenge.setCreatorId(9L);
+
+	        FamilyChallengeParticipant p = new FamilyChallengeParticipant();
+	        p.setId(1L);
+	        p.setChallengeId(1L);
+	        p.setMemberId(100L);
+	        p.setStatus("pending");
+
+	        FamilyChallengeParticipant initiator = new FamilyChallengeParticipant();
+	        initiator.setId(2L);
+	        initiator.setChallengeId(1L);
+	        initiator.setMemberId(200L);
+	        initiator.setStatus("agreed");
+
+	        FamilyMember member = new FamilyMember();
+	        member.setId(200L);
+	        member.setUserId(9L);
+
+	        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
+	        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(p);
+	        when(familyChallengeParticipantMapper.selectList(any()))
+	                .thenReturn(java.util.Arrays.asList(initiator, p));
+	        when(familyMemberMapper.selectList(any())).thenReturn(java.util.Arrays.asList(member));
+
+	        Result<Void> result = service.respondChallenge(1L, 100L, "reject");
+
+	        assertEquals(Integer.valueOf(200), result.getCode());
+	        verify(challengeScoreService).applyReject(eq(10L), eq(100L), eq(200L), anyList(), eq(1L));
+	        verify(familyChallengeMapper).updateById(argThat(c -> "cancelled".equals(c.getStatus())));
+	    }
+
+	    @Test
+	    void respondChallenge_shouldRejectWhenAlreadyProcessed() {
+	        FamilyChallenge challenge = new FamilyChallenge();
+	        challenge.setId(1L);
+	        challenge.setFamilyId(10L);
+	        challenge.setStatus("pending");
+	        challenge.setTargetMode("aggregate");
+	        challenge.setTargetValue(300);
+
+	        FamilyChallengeParticipant p = new FamilyChallengeParticipant();
+	        p.setId(1L);
+	        p.setChallengeId(1L);
+	        p.setMemberId(100L);
+	        p.setStatus("agreed"); // 已处理过
+
+	        when(familyChallengeMapper.selectById(1L)).thenReturn(challenge);
+	        when(familyChallengeParticipantMapper.selectOne(any())).thenReturn(p);
+
+	        Result<Void> result = service.respondChallenge(1L, 100L, "agree");
+
+	        assertEquals(Integer.valueOf(500), result.getCode());
+	        verify(challengeScoreService, never()).applyAgree(any(), any(), any(), any());
+	    }
+```
+
+补充静态导入:`import static org.mockito.ArgumentMatchers.anyList;` 与 `import com.etotem.cfc.entity.FamilyChallengeParticipant;`、`import com.etotem.cfc.entity.FamilyMember;`、`import com.etotem.cfc.mapper.FamilyChallengeParticipantMapper;`。
+
+- [x] **Step 3: 新建 ChallengeScoreServiceTest**
+
+创建 `cfc-backend/src/test/java/com/etotem/cfc/service/ChallengeScoreServiceTest.java`:
+
+```java
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.FamilyChallengeScoreConfig;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.FamilyRelationshipScore;
+import com.etotem.cfc.mapper.FamilyChallengeScoreConfigMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.FamilyRelationshipScoreMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.math.BigDecimal;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+public class ChallengeScoreServiceTest {
+
+    @Mock private FamilyRelationshipScoreMapper relationshipScoreMapper;
+    @Mock private FamilyChallengeScoreConfigMapper scoreConfigMapper;
+    @Mock private FamilyMemberMapper familyMemberMapper;
+    @Mock private EnergyService energyService;
+    @InjectMocks private ChallengeScoreService service;
+
+    @BeforeEach
+    void setUp() {
+        MockitoAnnotations.openMocks(this);
+        // 无配置行 → 用内置默认
+        when(scoreConfigMapper.selectOne(any())).thenReturn(null);
+    }
+
+    private FamilyRelationshipScore freshScore() {
+        FamilyRelationshipScore s = new FamilyRelationshipScore();
+        s.setId(1L);
+        s.setFamilyId(10L);
+        s.setFromMemberId(100L);
+        s.setToMemberId(200L);
+        s.setTrustScore(new BigDecimal("60.00"));
+        s.setIntimacyScore(new BigDecimal("60.00"));
+        s.setCommunicationScore(new BigDecimal("60.00"));
+        return s;
+    }
+
+    @Test
+    void applyAgree_shouldAddTrustIntimacyCommToOneDirectionOnly() {
+        FamilyRelationshipScore score = freshScore();
+        when(relationshipScoreMapper.selectOne(any())).thenReturn(score);
+
+        service.applyAgree(10L, 100L, Arrays.asList(100L, 200L), 1L);
+
+        assertEquals(new BigDecimal("61.00"), score.getTrustScore());
+        assertEquals(new BigDecimal("61.00"), score.getIntimacyScore());
+        assertEquals(new BigDecimal("61.00"), score.getCommunicationScore());
+        verify(relationshipScoreMapper).updateById(score);
+        verify(energyService).awardEnergyByCode(eq(100L), eq("action"), eq(1), anyString());
+    }
+
+    @Test
+    void applyReject_shouldFollowConfirmedMatrix() {
+        // I→X: trust-1/intimacy-1/comm-1
+        FamilyRelationshipScore iToX = freshScore();
+        iToX.setFromMemberId(100L); // I=100
+        iToX.setToMemberId(200L);   // X=200
+        // X→I: 仅 comm+1
+        FamilyRelationshipScore xToI = freshScore();
+        xToI.setFromMemberId(200L);
+        xToI.setToMemberId(100L);
+        // A→X: 仅 comm-1
+        FamilyRelationshipScore aToX = freshScore();
+        aToX.setFromMemberId(300L);
+        aToX.setToMemberId(200L);
+
+        when(relationshipScoreMapper.selectOne(any())).thenReturn(iToX, xToI, aToX);
+
+        service.applyReject(10L, 200L, 100L, Arrays.asList(300L), 1L);
+
+        // I→X
+        assertEquals(new BigDecimal("59.00"), iToX.getTrustScore());
+        assertEquals(new BigDecimal("59.00"), iToX.getIntimacyScore());
+        assertEquals(new BigDecimal("59.00"), iToX.getCommunicationScore());
+        // X→I
+        assertEquals(new BigDecimal("60.00"), xToI.getTrustScore());
+        assertEquals(new BigDecimal("60.00"), xToI.getIntimacyScore());
+        assertEquals(new BigDecimal("61.00"), xToI.getCommunicationScore());
+        // A→X(C3:只扣沟通)
+        assertEquals(new BigDecimal("60.00"), aToX.getTrustScore());
+        assertEquals(new BigDecimal("60.00"), aToX.getIntimacyScore());
+        assertEquals(new BigDecimal("59.00"), aToX.getCommunicationScore());
+
+        // 能量:I(100) 与 A(300) 各 -1
+        verify(energyService).deductEnergyByCode(eq(100L), eq("action"), eq(1), anyString());
+        verify(energyService).deductEnergyByCode(eq(300L), eq("action"), eq(1), anyString());
+    }
+
+    @Test
+    void applyInitiate_shouldNotAddTrust() {
+        FamilyRelationshipScore score = freshScore();
+        when(relationshipScoreMapper.selectOne(any())).thenReturn(score);
+
+        service.applyInitiate(10L, 100L, Arrays.asList(100L, 200L), 1L);
+
+        assertEquals(new BigDecimal("60.00"), score.getTrustScore()); // 不加信任
+        assertEquals(new BigDecimal("61.00"), score.getIntimacyScore());
+        assertEquals(new BigDecimal("61.00"), score.getCommunicationScore());
+    }
+
+    @Test
+    void getOrCreateScore_shouldUseMemberBaselineWhenNoRow() {
+        FamilyMember member = new FamilyMember();
+        member.setId(100L);
+        member.setTrustScore(new BigDecimal("70.00"));
+        member.setIntimacyScore(new BigDecimal("65.00"));
+        member.setCommunicationScore(new BigDecimal("55.00"));
+
+        when(relationshipScoreMapper.selectOne(any())).thenReturn(null, null); // 第一次查无、撞键重查仍无 → insert
+        when(relationshipScoreMapper.selectOne(any())).thenReturn(null);
+        when(familyMemberMapper.selectById(100L)).thenReturn(member);
+
+        service.applyAgree(10L, 100L, Arrays.asList(100L, 200L), 1L);
+
+        verify(relationshipScoreMapper).insert(any());
+    }
+}
+```
+
+补充静态导入:`import static org.mockito.ArgumentMatchers.anyString;`、`import static org.mockito.ArgumentMatchers.eq;`、`import static org.mockito.Mockito.verify;`。
+
+- [x] **Step 4: 运行测试**
+
+Run: `cd cfc-backend && mvn test -Dtest=FamilyChallengeServiceTest,ChallengeScoreServiceTest`
+Expected: 全部 PASS(若 `getOrCreateScore_shouldUseMemberBaselineWhenNoRow` 中 selectOne 连续两次 mock 相同值导致 insert 后重查仍 null 的兜底逻辑与 mock 不匹配,可简化为只断言 insert 被调用,不强求重查分支)
+
+- [x] **Step 5: 全量编译**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+- [x] **Step 6: Commit**
+
+```bash
+git add cfc-backend/src/test/java/com/etotem/cfc/service/FamilyChallengeServiceTest.java cfc-backend/src/test/java/com/etotem/cfc/service/ChallengeScoreServiceTest.java
+git commit -m "test(challenge): 参与机制单元测试(respond/评分矩阵)"
+```
+
+---
+
+### Task 8: cfc-web 评分配置管理页
+
+**Files:**
+- Create: `cfc-web/src/api/challengeScore.js`
+- Create: `cfc-web/src/views/admin/ChallengeScoreConfig.vue`
+- Modify: `cfc-web/src/router/index.js`
+
+**Interfaces:**
+- Consumes: 后端 `POST /api/admin/challenge-score-config/list` / `POST /api/admin/challenge-score-config/update`
+- Produces: 配置页路由 `/admin/challenge-score-config`(name: ChallengeScoreConfig)
+
+- [x] **Step 1: 创建 API 封装**
+
+创建 `cfc-web/src/api/challengeScore.js`(参照 `cfc-web/src/api/energy.js` 的 request 封装风格):
+
+```javascript
+import request from '@/utils/request'
+
+export function getChallengeScoreConfigList() {
+  return request({ url: '/api/admin/challenge-score-config/list', method: 'post' })
+}
+
+export function updateChallengeScoreConfig(data) {
+  return request({ url: '/api/admin/challenge-score-config/update', method: 'post', data })
+}
+```
+
+- [x] **Step 2: 创建管理页 ChallengeScoreConfig.vue**
+
+创建 `cfc-web/src/views/admin/ChallengeScoreConfig.vue`(参照 `EnergyBehaviorConfig.vue` 结构,简化为 3×4 矩阵编辑):
+
+```vue
+<template>
+  <div class="challenge-score-config admin-page">
+    <div class="header">
+      <h2>挑战评分配置</h2>
+      <div class="header-tip">家庭挑战 发起/同意/拒绝 的方向性关系评分与能量增减(可正可负)</div>
+    </div>
+
+    <el-table :data="matrix" v-loading="loading" border stripe>
+      <el-table-column label="动作" width="120">
+        <template slot-scope="{ row }">
+          <el-tag size="mini" :type="row.action === 'reject' ? 'danger' : 'success'">{{ actionLabel(row.action) }}</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="能量(行维度)" width="140">
+        <template slot-scope="{ row }">
+          <el-input-number v-model="row.energy" :min="-10" :max="10" size="mini" />
+        </template>
+      </el-table-column>
+      <el-table-column label="信任(方向)" width="140">
+        <template slot-scope="{ row }">
+          <el-input-number v-model="row.trust" :min="-10" :max="10" size="mini" />
+        </template>
+      </el-table-column>
+      <el-table-column label="亲近(方向)" width="140">
+        <template slot-scope="{ row }">
+          <el-input-number v-model="row.intimacy" :min="-10" :max="10" size="mini" />
+        </template>
+      </el-table-column>
+      <el-table-column label="沟通(方向)" width="140">
+        <template slot-scope="{ row }">
+          <el-input-number v-model="row.communication" :min="-10" :max="10" size="mini" />
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" width="120" fixed="right">
+        <template slot-scope="{ row }">
+          <el-button size="mini" type="primary" @click="saveRow(row)">保存</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="footer-tip">注:reject 的沟通方向由代码固定(拒绝人→发起人 +、发起人→拒绝人 -、已同意成员→拒绝人 -),此处的值为增减幅度。信任/亲近为单向扣减(发起人→拒绝人)。</div>
+  </div>
+</template>
+
+<script>
+import { getChallengeScoreConfigList, updateChallengeScoreConfig } from '@/api/challengeScore'
+
+export default {
+  name: 'ChallengeScoreConfig',
+  data() {
+    return {
+      loading: false,
+      matrix: []
+    }
+  },
+  created() {
+    this.loadList()
+  },
+  methods: {
+    actionLabel: function(action) {
+      var map = { initiate: '发起', agree: '同意', reject: '拒绝' }
+      return map[action] || action
+    },
+    loadList: function() {
+      var self = this
+      self.loading = true
+      getChallengeScoreConfigList().then(function(res) {
+        var list = (res && res.data) || []
+        var matrix = []
+        var actions = ['initiate', 'agree', 'reject']
+        for (var i = 0; i < actions.length; i++) {
+          var row = { action: actions[i], energy: 0, trust: 0, intimacy: 0, communication: 0 }
+          for (var j = 0; j < list.length; j++) {
+            var item = list[j]
+            if (item.action !== row.action) continue
+            if (item.scoreType === 'energy') row.energy = item.value
+            if (item.scoreType === 'trust') row.trust = item.value
+            if (item.scoreType === 'intimacy') row.intimacy = item.value
+            if (item.scoreType === 'communication') row.communication = item.value
+          }
+          matrix.push(row)
+        }
+        self.matrix = matrix
+      }).catch(function() {
+        self.matrix = []
+      }).finally(function() {
+        self.loading = false
+      })
+    },
+    saveRow: function(row) {
+      var self = this
+      var items = [
+        { action: row.action, scoreType: 'energy', value: row.energy },
+        { action: row.action, scoreType: 'trust', value: row.trust },
+        { action: row.action, scoreType: 'intimacy', value: row.intimacy },
+        { action: row.action, scoreType: 'communication', value: row.communication }
+      ]
+      var promises = items.map(function(item) {
+        return updateChallengeScoreConfig(item)
+      })
+      Promise.all(promises).then(function() {
+        self.$message.success(row.action + ' 配置已保存')
+      }).catch(function() {
+        self.$message.error('保存失败')
+      })
+    }
+  }
+}
+</script>
+
+<style scoped>
+.header-tip { color: #909399; font-size: 13px; margin-top: 4px; }
+.footer-tip { margin-top: 12px; color: #909399; font-size: 12px; line-height: 1.6; }
+</style>
+```
+
+- [x] **Step 3: 注册路由**
+
+在 `cfc-web/src/router/index.js` 中 `energy-behavior-config` 路由(309-314 行)之后插入:
+
+```javascript
+    {
+      path: 'challenge-score-config',
+      name: 'ChallengeScoreConfig',
+      component: () => import('@/views/admin/ChallengeScoreConfig.vue'),
+      meta: { title: '挑战评分配置', perm: 'energy' }
+    },
+```
+
+- [x] **Step 4: 创建后端管理控制器 AdminChallengeScoreConfigController**
+
+创建 `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminChallengeScoreConfigController.java`(参照 `AdminEnergyBehaviorConfigController` 风格):
+
+```java
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FamilyChallengeScoreConfig;
+import com.etotem.cfc.mapper.FamilyChallengeScoreConfigMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Tag(name = "管理端-挑战评分配置")
+@RestController
+@RequestMapping("/api/admin/challenge-score-config")
+public class AdminChallengeScoreConfigController {
+
+    @Resource
+    private FamilyChallengeScoreConfigMapper configMapper;
+
+    @Operation(summary = "获取全部挑战评分配置")
+    @PostMapping("/list")
+    public Result<List<FamilyChallengeScoreConfig>> list() {
+        List<FamilyChallengeScoreConfig> list = configMapper.selectList(
+                new LambdaQueryWrapper<FamilyChallengeScoreConfig>()
+                        .orderByAsc(FamilyChallengeScoreConfig::getAction)
+                        .orderByAsc(FamilyChallengeScoreConfig::getScoreType));
+        return Result.success(list);
+    }
+
+    @Operation(summary = "更新挑战评分配置(按 action+scoreType)")
+    @PostMapping("/update")
+    public Result<String> update(@RequestBody FamilyChallengeScoreConfig config) {
+        if (config.getAction() == null || config.getScoreType() == null) {
+            return Result.error("action和scoreType不能为空");
+        }
+        FamilyChallengeScoreConfig existing = configMapper.selectOne(
+                new LambdaQueryWrapper<FamilyChallengeScoreConfig>()
+                        .eq(FamilyChallengeScoreConfig::getAction, config.getAction())
+                        .eq(FamilyChallengeScoreConfig::getScoreType, config.getScoreType()));
+        if (existing == null) {
+            // 不存在则按默认创建
+            config.setEnabled(config.getEnabled() != null ? config.getEnabled() : 1);
+            config.setUpdatedAt(new Date());
+            configMapper.insert(config);
+            return Result.success("已创建配置");
+        }
+        if (config.getValue() != null) existing.setValue(config.getValue());
+        if (config.getEnabled() != null) existing.setEnabled(config.getEnabled());
+        existing.setUpdatedAt(new Date());
+        configMapper.updateById(existing);
+        return Result.success("更新成功");
+    }
+}
+```
+
+- [x] **Step 5: 编译验证(后端)+ 前端结构校验**
+
+Run: `cd cfc-backend && mvn clean compile`
+Expected: BUILD SUCCESS
+
+Run(前端 script 块语法校验,从 vue 文件提取 script 后 node --check):
+```bash
+cd cfc-web && node -e "const fs=require('fs');const s=fs.readFileSync('src/views/admin/ChallengeScoreConfig.vue','utf8');const m=s.match(/<script>([\s\S]*?)<\/script>/);require('child_process').execSync('node --check',{input:m[1]})"
+```
+Expected: 无输出(语法通过)
+
+- [x] **Step 6: Commit**
+
+```bash
+git add cfc-web/src/api/challengeScore.js cfc-web/src/views/admin/ChallengeScoreConfig.vue cfc-web/src/router/index.js cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminChallengeScoreConfigController.java
+git commit -m "feat(admin): 挑战评分配置管理页+后端配置CRUD"
+```
+
+---
+
+### Task 9: 小程序前端(api + 卡片同意/拒绝 + 管理页成员多选)
+
+**Files:**
+- Modify: `cfc-frontend/utils/api.js`
+- Modify: `cfc-frontend/components/FamilyChallengeCard.vue`
+- Modify: `cfc-frontend/pages/health/challenge-manage.vue`
+- Modify: `cfc-frontend/pages/index-home/index.vue`
+
+**小程序硬约束:** 禁止 `?.`、禁止 `:key` 表达式(用方法调用)、禁止 `new Date(string)`(用 `parseDate`)、Vue 2 Options API、prop 数组兜底(`|| []`)。
+
+- [x] **Step 1: api.js 新增 respondChallenge**
+
+在 `cfc-frontend/utils/api.js` 中 `updateChallengeProgress`(1940 行)之后追加:
+
+```javascript
+export const respondChallenge = (data) => request('/api/health/challenge/respond', 'POST', data)
+```
+
+- [x] **Step 2: FamilyChallengeCard.vue 支持 pending + 同意/拒绝 + rejected 徽标**
+
+模板(1-51 行)修改:
+
+```html
+    <view class="card-header">
+      <text class="card-title">{{ challenge.title }}</text>
+      <text class="card-status" :class="statusClass">
+        {{ statusText }}
+      </text>
+    </view>
+    <text class="card-desc">{{ challenge.description || '' }}</text>
+
+    <!-- 待同意:显示确认进度(pending 状态挑战) -->
+    <view class="pending-section" v-if="challenge.status === 'pending'">
+      <text class="pending-hint">等待成员确认(已同意 {{ agreedCount }}/{{ totalCount }})</text>
+    </view>
+
+    <!-- 成员状态徽标:rejected 成员显示"已拒绝" -->
+    <view class="member-status-row" v-if="challenge.memberProgress && challenge.memberProgress.length > 0">
+      <text class="member-status-tag member-rejected" v-for="mp in challenge.memberProgress" :key="getMemberKey(mp)" v-if="mp.status === 'rejected'">{{ mp.nickname }}已拒绝</text>
+    </view>
+
+    <!-- 目标展示(两种模式) -->
+    <view class="target-section" v-if="challenge.targetMode">
+      ...
+```
+
+**说明**:目标展示区(target-section)保持原样;`v-if` 与 `v-for` 同元素在小程序下不推荐——改为外层包一层:
+
+```html
+    <view class="member-status-row" v-if="hasRejected">
+      <text class="member-status-tag member-rejected" v-for="mp in rejectedMembers" :key="getMemberKey(mp)">{{ mp.nickname }} 已拒绝</text>
+    </view>
+```
+
+卡片动作区(47-49 行)替换为三态:
+
+```html
+    <!-- 待同意:同意/拒绝按钮(仅我的状态=pending 时显示) -->
+    <view class="card-action" v-if="challenge.status === 'pending' && challenge.myStatus === 'pending'">
+      <view class="respond-btn respond-agree" @click="onRespond('agree')">✅ 同意</view>
+      <view class="respond-btn respond-reject" @click="onRespond('reject')">✖ 拒绝</view>
+    </view>
+
+    <!-- 进行中:打卡 -->
+    <view class="card-action" v-if="showCheckin && challenge.status === 'active'">
+      <view class="checkin-btn" @click="onCheckin">✅ 打卡</view>
+    </view>
+```
+
+script 部分(54-104 行)修改:新增 computed `statusText`、`agreedCount`、`totalCount`、`hasRejected`、`rejectedMembers`;methods 新增 `getMemberKey`、`onRespond`:
+
+```javascript
+  computed: {
+    statusClass: function() {
+      return 'status-' + (this.challenge && this.challenge.status ? this.challenge.status : 'active')
+    },
+    statusText: function() {
+      if (!this.challenge) return ''
+      if (this.challenge.status === 'active') return '进行中'
+      if (this.challenge.status === 'pending') return '待同意'
+      if (this.challenge.status === 'cancelled') return '已取消'
+      return '已完成'
+    },
+    agreedCount: function() {
+      var list = this.challenge && this.challenge.memberProgress ? this.challenge.memberProgress : []
+      var count = 0
+      for (var i = 0; i < list.length; i++) {
+        if (list[i].status === 'agreed') count++
+      }
+      return count
+    },
+    totalCount: function() {
+      var list = this.challenge && this.challenge.memberProgress ? this.challenge.memberProgress : []
+      return list.length
+    },
+    hasRejected: function() {
+      return this.rejectedMembers.length > 0
+    },
+    rejectedMembers: function() {
+      var list = this.challenge && this.challenge.memberProgress ? this.challenge.memberProgress : []
+      var result = []
+      for (var i = 0; i < list.length; i++) {
+        if (list[i].status === 'rejected') result.push(list[i])
+      }
+      return result
+    },
+    targetModeLabel: function() {
+      if (!this.challenge) return ''
+      if (this.challenge.targetMode === 'aggregate') return '🎯 全家目标'
+      return '🎯 每人目标'
+    },
+    formatTargetValue: function() {
+      if (!this.challenge) return ''
+      return this.challenge.targetValue
+    },
+    totalPercent: function() {
+      var total = this.challenge && this.challenge.totalProgress ? this.challenge.totalProgress : 0
+      var target = this.challenge && this.challenge.targetValue ? this.challenge.targetValue : 1
+      if (target <= 0) return 0
+      var p = Math.round(total / target * 100)
+      return Math.min(p, 100)
+    }
+  },
+  methods: {
+    formatDate: function(dateStr) {
+      if (!dateStr) return ''
+      return String(dateStr).slice(0, 10)
+    },
+    memberPercent: function(value) {
+      var target = this.challenge && this.challenge.targetValue ? this.challenge.targetValue : 1
+      if (target <= 0) return 0
+      var p = Math.round((value || 0) / target * 100)
+      return Math.min(p, 100)
+    },
+    getMemberKey: function(mp) {
+      return mp.memberId
+    },
+    onRespond: function(action) {
+      var self = this
+      var tip = action === 'agree' ? '确认同意参与该挑战吗?' : '确认拒绝该挑战吗?'
+      uni.showModal({
+        title: '确认操作',
+        content: tip,
+        success: function(res) {
+          if (res.confirm) {
+            self.$emit('respond', { challengeId: self.challenge.id, action: action })
+          }
+        }
+      })
+    },
+    onCheckin: function() {
+      var self = this
+      uni.showModal({
+        title: '确认打卡',
+        content: '确认完成今日打卡吗?',
+        success: function(res) {
+          if (res.confirm) {
+            self.$emit('checkin', { challengeId: self.challenge.id })
+          }
+        }
+      })
+    }
+  }
+```
+
+style 追加:
+
+```css
+.pending-section { background: #FEF3C7; border-radius: 10rpx; padding: 10rpx 12rpx; margin-bottom: 6rpx; }
+.pending-hint { font-size: 22rpx; color: #B45309; font-weight: 600; }
+.member-status-row { display: flex; flex-wrap: wrap; gap: 8rpx; margin-bottom: 6rpx; }
+.member-status-tag { font-size: 18rpx; padding: 2rpx 10rpx; border-radius: 999rpx; }
+.member-rejected { background: #FEE2E2; color: #DC2626; }
+.respond-btn { padding: 10rpx 24rpx; border-radius: 999rpx; font-size: 22rpx; font-weight: 600; margin-left: 12rpx; }
+.respond-agree { background: #10B981; color: #FFFFFF; }
+.respond-reject { background: #FFFFFF; color: #DC2626; border: 2rpx solid #FCA5A5; }
+```
+
+- [x] **Step 3: challenge-manage.vue 移除角色门控 + 成员多选**
+
+onLoad(172-182 行)替换为(移除 parent 门控):
+
+```javascript
+  onLoad() {
+    this.loadFamilyId()
+    this.loadData()
+    this.loadTemplates()
+    this.loadMembers()
+  },
+```
+
+data 新增 `familyMembers: []` 与 `selectedMemberIds: []`(144-163 行 form 附近)。
+
+methods 新增:
+
+```javascript
+    loadMembers: function() {
+      var self = this
+      listFamilyMembers().then(function(res) {
+        var list = (res && res.data) || []
+        self.familyMembers = Array.isArray(list) ? list : []
+        // 默认全选
+        self.selectedMemberIds = []
+        for (var i = 0; i < self.familyMembers.length; i++) {
+          self.selectedMemberIds.push(self.familyMembers[i].id)
+        }
+      }).catch(function() {
+        self.familyMembers = []
+        self.selectedMemberIds = []
+      })
+    },
+```
+
+import 补充(139 行):加入 `listFamilyMembers`。
+
+创建弹窗表单(`openCreateModal`/`createFromTemplate`)后,弹窗 modal-body 中追加参与成员多选(放在"奖励积分"之后、modal-footer 之前):
+
+```html
+          <view class="form-item">
+            <text class="form-label">参与成员</text>
+            <view class="member-picker">
+              <text class="member-pick-item" :class="{ active: isMemberSelected(m.id) }"
+                    v-for="m in familyMembers" :key="m.id"
+                    @click="toggleMember(m.id)">{{ m.nickname }}</text>
+            </view>
+          </view>
+```
+
+methods 新增:
+
+```javascript
+    isMemberSelected: function(memberId) {
+      return this.selectedMemberIds.indexOf(memberId) >= 0
+    },
+    toggleMember: function(memberId) {
+      var idx = this.selectedMemberIds.indexOf(memberId)
+      if (idx >= 0) {
+        this.selectedMemberIds.splice(idx, 1)
+      } else {
+        this.selectedMemberIds.push(memberId)
+      }
+    },
+```
+
+`submitForm`(285-328 行)的 data 构造加入 participantMemberIds(仅在创建模式传;编辑模式不传):
+
+```javascript
+      if (!self.editMode) {
+        data.participantMemberIds = self.selectedMemberIds
+      }
+```
+
+style 追加:
+
+```css
+.member-picker { display: flex; flex-wrap: wrap; gap: 12rpx; }
+.member-pick-item { padding: 8rpx 20rpx; border-radius: 999rpx; background: #F1F5F9; color: #475569; font-size: 22rpx; border: 2rpx solid transparent; }
+.member-pick-item.active { background: #DCFCE7; color: #16A34A; border-color: #10B981; }
+```
+
+- [x] **Step 4: index-home/index.vue 处理 respond 事件**
+
+模板(108-113 行)FamilyChallengeCard 追加 `@respond="onChallengeRespond"`:
+
+```html
+      <FamilyChallengeCard
+        v-for="ch in activeChallenges"
+        :key="ch.id"
+        :challenge="ch"
+        :showCheckin="showCheckinBtn"
+        @checkin="onChallengeCheckin"
+        @respond="onChallengeRespond" />
+```
+
+methods 新增(`onChallengeCheckin` 之后):
+
+```javascript
+    onChallengeRespond: function(payload) {
+      var self = this
+      respondChallenge({ challengeId: payload.challengeId, action: payload.action }).then(function(res) {
+        if (res && res.code === 200) {
+          uni.showToast({ title: payload.action === 'agree' ? '已同意' : '已拒绝', icon: 'success' })
+          self.loadChallenges()
+        } else {
+          uni.showToast({ title: (res && res.message) || '操作失败', icon: 'none' })
+          self.loadChallenges()
+        }
+      }).catch(function() {
+        uni.showToast({ title: '操作失败,请重试', icon: 'none' })
+      })
+    },
+```
+
+import 补充:`respondChallenge`。
+
+- [x] **Step 5: 结构校验**
+
+对修改的 4 个前端文件提取 script 块做语法校验(node --check),并全局 grep 确认无 `?.` 出现在新代码中:
+
+Run:
+```bash
+cd cfc-frontend && node -e "const fs=require('fs');['components/FamilyChallengeCard.vue','pages/health/challenge-manage.vue','pages/index-home/index.vue'].forEach(f=>{const s=fs.readFileSync(f,'utf8');const m=s.match(/<script>([\s\S]*?)<\/script>/);require('child_process').execSync('node --check',{input:m[1]});console.log('OK',f)})"
+```
+Expected: 3 行 OK 输出
+
+- [x] **Step 6: Commit**
+
+```bash
+git add cfc-frontend/utils/api.js cfc-frontend/components/FamilyChallengeCard.vue cfc-frontend/pages/health/challenge-manage.vue cfc-frontend/pages/index-home/index.vue
+git commit -m "feat(challenge): 小程序同意/拒绝+参与成员多选+pending展示"
+```
+
+---
+
+### Task 10: 全量验证 + 文档同步
+
+**Files:**
+- Modify: `docs/superpowers/PROJECT-OVERVIEW.md`
+- Modify: `docs/superpowers/plans/2026-08-08-family-challenge-participation.md`(状态标记)
+
+- [x] **Step 1: 后端全量编译 + 测试**
+
+Run: `cd cfc-backend && mvn clean compile && mvn test -Dtest=FamilyChallengeServiceTest,ChallengeScoreServiceTest`
+Expected: BUILD SUCCESS + 相关测试 PASS
+
+- [x] **Step 2: 路由/Bean 冲突复查**
+
+Run:
+```bash
+cd cfc-backend && grep -rn 'class AdminChallengeScoreConfigController\|class ChallengeScoreService' src/main/java --include=*.java
+```
+Expected: 各 1 处,无同名类冲突(Bean Name:`adminChallengeScoreConfigController`、`challengeScoreService`)
+
+- [x] **Step 3: PROJECT-OVERVIEW.md 同步计划条目**
+
+在 `docs/superpowers/PROJECT-OVERVIEW.md` 的"实施计划(plans/)"表格中追加:
+
+```markdown
+| `2026-08-08-family-challenge-participation.md` | 🟡 计划中 | 家庭挑战参与机制重构(任意成员发起/可选参与/同意后开始/方向性评分) |
+```
+
+并将 specs 表格中 `2026-08-08-family-challenge-participation-design.md` 的状态列保持 `✅ 设计已确认`。
+
+- [x] **Step 4: 提交文档**
+
+```bash
+git add docs/superpowers/PROJECT-OVERVIEW.md docs/superpowers/plans/2026-08-08-family-challenge-participation.md
+git commit -m "docs: 家庭挑战参与机制实现计划"
+```
+
+---
+
+## Self-Review
+
+**Spec 覆盖核对:**
+
+| spec 要求 | 对应任务 |
+|-----------|---------|
+| §3.1-3.3 三张新表 + status 状态机 | Task 1(迁移182-185) |
+| §3.4 评分配置默认值 | Task 1 迁移184 INSERT;Task 4 DEFAULT_CONFIG 兜底 |
+| §4.2 同意矩阵 | Task 4 applyAgree |
+| §4.3 拒绝矩阵(含 C3 只扣沟通) | Task 4 applyReject |
+| §4.4 发起矩阵 | Task 4 applyInitiate |
+| §5.1 create 改造(pending+participant+发起结算) | Task 5 Step 4 |
+| §5.2 respond 端点(agree/reject + 自动 active/cancelled) | Task 5 Step 3 + Task 6 Step 3 |
+| §5.3 list 返回 myStatus + pending 进度 | Task 5 Step 5 + Task 6 Step 1 |
+| §5.4 打卡校验(仅 agreed 参与成员) | Task 5 Step 9 |
+| §5.5 结算口径(仅 agreed) | Task 5 Step 7 |
+| §5.6 评分配置管理端 | Task 8 |
+| §7 前端:管理页去门控/成员多选、卡片同意拒绝、api.js | Task 9 |
+| §8 后端改动清单 | Task 2-8 |
+| §9 验收标准 | Task 10 全量验证 |
+| 存量 active 挑战兼容(迁移补 participant) | Task 1 迁移185 |
+
+**占位符扫描**:无 TBD/TODO;所有代码步骤含完整实现。
+
+**类型一致性**:`respondChallenge(challengeId, memberId, action)` 在 Service(Task 5)与 Controller(Task 6)、测试(Task 7)签名一致;`getActiveChallenges(familyId, currentMemberId)` 在 Service/Controller/测试一致;`createChallenge(familyId, userId, memberId, params)` 三处一致;`ChallengeScoreService` 三方法签名在 Task 4 定义、Task 5/7 消费一致;`EnergyService.awardEnergyByCode` 在 Task 3 定义、Task 4 消费一致。