# 五维能量系统实施计划(更新版) **日期:** 2026-06-09 **状态:** 待实施 **分支:** cfclub **工作量预估:** 3~4周 --- ## 现状分析 ### 现有代码(旧版·评分系统) | 文件 | 说明 | |------|------| | `EnergyController.java` | `/api/energy/sandbox` — 返回家庭能量沙盘(百分比评分) | | `EnergyService.java` | 基于任务完成率 + 游戏得分 + 测评结果 + 阅读时长 计算 0-100% 评分 | | `EnergyDimensionConfig.java` | 单表 `energy_dimension_config`(dimension/data_source/category/weight)⚠️ 将被废弃 | | `EnergyDimensionConfigMapper.java` | 旧表 Mapper ⚠️ 将被废弃 | | `EnergySandboxDTO.java` | 家庭聚合 + 成员列表 DTO | | `MemberEnergyDTO.java` | 成员五维评分DTO | | `api.js` | `getFamilyEnergySandbox()` → `/api/energy/sandbox` | ### 现有限制 - 旧版 EnergyService 是**计算型**(查询历史数据算百分比),不是**账本型**(发放/消耗) - 旧表 `energy_dimension_config` 是单表维度权重配置,与新 4 表设计不兼容 - DatabaseInitializer 中未包含任何能量表 DDL - 前端无能量详情页 ### 共存策略 旧版评分系统 = **健康指数(healthIndex)**的前身,新账本系统 = **能量值(energy)**。 两个概念在 API 响应中并存:`energy`(账本余额)+ `healthIndex`(百分比,暂时返回 0)。 **实施原则:** 1. 不修改旧版 `EnergyService` 的评分计算方法 2. 在旧版 `EnergyService` 中新增账本方法(award/deduct/overview/logs) 3. 旧版 Controller 保留 `/sandbox`,新增 `/overview` `/logs` `/configure` 4. 旧表 `energy_dimension_config` 暂不删除,新 4 表独立创建 --- ## Task 1: 数据库迁移 — 4张新表 + 种子数据 **文件:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` 在 `initSchema()` 末尾(`log.info("数据库迁移完成")` 之前)添加。 > 以下 SQL 可直接复制。注意:旧表 `energy_dimension_config` 不在此处创建(已在数据库中存在)。 - [ ] **1.1 DDL: 创建 4 张能量表** ```java // ==================== 五维能量系统(账本模式) ==================== // 维度定义表 jdbcTemplate.execute( "CREATE TABLE IF NOT EXISTS energy_dimension (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "code VARCHAR(16) NOT NULL UNIQUE COMMENT 'body/mind/wisdom/action/wealth', " + "name VARCHAR(20) NOT NULL COMMENT '身/心/智/行/富', " + "icon VARCHAR(16) COMMENT '图标emoji', " + "element VARCHAR(10) COMMENT '五行: 土/火/金/木/水', " + "sort_order INT DEFAULT 0 COMMENT '显示顺序', " + "status TINYINT DEFAULT 1 COMMENT '1启用/0禁用', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量维度定义表'" ); // 服务-维度比例配置表 jdbcTemplate.execute( "CREATE TABLE IF NOT EXISTS energy_source_config (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "source_type VARCHAR(32) NOT NULL COMMENT '来源类型: task/activity/course/product/consultation', " + "source_id BIGINT NOT NULL COMMENT '来源业务表ID', " + "source_name VARCHAR(200) COMMENT '冗余名称', " + "dimension_id BIGINT NOT NULL COMMENT '维度ID', " + "ratio DECIMAL(5,4) NOT NULL COMMENT '占比如0.6000=60%', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "INDEX idx_source (source_type, source_id)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务-维度比例配置表'" ); // 能量流水表 jdbcTemplate.execute( "CREATE TABLE IF NOT EXISTS energy_log (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "child_id BIGINT NOT NULL COMMENT '孩子ID', " + "dimension_id BIGINT NOT NULL COMMENT '维度ID', " + "amount INT NOT NULL COMMENT '变动量(正=获得,负=消耗)', " + "balance_after INT NOT NULL COMMENT '变动后余额', " + "source_type VARCHAR(32) COMMENT '来源类型: task/product/medical_report/daily_record', " + "source_id BIGINT COMMENT '来源ID', " + "ref_id BIGINT COMMENT '关联旧流水ID(覆盖时标记)', " + "expires_at DATETIME COMMENT '过期时间(空=永不过期)', " + "description VARCHAR(500) COMMENT '描述', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "INDEX idx_child_dim (child_id, dimension_id), " + "INDEX idx_created (created_at), " + "INDEX idx_source (source_type, source_id)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量流水表'" ); // 维度余额表 jdbcTemplate.execute( "CREATE TABLE IF NOT EXISTS energy_balance (" + "id BIGINT AUTO_INCREMENT PRIMARY KEY, " + "child_id BIGINT NOT NULL COMMENT '孩子ID', " + "dimension_id BIGINT NOT NULL COMMENT '维度ID', " + "balance INT DEFAULT 0 COMMENT '当前能量值(可为负)', " + "total_earned INT DEFAULT 0 COMMENT '累计获得', " + "total_spent INT DEFAULT 0 COMMENT '累计消耗', " + "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " + "UNIQUE KEY uk_child_dim (child_id, dimension_id)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维度余额表'" ); ``` - [ ] **1.2 种子数据: 维度定义** ```java // 维度种子数据(按相生链: 智→富→行→心→身) Integer dimCount = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM energy_dimension", Integer.class); if (dimCount == null || dimCount == 0) { jdbcTemplate.execute("INSERT INTO energy_dimension (code, name, icon, element, sort_order, status) VALUES " + "('mind', '心', '🔥', '火', 4, 1), " + "('body', '身', '🌏', '土', 5, 1), " + "('wisdom', '智', '⚔️', '金', 1, 1), " + "('action', '行', '🌿', '木', 3, 1), " + "('wealth', '富', '💧', '水', 2, 1)"); } ``` - [ ] **1.3 种子数据: 已有商品映射** ```java // 为已有商品插入默认比例配置(与Product.domain一致) Integer productConfigCount = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM energy_source_config WHERE source_type = 'product'", Integer.class); if (productConfigCount == null || productConfigCount == 0) { String[][] domainMapping = { {"action", "action"}, {"mind", "mind"}, {"body", "body"}, {"wisdom", "wisdom"}, {"wealth", "wealth"} }; for (String[] mapping : domainMapping) { jdbcTemplate.execute( "INSERT INTO energy_source_config (source_type, source_id, source_name, dimension_id, ratio) " + "SELECT 'product', p.id, p.name, d.id, 1.0000 " + "FROM products p " + "JOIN energy_dimension d ON d.code = '" + mapping[1] + "' " + "WHERE p.domain = '" + mapping[0] + "' " + "AND NOT EXISTS (" + " SELECT 1 FROM energy_source_config esc " + " WHERE esc.source_type = 'product' AND esc.source_id = p.id" + ")" ); } } ``` - [ ] **1.4 验证:** `cd cfc-backend && mvn clean compile -q` --- ## Task 2: 4个新 Entity **新建:** - `entity/EnergyDimension.java` - `entity/EnergySourceConfig.java` - `entity/EnergyLog.java` - `entity/EnergyBalance.java` 遵循现有模式:`@Data @TableName @TableId(type = IdType.AUTO) implements Serializable` > **注意:** 新增 `EnergyLog.expiresAt` 和 `EnergyLog.refId` 字段(与总设计 spec 相比新增,用于支持过期和覆盖机制)。 - [ ] **2.1 创建 EnergyDimension.java** ```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("energy_dimension") public class EnergyDimension implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String code; // body/mind/wisdom/action/wealth private String name; // 身/心/智/行/富 private String icon; // emoji private String element; // 土/火/金/木/水 private Integer sortOrder; private Integer status; // 1启用/0禁用 private Date createdAt; } ``` - [ ] **2.2 创建 EnergySourceConfig.java** ```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("energy_source_config") public class EnergySourceConfig implements Serializable { @TableId(type = IdType.AUTO) private Long id; private String sourceType; // task/activity/course/product/consultation private Long sourceId; private String sourceName; private Long dimensionId; private BigDecimal ratio; // 0.6000 = 60% private Date createdAt; } ``` - [ ] **2.3 创建 EnergyLog.java** ```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("energy_log") public class EnergyLog implements Serializable { @TableId(type = IdType.AUTO) private Long id; private Long childId; private Long dimensionId; private Integer amount; private Integer balanceAfter; private String sourceType; // task/product/medical_report/daily_record private Long sourceId; private Long refId; // 关联旧流水(覆盖时标记) private Date expiresAt; // 过期时间(空=永不过期) private String description; private Date createdAt; } ``` - [ ] **2.4 创建 EnergyBalance.java** ```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("energy_balance") public class EnergyBalance implements Serializable { @TableId(type = IdType.AUTO) private Long id; private Long childId; private Long dimensionId; private Integer balance; private Integer totalEarned; private Integer totalSpent; private Date updatedAt; } ``` - [ ] **2.5 验证:** `cd cfc-backend && mvn clean compile -q` --- ## Task 3: 4个 Mapper **新建:** - `mapper/EnergyDimensionMapper.java` - `mapper/EnergySourceConfigMapper.java` - `mapper/EnergyLogMapper.java` - `mapper/EnergyBalanceMapper.java` 遵循现有模式:`@Mapper interface XxxMapper extends BaseMapper`,无自定义方法。 - [ ] **3.1 创建 4 个 Mapper**(略,标准模板) - [ ] **3.2 验证:** `cd cfc-backend && mvn clean compile -q` --- ## Task 4: EnergyService 扩展 — 新增账本方法 **文件:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java` 在现有`EnergyService`类中**新增**以下账本方法,保留旧方法不动。 - [ ] **4.1 新增注入 + 维度缓存** ```java // ==== 新增: 账本注入 ==== @Resource private EnergyDimensionMapper energyDimensionMapper; @Resource private EnergySourceConfigMapper energySourceConfigMapper; @Resource private EnergyLogMapper energyLogMapper; @Resource private EnergyBalanceMapper energyBalanceMapper; // 维度缓存(懒加载,维度数据极少变更) private List dimCache; private Map dimCodeMap; ``` - [ ] **4.2 新增: awardEnergy() — 发放能量,自动查比例配置分配各维度** ```java /** * 发放能量 — 按 energy_source_config 比例分配至各维度 * @param childId 孩子ID * @param sourceType 来源类型 (task/product/medical_report/etc) * @param sourceId 来源ID * @param totalAmount 总能量值 * @param description 描述 * @param daysToExpire 过期天数(null=不过期) * @return Map */ @Transactional public Map awardEnergy(Long childId, String sourceType, Long sourceId, Integer totalAmount, String description, Integer daysToExpire) { if (totalAmount == null || totalAmount <= 0) return new HashMap<>(); // 1. 查比例配置 → Fallback链 List configs = energySourceConfigMapper.selectList( new LambdaQueryWrapper() .eq(EnergySourceConfig::getSourceType, sourceType) .eq(EnergySourceConfig::getSourceId, sourceId) ); Map dimRatios = resolveDimensionRatios(configs, sourceType, sourceId); if (dimRatios.isEmpty()) return new HashMap<>(); // 2. 日上限检查 if (isDailyLimitExceeded(childId, dimRatios.keySet(), totalAmount)) { log.warn("维度日上限已达,跳过发放"); return new HashMap<>(); } // 3. 按比例分配(整数,余数加到最大比例维度) Map allocations = calculateAllocations(totalAmount, dimRatios); // 4. 更新余额 + 写流水 Map result = new LinkedHashMap<>(); Date now = new Date(); Date expiresAt = daysToExpire != null ? new Date(now.getTime() + (long) daysToExpire * 86400000L) : null; for (Map.Entry entry : allocations.entrySet()) { if (entry.getValue() <= 0) continue; EnergyBalance balance = getOrCreateBalance(childId, entry.getKey()); balance.setBalance(balance.getBalance() + entry.getValue()); balance.setTotalEarned(balance.getTotalEarned() + entry.getValue()); balance.setUpdatedAt(now); energyBalanceMapper.updateById(balance); EnergyLog log = new EnergyLog(); log.setChildId(childId); log.setDimensionId(entry.getKey()); log.setAmount(entry.getValue()); log.setBalanceAfter(balance.getBalance()); log.setSourceType(sourceType); log.setSourceId(sourceId); log.setExpiresAt(expiresAt); log.setDescription(description); log.setCreatedAt(now); energyLogMapper.insert(log); EnergyDimension dim = getDimById(entry.getKey()); if (dim != null) result.put(dim.getCode(), entry.getValue()); } return result; } ``` - [ ] **4.3 新增: deductEnergy() — 扣除能量(余额不足返回 -1)** ```java /** * 扣除能量 * @return 扣除后余额,-1 = 余额不足 */ @Transactional public int deductEnergy(Long childId, Long dimensionId, Integer amount, String reason) { if (amount == null || amount <= 0) throw new IllegalArgumentException("扣除数量必须为正数"); EnergyBalance balance = getOrCreateBalance(childId, dimensionId); if (balance.getBalance() < amount) return -1; balance.setBalance(balance.getBalance() - amount); balance.setTotalSpent(balance.getTotalSpent() + amount); balance.setUpdatedAt(new Date()); energyBalanceMapper.updateById(balance); EnergyLog log = new EnergyLog(); log.setChildId(childId); log.setDimensionId(dimensionId); log.setAmount(-amount); log.setBalanceAfter(balance.getBalance()); log.setDescription(reason); log.setCreatedAt(new Date()); energyLogMapper.insert(log); return balance.getBalance(); } ``` - [ ] **4.4 新增: getOverview() — 查询五维概览** ```java /** * 查询五维能量概览 */ public Map getOverview(Long childId) { List allDims = getAllDimensions(); List> dimList = new ArrayList<>(); int totalEnergy = 0; for (EnergyDimension dim : allDims) { if (dim.getStatus() != 1) continue; EnergyBalance balance = energyBalanceMapper.selectOne( new LambdaQueryWrapper() .eq(EnergyBalance::getChildId, childId) .eq(EnergyBalance::getDimensionId, dim.getId()) ); int energy = balance != null ? balance.getBalance() : 0; totalEnergy += energy; Map item = new LinkedHashMap<>(); item.put("code", dim.getCode()); item.put("name", dim.getName()); item.put("icon", dim.getIcon()); item.put("element", dim.getElement()); item.put("energy", energy); item.put("healthIndex", 0); // 算法待定 dimList.add(item); } Map result = new LinkedHashMap<>(); result.put("dimensions", dimList); result.put("totalEnergy", totalEnergy); result.put("totalHealthIndex", 0); return result; } ``` - [ ] **4.5 新增: getLogs() — 流水查询(按维度筛选,分页)** ```java public Page getLogs(Long childId, String dimensionCode, Integer page, Integer size) { Page pageParam = new Page<>(page, size); LambdaQueryWrapper wrapper = new LambdaQueryWrapper() .eq(EnergyLog::getChildId, childId); if (dimensionCode != null && !dimensionCode.isEmpty()) { EnergyDimension dim = getDimByCode(dimensionCode); if (dim != null) wrapper.eq(EnergyLog::getDimensionId, dim.getId()); } wrapper.orderByDesc(EnergyLog::getCreatedAt); return energyLogMapper.selectPage(pageParam, wrapper); } ``` - [ ] **4.6 新增: configureSourceRatios() — 配置来源比例** ```java @Transactional public void configureSourceRatios(String sourceType, Long sourceId, String sourceName, List> ratios) { energySourceConfigMapper.delete( new LambdaQueryWrapper() .eq(EnergySourceConfig::getSourceType, sourceType) .eq(EnergySourceConfig::getSourceId, sourceId) ); for (Map r : ratios) { String dimCode = (String) r.get("dimensionCode"); BigDecimal ratioVal = new BigDecimal(r.get("ratio").toString()); EnergyDimension dim = getDimByCode(dimCode); if (dim == null) continue; EnergySourceConfig config = new EnergySourceConfig(); config.setSourceType(sourceType); config.setSourceId(sourceId); config.setSourceName(sourceName); config.setDimensionId(dim.getId()); config.setRatio(ratioVal); config.setCreatedAt(new Date()); energySourceConfigMapper.insert(config); } } ``` - [ ] **4.7 新增: 内部辅助方法** ```java // ===== 辅助方法 ===== private Map resolveDimensionRatios( List configs, String sourceType, Long sourceId) { if (!configs.isEmpty()) { Map result = new LinkedHashMap<>(); for (EnergySourceConfig c : configs) result.put(c.getDimensionId(), c.getRatio()); return result; } // Fallback 1: Product.domain if ("product".equals(sourceType)) { // 查 Product.domain → 单维度 100% try { ... } catch (Exception ignored) {} } // Fallback 2: 默认 → 行(action) 100% EnergyDimension action = getDimByCode("action"); if (action != null) return Collections.singletonMap(action.getId(), BigDecimal.ONE); return Collections.emptyMap(); } private Map calculateAllocations(Integer total, Map ratios) { ... } private EnergyBalance getOrCreateBalance(Long childId, Long dimId) { ... } private boolean isDailyLimitExceeded(Long childId, Set dimIds, Integer amount) { ... } private List getAllDimensions() { ... } private EnergyDimension getDimByCode(String code) { ... } private EnergyDimension getDimById(Long id) { ... } ``` > **`isDailyLimitExceeded` 逻辑:** > `energy_log` 查 childId + dimensionId + 今日 created_at 的 amount 总和 + 本次发放量 > 该维度日上限 → true > 同时校验五维总和日上限 **默认日上限(从 remaining-dimensions 设计):** | 维度 | 日上限 | |------|-------| | 身 | 50 | | 心 | 40 | | 智 | 80 | | 行 | 60 | | 富 | 30 | | ⚠️ 全局 | 200 | - [ ] **4.8 验证:** `cd cfc-backend && mvn clean compile -q` --- ## Task 5: EnergyController 新增端点 **文件:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/EnergyController.java` 保留现有 `/api/energy/sandbox` 端点,新增 3 个端点。 > 注意:现有 controller 在 `controller/` 下(不在 `/energy` 子包),保持该目录。 - [ ] **5.1 新增 3 个端点** ```java @PostMapping("/overview") public Result> getOverview(@RequestBody Map params) { ... } @PostMapping("/logs") public Result> getLogs(@RequestBody Map params) { ... } @PostMapping("/configure") public Result configure(@RequestBody Map params, @RequestAttribute("userId") Long userId) { ... } ``` > 路径映射确认(避免冲突):当前 `@RequestMapping("/api/energy")` 下有 `/sandbox`、`/overview`、`/logs`、`/configure`,无重复。 - [ ] **5.2 验证:** `cd cfc-backend && mvn clean compile -q` --- ## Task 6: TaskService 集成 — 完成任务时发放能量 **文件:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java` 在 `completeTask()` 中,积分发放之后、`Map result` 之前,添加能量发放调用。 - [ ] **6.1 注入 EnergyService** ```java @Resource private EnergyService energyService; ``` - [ ] **6.2 在 completeTask() 末尾添加** ```java // 五维能量发放(与积分并行,独立系统,失败不影响积分) try { int energyAmount = Math.abs(pointsEarned); if (energyAmount > 0) { // 查 task.category 的过期天数配置,目前任务不过期 energyService.awardEnergy(childId, "task", taskId, energyAmount, "完成任务: " + task.getTitle(), null); } } catch (Exception e) { log.warn("能量发放失败(不影响积分): childId={}, taskId={}, error={}", childId, taskId, e.getMessage()); } ``` - [ ] **6.3 验证:** `cd cfc-backend && mvn clean compile -q` --- ## Task 7: 前端 API 封装 **文件:** - Modify: `cfc-frontend/utils/api.js` 在末尾新增 3 个 API 调用。 - [ ] **7.1 在 api.js 末尾添加** ```js // ==== 五维能量系统 ==== export const getEnergyOverview = (childId) => { return request('/api/energy/overview', 'POST', { childId }) } export const getEnergyLogs = (childId, dimensionCode, page = 1, size = 10) => { return request('/api/energy/logs', 'POST', { childId, dimensionCode, page, size }) } ``` > 已有 `getFamilyEnergySandbox`(旧版 `/api/energy/sandbox`),保留不动。 - [ ] **7.2 Commit** --- ## Task 8: 前端 wuxing-sandbox 组件重写 **文件:** - Modify: `cfc-frontend/components/wuxing-sandbox.vue` 组件改造为接收真实数据 props,实现三层视觉结构(外层维度角、中层健康指数扇区、内层中心圆)。 - [ ] **8.1 组件 props 更新** 当前组件已有 props: `mode`, `badgeText`。 新增 props: `dimensions`, `totalEnergy`, `totalHealthIndex`。 ```js props: { mode: { type: String, default: 'preview' }, badgeText: { type: String, default: '综合成长力 0%' }, dimensions: { type: Array, default: () => [] }, totalEnergy: { type: Number, default: 0 }, totalHealthIndex: { type: Number, default: 0 } }, computed: { sortedDimensions: function () { // 按顺序: 心/行/富/智/身 → mind/action/wealth/wisdom/body var order = ['mind', 'action', 'wealth', 'wisdom', 'body']; var dimMap = {} for (var i = 0; i < this.dimensions.length; i++) { dimMap[this.dimensions[i].code] = this.dimensions[i] } var result = [] for (var j = 0; j < order.length; j++) { result.push(dimMap[order[j]] || { code: order[j], name: '', icon: '', element: '', energy: 0, healthIndex: 0 }) } return result } } ``` > ⚠️ **Vue 2 限制(不使用 Composition API):** 禁止可选链 `?.`,用 `&&` 短路或用 `|| {}` 兜底 - [ ] **8.2 template 改造** ```html {{ sortedDimensions[0].icon }} {{ sortedDimensions[0].name }}·{{ sortedDimensions[0].element }} {{ sortedDimensions[0].energy }} ``` - [ ] **8.3 验证:** `cd cfc-frontend && npm run dev:mp-weixin`(检查编译) --- ## Task 9: 首页集成 — parent-index + child-index **文件:** - Modify: `cfc-frontend/pages/index/parent-index.vue` - Modify: `cfc-frontend/pages/index/child-index.vue` 在 `onShow` 中调用 `getEnergyOverview`,将数据传入 wuxing-sandbox。 - [ ] **9.1 parent-index.vue 修改** ```js import { getEnergyOverview } from '../../utils/api.js' // data 新增: energyDimensions: [], totalEnergy: 0, totalHealthIndex: 0, // methods 新增: loadEnergyOverview: function () { var that = this var childId = that.currentChildId if (!childId) return getEnergyOverview(childId).then(function (res) { if (res.data) { that.energyDimensions = res.data.dimensions || [] that.totalEnergy = res.data.totalEnergy || 0 that.totalHealthIndex = res.data.totalHealthIndex || 0 } }).catch(function () { /* 静默失败 */ }) }, // onShow 末尾调用: this.loadEnergyOverview() ``` template 中的 wuxing-sandbox: ```html ``` - [ ] **9.2 child-index.vue 修改** (同上模式,使用 `that.childId` 代替 `currentChildId`) --- ## Task 10: 前端能量详情页 + pages.json **文件:** - Create: `cfc-frontend/pages/energy/detail.vue` - Modify: `cfc-frontend/pages.json` - [ ] **10.1 创建 detail.vue**(维度详情页:头部显示维度信息 + 流水列表 + 加载更多) - [ ] **10.2 pages.json 注册** ```json { "path": "pages/energy/detail", "style": { "navigationBarTitleText": "维度能量详情" } } ``` - [ ] **10.3 从 wuxing-sandbox 点击跳转** wuxing-sandbox 的 `@point-click` 事件在父页面处理: ```js goToDomain: function (code) { var childId = this.currentChildId || this.childId var dim = this.sortedDimensions ? this.sortedDimensions.find(function (d) { return d.code === code }) : null if (!childId || !dim) return uni.navigateTo({ url: '/pages/energy/detail?childId=' + childId + '&code=' + code + '&dimData=' + encodeURIComponent(JSON.stringify(dim)) }) } ``` --- ## Phase 2(后续实施) 以下任务在核心框架上线后,按业务优先级分步实施。 ### Task 11: 身体维度接口集成 - 新建 `BodyHealthMetricController`(或扩展现有)中接入 `awardEnergy` - 实现体检报告能量计算逻辑(基础50 + 覆盖率 + 质量奖励 + 专项奖励) - 实现日常记录能量计算(连续性系数 × 丰富性系数,日上限50) - 覆盖逻辑:新报告 → 查找旧流水 → 扣旧发新 ### Task 12: 任务 category 默认比例映射 在 `TaskService.completeTask()` 中,若 `energy_source_config` 无配置,按 `task.category` 找默认比例: | category | body | mind | wisdom | action | wealth | |----------|------|------|--------|--------|--------| | 运动类 | 60% | — | — | 40% | — | | 学习类 | — | — | 100% | — | — | | 生活习惯 | 40% | 30% | — | 30% | — | | 家务类 | — | 10% | — | 90% | — | | 阅读类 | — | 20% | 80% | — | — | | 财商类 | — | — | 10% | — | 90% | | 默认 | 30% | 20% | 20% | 25% | 5% | > `CATEGORY_DIMENSION_MAP` 定义在 `EnergyService` 中,`TaskService` 调用时自动 fallback ### Task 13: 能量过期定时任务 - 每日凌晨执行 `ScheduledEnergyExpiryTask` - SQL: `SELECT * FROM energy_log WHERE expires_at IS NOT NULL AND expires_at < NOW() AND ref_id IS NULL` - 对每条过期流水:扣减余额 + 写入回收流水(标记 `ref_id`) ### Task 14: 旧代码清理(可选) - 确认 `energy_dimension_config` 表不再被任何功能引用 - 删除 `EnergyDimensionConfig.java` / `EnergyDimensionConfigMapper.java` / `EnergySandboxDTO.java` - `MemberEnergyDTO.java` 保留(仍是健康指数的基础) --- ## 工作量汇总 | Task | 内容 | 文件数 | 预估工时 | 依赖 | |------|------|--------|---------|------| | 1 | 数据库迁移 | 1 | 2h | — | | 2 | 4个Entity | 4 | 1h | T1 | | 3 | 4个Mapper | 4 | 0.5h | T2 | | 4 | EnergyService账本方法 | 1 | 3h | T3 | | 5 | EnergyController端点 | 1 | 1h | T4 | | 6 | TaskService集成 | 1 | 1h | T4 | | 7 | 前端API封装 | 1 | 0.5h | T5 | | 8 | wuxing-sandbox重写 | 1 | 2h | T7 | | 9 | 首页集成 | 2 | 1h | T7-T8 | | 10 | 能量详情页 | 2 | 2h | T7 | | **Phase 1** | **基础框架上线** | **18** | **≈2周** | — | | 11 | 身体维度集成 | 2+ | 2d | T4 | | 12 | category映射 | 1 | 1d | T6 | | 13 | 过期定时任务 | 1 | 1d | T4 | | 14 | 旧代码清理 | 3 | 0.5d | T4-T10 | | **Phase 2** | **全功能上线** | — | **≈1周** | — | --- ## 验证清单 - [ ] `mvn clean compile` 通过 - [ ] 应用启动无报错 - [ ] `POST /api/energy/overview` 返回五维能量值 - [ ] `POST /api/energy/logs` 返回流水(分页) - [ ] 完成任务后 `energy_log` 产生记录 - [ ] `GET /api/energy/sandbox` 旧接口不破坏(向后兼容) - [ ] wuxing-sandbox 显示真实数据(非0占位) - [ ] 维度能量详情页可跳转 - [ ] 日上限生效(测试连续发同名能量) ## 相关文档 - `docs/superpowers/specs/2026-06-05-five-dimension-energy-design.md` — 五维能量系统总设计(4表方案) - `docs/superpowers/specs/2026-06-09-body-dimension-energy-design.md` — 身维度计算规则 - `docs/superpowers/specs/2026-06-09-remaining-dimensions-energy-design.md` — 心/智/行/富四维 + category映射 - `docs/superpowers/plans/2026-06-09-energy-system-implementation-plan.md` — 本文件