2026-06-09-energy-system-implementation-plan.md 30 KB

五维能量系统实施计划(更新版)

日期: 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 张能量表

    // ==================== 五维能量系统(账本模式) ====================
    
    // 维度定义表
    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 种子数据: 维度定义

    // 维度种子数据(按相生链: 智→富→行→心→身)
    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 种子数据: 已有商品映射

    // 为已有商品插入默认比例配置(与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.expiresAtEnergyLog.refId 字段(与总设计 spec 相比新增,用于支持过期和覆盖机制)。

  • [ ] 2.1 创建 EnergyDimension.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

    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

    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

    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<Xxx>,无自定义方法。

  • 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 新增注入 + 维度缓存

    // ==== 新增: 账本注入 ====
    @Resource
    private EnergyDimensionMapper energyDimensionMapper;
    @Resource
    private EnergySourceConfigMapper energySourceConfigMapper;
    @Resource
    private EnergyLogMapper energyLogMapper;
    @Resource
    private EnergyBalanceMapper energyBalanceMapper;
    
    // 维度缓存(懒加载,维度数据极少变更)
    private List<EnergyDimension> dimCache;
    private Map<String, EnergyDimension> dimCodeMap;
    
  • [ ] 4.2 新增: awardEnergy() — 发放能量,自动查比例配置分配各维度

    /**
    * 发放能量 — 按 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<dimensionCode, amount>
    */
    @Transactional
    public Map<String, Integer> awardEnergy(Long childId, String sourceType, Long sourceId,
                                         Integer totalAmount, String description,
                                         Integer daysToExpire) {
    if (totalAmount == null || totalAmount <= 0) return new HashMap<>();
    
    // 1. 查比例配置 → Fallback链
    List<EnergySourceConfig> configs = energySourceConfigMapper.selectList(
        new LambdaQueryWrapper<EnergySourceConfig>()
            .eq(EnergySourceConfig::getSourceType, sourceType)
            .eq(EnergySourceConfig::getSourceId, sourceId)
    );
    Map<Long, BigDecimal> 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<Long, Integer> allocations = calculateAllocations(totalAmount, dimRatios);
    
    // 4. 更新余额 + 写流水
    Map<String, Integer> result = new LinkedHashMap<>();
    Date now = new Date();
    Date expiresAt = daysToExpire != null
        ? new Date(now.getTime() + (long) daysToExpire * 86400000L) : null;
    
    for (Map.Entry<Long, Integer> 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)

    /**
    * 扣除能量
    * @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() — 查询五维概览

    /**
    * 查询五维能量概览
    */
    public Map<String, Object> getOverview(Long childId) {
    List<EnergyDimension> allDims = getAllDimensions();
    List<Map<String, Object>> dimList = new ArrayList<>();
    int totalEnergy = 0;
    
    for (EnergyDimension dim : allDims) {
        if (dim.getStatus() != 1) continue;
        EnergyBalance balance = energyBalanceMapper.selectOne(
            new LambdaQueryWrapper<EnergyBalance>()
                .eq(EnergyBalance::getChildId, childId)
                .eq(EnergyBalance::getDimensionId, dim.getId())
        );
        int energy = balance != null ? balance.getBalance() : 0;
        totalEnergy += energy;
    
        Map<String, Object> 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<String, Object> result = new LinkedHashMap<>();
    result.put("dimensions", dimList);
    result.put("totalEnergy", totalEnergy);
    result.put("totalHealthIndex", 0);
    return result;
    }
    
  • [ ] 4.5 新增: getLogs() — 流水查询(按维度筛选,分页)

    public Page<EnergyLog> getLogs(Long childId, String dimensionCode, Integer page, Integer size) {
    Page<EnergyLog> pageParam = new Page<>(page, size);
    LambdaQueryWrapper<EnergyLog> wrapper = new LambdaQueryWrapper<EnergyLog>()
        .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() — 配置来源比例

    @Transactional
    public void configureSourceRatios(String sourceType, Long sourceId, String sourceName,
                                   List<Map<String, Object>> ratios) {
    energySourceConfigMapper.delete(
        new LambdaQueryWrapper<EnergySourceConfig>()
            .eq(EnergySourceConfig::getSourceType, sourceType)
            .eq(EnergySourceConfig::getSourceId, sourceId)
    );
    for (Map<String, Object> 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 新增: 内部辅助方法

    // ===== 辅助方法 =====
    
    private Map<Long, BigDecimal> resolveDimensionRatios(
    List<EnergySourceConfig> configs, String sourceType, Long sourceId) {
        
    if (!configs.isEmpty()) {
        Map<Long, BigDecimal> 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<Long, Integer> calculateAllocations(Integer total, Map<Long, BigDecimal> ratios) { ... }
    private EnergyBalance getOrCreateBalance(Long childId, Long dimId) { ... }
    private boolean isDailyLimitExceeded(Long childId, Set<Long> dimIds, Integer amount) { ... }
    private List<EnergyDimension> 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 个端点

    @PostMapping("/overview")
    public Result<Map<String, Object>> getOverview(@RequestBody Map<String, Object> params) { ... }
    
    @PostMapping("/logs")
    public Result<Page<EnergyLog>> getLogs(@RequestBody Map<String, Object> params) { ... }
    
    @PostMapping("/configure")
    public Result<String> configure(@RequestBody Map<String, Object> 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<String, Object> result 之前,添加能量发放调用。

  • [ ] 6.1 注入 EnergyService

    @Resource
    private EnergyService energyService;
    
  • [ ] 6.2 在 completeTask() 末尾添加

    // 五维能量发放(与积分并行,独立系统,失败不影响积分)
    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 末尾添加

    // ==== 五维能量系统 ====
    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

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 改造

    <!-- 心·火 (顶) → sortedDimensions[0] -->
    <view class="star-point star-point-top" @click="onPointClick(0)">
    <text class="point-icon">{{ sortedDimensions[0].icon }}</text>
    <text class="point-label">{{ sortedDimensions[0].name }}·{{ sortedDimensions[0].element }}</text>
    <text class="point-energy">{{ sortedDimensions[0].energy }}</text>
    </view>
    <!-- 行·木 (左), 富·水 (右), 智·金 (左下), 身·土 (右下) → index 1-4 -->
    
  • [ ] 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 修改

    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:

<wuxing-sandbox mode="parent"
  :badgeText="'综合成长力 ' + totalHealthIndex + '%'"
  :dimensions="energyDimensions"
  :totalEnergy="totalEnergy"
  :totalHealthIndex="totalHealthIndex"
  @point-click="goToDomain" />
  • 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 注册

    {
    "path": "pages/energy/detail",
    "style": {
    "navigationBarTitleText": "维度能量详情"
    }
    }
    
  • [ ] 10.3 从 wuxing-sandbox 点击跳转

wuxing-sandbox 的 @point-click 事件在父页面处理:

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 — 本文件