# 五维能量系统实施计划 > **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 (`- [ ]`) syntax for tracking. **Goal:** 实现身/心/智/行/富五维能量系统,包括4张新表、后端Entity/Mapper/Service/Controller、数据库迁移、前端wuxing-sandbox组件重写、能量详情页、API集成 **Architecture:** 方案B — 维度定义表+流水表+余额表+比例配置表。标准ledger模式(流水+余额双写),awardEnergy自动查比例分配各维度,deductEnergy检查余额后扣减。与现有积分系统完全解耦并行。 **Tech Stack:** Java 1.8 + Spring Boot 2.7.18 + MyBatis-Plus + MySQL 8.0(后端)| uni-app Vue 2(前端小程序) **Design Spec:** `docs/superpowers/specs/2026-06-05-five-dimension-energy-design.md` --- ## File Structure ### 后端新建文件 | 文件 | 职责 | |------|------| | `cfc-backend/src/main/java/com/etotem/cfc/entity/EnergyDimension.java` | 维度定义实体 | | `cfc-backend/src/main/java/com/etotem/cfc/entity/EnergySourceConfig.java` | 服务-维度比例配置实体 | | `cfc-backend/src/main/java/com/etotem/cfc/entity/EnergyLog.java` | 能量流水实体 | | `cfc-backend/src/main/java/com/etotem/cfc/entity/EnergyBalance.java` | 维度余额实体 | | `cfc-backend/src/main/java/com/etotem/cfc/mapper/EnergyDimensionMapper.java` | 维度CRUD | | `cfc-backend/src/main/java/com/etotem/cfc/mapper/EnergySourceConfigMapper.java` | 比例配置CRUD | | `cfc-backend/src/main/java/com/etotem/cfc/mapper/EnergyLogMapper.java` | 流水查询 | | `cfc-backend/src/main/java/com/etotem/cfc/mapper/EnergyBalanceMapper.java` | 余额查询/更新 | | `cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java` | 核心业务:发放/扣除/查概览/查流水/配比例 | | `cfc-backend/src/main/java/com/etotem/cfc/controller/energy/EnergyController.java` | API入口 | ### 后端修改文件 | 文件 | 修改内容 | |------|----------| | `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` | 添加4张表DDL + 种子数据 + 比例配置迁移 | | `cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java` | completeTask()末尾添加energyService.awardEnergy()调用 | ### 前端修改文件 | 文件 | 修改内容 | |------|----------| | `cfc-frontend/components/wuxing-sandbox.vue` | 完全重写:接收dimensions/totalEnergy/totalHealthIndex props,显示真实数据,三层视觉结构 | | `cfc-frontend/utils/api.js` | 添加getEnergyOverview/getEnergyLogs | | `cfc-frontend/pages/index/parent-index.vue` | 传入API获取的dimensions/totalEnergy/totalHealthIndex | | `cfc-frontend/pages/index/child-index.vue` | 同上 | | `cfc-frontend/pages.json` | 注册energy/detail页面 | ### 前端新建文件 | 文件 | 职责 | |------|------| | `cfc-frontend/pages/energy/detail.vue` | 维度详情页:能量值、流水记录 | --- ## Task 1: 数据库迁移 — 4张新表 + 种子数据 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` 在 `initSchema()` 方法末尾(`log.info("数据库迁移完成")` 之前)添加能量系统4张表DDL和种子数据。 - [ ] **Step 1: 在DatabaseInitializer.initSchema()末尾添加4张表DDL** 在 `log.info("数据库迁移完成")` 之前添加: ```java // ==================== 五维能量系统 ==================== // 维度定义表 try { 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='能量维度定义表'" ); log.info("已创建energy_dimension表"); } catch (Exception e) { log.warn("创建energy_dimension表失败: {}", e.getMessage()); } // 服务-维度比例配置表 try { 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='服务-维度比例配置表'" ); log.info("已创建energy_source_config表"); } catch (Exception e) { log.warn("创建energy_source_config表失败: {}", e.getMessage()); } // 能量流水表 try { 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 '来源类型', " + "source_id BIGINT COMMENT '来源ID', " + "description VARCHAR(500) COMMENT '描述', " + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + "INDEX idx_child_dim (child_id, dimension_id), " + "INDEX idx_created (created_at)" + ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量流水表'" ); log.info("已创建energy_log表"); } catch (Exception e) { log.warn("创建energy_log表失败: {}", e.getMessage()); } // 维度余额表 try { 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='维度余额表'" ); log.info("已创建energy_balance表"); } catch (Exception e) { log.warn("创建energy_balance表失败: {}", e.getMessage()); } ``` - [ ] **Step 2: 在表DDL之后添加维度种子数据** ```java // 维度种子数据 try { 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', '心', '🔥', '火', 1, 1), " + "('action', '行', '🌿', '木', 2, 1), " + "('wealth', '富', '💧', '水', 3, 1), " + "('wisdom', '智', '⚔️', '金', 4, 1), " + "('body', '身', '🌏', '土', 5, 1)"); log.info("能量维度种子数据已初始化"); } } catch (Exception e) { log.warn("初始化能量维度种子数据失败: {}", e.getMessage()); } ``` - [ ] **Step 3: 为已有Product种子数据插入默认比例配置** 在种子数据插入之后,添加Product→energy_source_config的1:1单维度映射: ```java // 为已有商品插入默认比例配置(与Product.domain一致) try { Integer configCount = jdbcTemplate.queryForObject( "SELECT COUNT(*) FROM energy_source_config WHERE source_type = 'product'", Integer.class); if (configCount == null || configCount == 0) { // domain→dimension_code映射 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" + ")" ); } log.info("已有商品能量比例配置已初始化"); } } catch (Exception e) { log.warn("初始化商品能量比例配置失败: {}", e.getMessage()); } ``` - [ ] **Step 4: 验证编译通过** Run: `cd cfc-backend && mvn clean compile -q` Expected: BUILD SUCCESS - [ ] **Step 5: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java git commit -m "feat(energy): add 4 energy tables + seed data to DatabaseInitializer" ``` --- ## Task 2: 后端实体类 — 4个Entity **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/EnergyDimension.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/EnergySourceConfig.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/EnergyLog.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/EnergyBalance.java` 遵循现有entity模式:`@Data @TableName @TableId(type=IdType.AUTO) implements Serializable`,字段名用驼峰。 - [ ] **Step 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; private String name; private String icon; private String element; private Integer sortOrder; private Integer status; private Date createdAt; } ``` - [ ] **Step 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; private Long sourceId; private String sourceName; private Long dimensionId; private BigDecimal ratio; private Date createdAt; } ``` - [ ] **Step 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; private Long sourceId; private String description; private Date createdAt; } ``` - [ ] **Step 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; } ``` - [ ] **Step 5: 验证编译** Run: `cd cfc-backend && mvn clean compile -q` Expected: BUILD SUCCESS - [ ] **Step 6: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/entity/Energy*.java git commit -m "feat(energy): add 4 energy entity classes" ``` --- ## Task 3: 后端Mapper接口 — 4个Mapper **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/EnergyDimensionMapper.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/EnergySourceConfigMapper.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/EnergyLogMapper.java` - Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/EnergyBalanceMapper.java` 遵循现有mapper模式:`@Mapper interface XxxMapper extends BaseMapper`,无自定义方法。 - [ ] **Step 1: 创建4个Mapper** ```java // EnergyDimensionMapper.java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.EnergyDimension; import org.apache.ibatis.annotations.Mapper; @Mapper public interface EnergyDimensionMapper extends BaseMapper { } ``` ```java // EnergySourceConfigMapper.java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.EnergySourceConfig; import org.apache.ibatis.annotations.Mapper; @Mapper public interface EnergySourceConfigMapper extends BaseMapper { } ``` ```java // EnergyLogMapper.java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.EnergyLog; import org.apache.ibatis.annotations.Mapper; @Mapper public interface EnergyLogMapper extends BaseMapper { } ``` ```java // EnergyBalanceMapper.java package com.etotem.cfc.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.etotem.cfc.entity.EnergyBalance; import org.apache.ibatis.annotations.Mapper; @Mapper public interface EnergyBalanceMapper extends BaseMapper { } ``` - [ ] **Step 2: 验证编译** Run: `cd cfc-backend && mvn clean compile -q` Expected: BUILD SUCCESS - [ ] **Step 3: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/mapper/Energy*Mapper.java git commit -m "feat(energy): add 4 energy mapper interfaces" ``` --- ## Task 4: 后端核心服务 — EnergyService **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java` 这是能量系统最核心的文件。实现5个核心方法:awardEnergy、deductEnergy、getOverview、getLogs、configureSourceRatios。 - [ ] **Step 1: 创建EnergyService.java** ```java package com.etotem.cfc.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.etotem.cfc.entity.*; import com.etotem.cfc.mapper.*; import javax.annotation.Resource; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.util.*; import java.util.stream.Collectors; @Service public class EnergyService { @Resource private EnergyDimensionMapper dimensionMapper; @Resource private EnergySourceConfigMapper sourceConfigMapper; @Resource private EnergyLogMapper logMapper; @Resource private EnergyBalanceMapper balanceMapper; @Resource private ProductMapper productMapper; // 维度缓存(启动后首次查询缓存,维度数据极少变更) private List dimensionCache; private Map codeToDimension; /** * 发放能量 — 查比例配置,按比例分配到各维度 * @param childId 孩子ID * @param sourceType 来源类型(task/activity/course/product/consultation) * @param sourceId 来源ID * @param totalAmount 总能量值 * @param description 描述 * @return 各维度实际发放量 Map */ @Transactional public Map awardEnergy(Long childId, String sourceType, Long sourceId, Integer totalAmount, String description) { if (totalAmount <= 0) { return new HashMap<>(); } // 1. 查比例配置 List configs = sourceConfigMapper.selectList( new LambdaQueryWrapper() .eq(EnergySourceConfig::getSourceType, sourceType) .eq(EnergySourceConfig::getSourceId, sourceId) ); // 2. Fallback链 Map dimensionRatios = new LinkedHashMap<>(); if (!configs.isEmpty()) { // 有配置,使用配置比例 for (EnergySourceConfig config : configs) { dimensionRatios.put(config.getDimensionId(), config.getRatio()); } } else if ("product".equals(sourceType)) { // Fallback: 查Product.domain dimensionRatios = resolveFromProductDomain(sourceId); } if (dimensionRatios.isEmpty()) { // 最终fallback: 默认分配到"行"维度100% EnergyDimension actionDim = getDimensionByCode("action"); if (actionDim != null) { dimensionRatios.put(actionDim.getId(), BigDecimal.ONE); } } if (dimensionRatios.isEmpty()) { return new HashMap<>(); } // 3. 按比例计算各维度发放量(整数,余数加到最大比例维度) Map allocations = calculateAllocations(totalAmount, dimensionRatios); // 4. 对每个维度:更新余额 + 写流水 Map result = new LinkedHashMap<>(); for (Map.Entry entry : allocations.entrySet()) { Long dimensionId = entry.getKey(); Integer amount = entry.getValue(); if (amount <= 0) continue; // 更新余额 EnergyBalance balance = getOrCreateBalance(childId, dimensionId); balance.setBalance(balance.getBalance() + amount); balance.setTotalEarned(balance.getTotalEarned() + amount); balance.setUpdatedAt(new Date()); balanceMapper.updateById(balance); // 写流水 EnergyLog log = new EnergyLog(); log.setChildId(childId); log.setDimensionId(dimensionId); log.setAmount(amount); log.setBalanceAfter(balance.getBalance()); log.setSourceType(sourceType); log.setSourceId(sourceId); log.setDescription(description); log.setCreatedAt(new Date()); logMapper.insert(log); // 记录返回结果 EnergyDimension dim = getDimensionById(dimensionId); if (dim != null) { result.put(dim.getCode(), amount); } } return result; } /** * 扣除能量 — 指定维度,检查余额 * @return 扣除后余额,-1表示余额不足 */ @Transactional public int deductEnergy(Long childId, Long dimensionId, Integer amount, String reason) { if (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()); balanceMapper.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()); logMapper.insert(log); return balance.getBalance(); } /** * 查询概览 — 5维度能量值+健康指数+总能量+总指数 */ public Map getOverview(Long childId) { List allDimensions = getAllDimensions(); List> dimensionList = new ArrayList<>(); int totalEnergy = 0; for (EnergyDimension dim : allDimensions) { if (dim.getStatus() != 1) continue; EnergyBalance balance = balanceMapper.selectOne( new LambdaQueryWrapper() .eq(EnergyBalance::getChildId, childId) .eq(EnergyBalance::getDimensionId, dim.getId()) ); int energy = (balance != null) ? balance.getBalance() : 0; totalEnergy += energy; Map dimData = new LinkedHashMap<>(); dimData.put("code", dim.getCode()); dimData.put("name", dim.getName()); dimData.put("icon", dim.getIcon()); dimData.put("element", dim.getElement()); dimData.put("energy", energy); dimData.put("healthIndex", 0); // 算法待定,先返回0 dimensionList.add(dimData); } Map result = new LinkedHashMap<>(); result.put("dimensions", dimensionList); result.put("totalEnergy", totalEnergy); result.put("totalHealthIndex", 0); // 算法待定,先返回0 return result; } /** * 查询流水 — 按维度筛选,分页 */ 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 = getDimensionByCode(dimensionCode); if (dim != null) { wrapper.eq(EnergyLog::getDimensionId, dim.getId()); } } wrapper.orderByDesc(EnergyLog::getCreatedAt); return logMapper.selectPage(pageParam, wrapper); } /** * 配置比例 — 为某服务实例设置维度比例 */ @Transactional public void configureSourceRatios(String sourceType, Long sourceId, String sourceName, List> ratios) { // 删除旧配置 sourceConfigMapper.delete( new LambdaQueryWrapper() .eq(EnergySourceConfig::getSourceType, sourceType) .eq(EnergySourceConfig::getSourceId, sourceId) ); // 插入新配置 for (Map ratio : ratios) { String dimensionCode = (String) ratio.get("dimensionCode"); BigDecimal ratioValue = new BigDecimal(ratio.get("ratio").toString()); EnergyDimension dim = getDimensionByCode(dimensionCode); if (dim == null) continue; EnergySourceConfig config = new EnergySourceConfig(); config.setSourceType(sourceType); config.setSourceId(sourceId); config.setSourceName(sourceName); config.setDimensionId(dim.getId()); config.setRatio(ratioValue); config.setCreatedAt(new Date()); sourceConfigMapper.insert(config); } } // ==================== 内部辅助方法 ==================== private EnergyBalance getOrCreateBalance(Long childId, Long dimensionId) { EnergyBalance balance = balanceMapper.selectOne( new LambdaQueryWrapper() .eq(EnergyBalance::getChildId, childId) .eq(EnergyBalance::getDimensionId, dimensionId) ); if (balance == null) { balance = new EnergyBalance(); balance.setChildId(childId); balance.setDimensionId(dimensionId); balance.setBalance(0); balance.setTotalEarned(0); balance.setTotalSpent(0); balance.setUpdatedAt(new Date()); balanceMapper.insert(balance); } return balance; } private Map resolveFromProductDomain(Long productId) { Map result = new LinkedHashMap<>(); try { Product product = productMapper.selectById(productId); if (product != null && product.getDomain() != null && !product.getDomain().isEmpty()) { EnergyDimension dim = getDimensionByCode(product.getDomain()); if (dim != null) { result.put(dim.getId(), BigDecimal.ONE); } } } catch (Exception e) { // Product可能不存在,忽略 } return result; } /** * 按比例计算各维度分配量(整数分配,余数加到最大比例维度) */ private Map calculateAllocations(Integer totalAmount, Map dimensionRatios) { Map allocations = new LinkedHashMap<>(); int allocated = 0; Long maxRatioDimId = null; BigDecimal maxRatio = BigDecimal.ZERO; for (Map.Entry entry : dimensionRatios.entrySet()) { int amount = entry.getValue().multiply(new BigDecimal(totalAmount)) .setScale(0, BigDecimal.ROUND_DOWN).intValue(); allocations.put(entry.getKey(), amount); allocated += amount; if (entry.getValue().compareTo(maxRatio) > 0) { maxRatio = entry.getValue(); maxRatioDimId = entry.getKey(); } } // 余数加到最大比例维度 int remainder = totalAmount - allocated; if (remainder > 0 && maxRatioDimId != null) { allocations.put(maxRatioDimId, allocations.get(maxRatioDimId) + remainder); } return allocations; } private List getAllDimensions() { if (dimensionCache == null) { dimensionCache = dimensionMapper.selectList( new LambdaQueryWrapper().orderByAsc(EnergyDimension::getSortOrder) ); codeToDimension = new HashMap<>(); for (EnergyDimension dim : dimensionCache) { codeToDimension.put(dim.getCode(), dim); } } return dimensionCache; } private EnergyDimension getDimensionByCode(String code) { getAllDimensions(); // 确保缓存加载 return codeToDimension.get(code); } private EnergyDimension getDimensionById(Long id) { getAllDimensions(); for (EnergyDimension dim : dimensionCache) { if (dim.getId().equals(id)) { return dim; } } return null; } } ``` - [ ] **Step 2: 验证编译** Run: `cd cfc-backend && mvn clean compile -q` Expected: BUILD SUCCESS - [ ] **Step 3: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java git commit -m "feat(energy): add EnergyService with award/deduct/overview/logs/configure" ``` --- ## Task 5: 后端API入口 — EnergyController **Files:** - Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/energy/EnergyController.java` 遵循现有controller模式:`@RestController @RequestMapping`,统一`@PostMapping`,`@Resource`注入Service,`Result`响应。 - [ ] **Step 1: 创建EnergyController.java** ```java package com.etotem.cfc.controller.energy; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.etotem.cfc.common.Result; import com.etotem.cfc.entity.EnergyLog; import com.etotem.cfc.service.EnergyService; import javax.annotation.Resource; import org.springframework.web.bind.annotation.*; import java.util.Map; @RestController @RequestMapping("/api/energy") public class EnergyController { @Resource private EnergyService energyService; @PostMapping("/overview") public Result> getOverview(@RequestBody Map params) { Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null; if (childId == null) { return Result.error("childId不能为空"); } Map overview = energyService.getOverview(childId); return Result.success(overview); } @PostMapping("/logs") public Result> getLogs(@RequestBody Map params) { Long childId = params.get("childId") != null ? Long.valueOf(params.get("childId").toString()) : null; String dimensionCode = params.get("dimensionCode") != null ? params.get("dimensionCode").toString() : null; Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1; Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10; if (childId == null) { return Result.error("childId不能为空"); } Page logs = energyService.getLogs(childId, dimensionCode, page, size); return Result.success(logs); } @PostMapping("/configure") public Result configureSourceRatios(@RequestBody Map params, @RequestAttribute("userId") Long userId) { String sourceType = (String) params.get("sourceType"); Long sourceId = Long.valueOf(params.get("sourceId").toString()); String sourceName = (String) params.get("sourceName"); java.util.List> ratios = (java.util.List>) params.get("ratios"); if (sourceType == null || sourceId == null || ratios == null || ratios.isEmpty()) { return Result.error("参数不完整"); } energyService.configureSourceRatios(sourceType, sourceId, sourceName, ratios); return Result.success("配置成功"); } } ``` - [ ] **Step 2: 验证编译** Run: `cd cfc-backend && mvn clean compile -q` Expected: BUILD SUCCESS - [ ] **Step 3: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/controller/energy/EnergyController.java git commit -m "feat(energy): add EnergyController with overview/logs/configure endpoints" ``` --- ## Task 6: 集成到TaskService — 完成任务时发放能量 **Files:** - Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java` 在`completeTask()`方法的`pointsLogMapper.insert(pointsLog)`之后、`Map result`之前,添加一行energyService调用。 - [ ] **Step 1: 在TaskService中注入EnergyService** 在现有`@Resource`注入区域添加: ```java @Resource private EnergyService energyService; ``` - [ ] **Step 2: 在completeTask()末尾添加energy发放调用** 在 `pointsLogMapper.insert(pointsLog);` 之后,`Map result = new HashMap<>();` 之前,添加: ```java // 五维能量发放(与积分并行,独立系统) try { int energyAmount = Math.abs(pointsEarned); // 能量值为正数 if (energyAmount > 0) { energyService.awardEnergy(childId, "task", taskId, energyAmount, "完成任务: " + task.getTitle()); } } catch (Exception e) { log.warn("能量发放失败(不影响积分): childId={}, taskId={}, error={}", childId, taskId, e.getMessage()); } ``` 注意:用try-catch包裹,能量发放失败不影响积分逻辑(解耦原则)。 - [ ] **Step 3: 验证编译** Run: `cd cfc-backend && mvn clean compile -q` Expected: BUILD SUCCESS - [ ] **Step 4: Commit** ```bash git add cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java git commit -m "feat(energy): integrate energy award into TaskService.completeTask()" ``` --- ## Task 7: 前端API封装 — api.js添加能量接口 **Files:** - Modify: `cfc-frontend/utils/api.js` 在文件末尾添加能量系统两个API调用。 - [ ] **Step 1: 在api.js末尾添加能量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 }) } ``` - [ ] **Step 2: Commit** ```bash git add cfc-frontend/utils/api.js git commit -m "feat(energy): add energy API calls to frontend api.js" ``` --- ## Task 8: 前端wuxing-sandbox组件重写 **Files:** - Modify: `cfc-frontend/components/wuxing-sandbox.vue` 完全重写组件,接收真实数据props,实现三层视觉结构(外层维度角、中层健康指数扇区、内层中心圆)。 - [ ] **Step 1: 重写wuxing-sandbox.vue** ```vue ``` 关键设计决策: - Vue 2语法(不用可选链`?.`,用条件表达式代替) - `sortedDimensions` computed按spec顺序排列 - 三层视觉结构:外层角(能量值)、中层扇区(健康指数)、内层圆(总能量+总指数) - 向后兼容:保留`badgeText`和`mode` props,新增`dimensions`/`totalEnergy`/`totalHealthIndex` - 无数据时用DEFAULT_DIMS占位 - [ ] **Step 2: Commit** ```bash git add cfc-frontend/components/wuxing-sandbox.vue git commit -m "feat(energy): rewrite wuxing-sandbox with real data props and 3-layer visual structure" ``` --- ## Task 9: 前端首页集成 — parent-index和child-index传入真实数据 **Files:** - Modify: `cfc-frontend/pages/index/parent-index.vue` - Modify: `cfc-frontend/pages/index/child-index.vue` 在两个首页的`onShow`或`onLoad`生命周期中调用`getEnergyOverview`API,将结果传给wuxing-sandbox组件。 - [ ] **Step 1: 修改parent-index.vue** **import区域**:添加能量API导入 在现有 `import WuxingSandbox from ...` 之后添加: ```js import { getEnergyOverview } from '../../utils/api.js' ``` **data区域**:添加能量数据字段 ```js energyDimensions: [], totalEnergy: 0, totalHealthIndex: 0, ``` **methods区域**:添加加载能量数据方法 ```js loadEnergyOverview: function () { var that = this var childId = that.currentChildId // 使用页面现有的childId 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生命周期**:调用加载 在现有 `onShow()` 方法末尾添加 `this.loadEnergyOverview()` **template区域**:修改wuxing-sandbox调用 将: ```html ``` 改为: ```html ``` - [ ] **Step 2: 修改child-index.vue** 同样模式: **import区域**添加: ```js import { getEnergyOverview } from '../../utils/api.js' ``` **data区域**添加: ```js energyDimensions: [], totalEnergy: 0, totalHealthIndex: 0, ``` **methods区域**添加: ```js loadEnergyOverview: function () { var that = this var childId = that.childId // 使用页面现有的childId 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区域**将: ```html ``` 改为: ```html ``` - [ ] **Step 3: Commit** ```bash git add cfc-frontend/pages/index/parent-index.vue cfc-frontend/pages/index/child-index.vue git commit -m "feat(energy): integrate energy overview API into parent and child index pages" ``` --- ## Task 10: 前端能量详情页 + pages.json注册 **Files:** - Create: `cfc-frontend/pages/energy/detail.vue` - Modify: `cfc-frontend/pages.json` 创建维度详情页:显示维度名称/图标/能量值/健康指数,下方展示流水记录。从wuxing-sandbox点击维度时navigateTo进入。 - [ ] **Step 1: 创建 pages/energy/detail.vue** ```vue ``` - [ ] **Step 2: 在pages.json中注册新页面** 在pages数组的 `pages/vendor/orders/orders` 条目之后添加: ```json { "path": "pages/energy/detail", "style": { "navigationBarTitleText": "维度详情" } } ``` - [ ] **Step 3: 在parent-index.vue和child-index.vue的goToDomain方法中添加导航** 在两个页面的 `goToDomain` 方法中,添加导航到能量详情页: ```js goToDomain: function (code) { var dimData = {} for (var i = 0; i < this.energyDimensions.length; i++) { if (this.energyDimensions[i].code === code) { dimData = this.energyDimensions[i] break } } uni.navigateTo({ url: '/pages/energy/detail?code=' + code + '&childId=' + (this.currentChildId || this.childId) + '&dimData=' + encodeURIComponent(JSON.stringify(dimData)) }) } ``` 注意:parent-index使用`this.currentChildId`,child-index使用`this.childId`,请根据实际页面变量名调整。 - [ ] **Step 4: Commit** ```bash git add cfc-frontend/pages/energy/detail.vue cfc-frontend/pages.json cfc-frontend/pages/index/parent-index.vue cfc-frontend/pages/index/child-index.vue git commit -m "feat(energy): add energy detail page + register in pages.json + add navigation" ``` --- ## Task 11: discover页预览模式更新 **Files:** - Modify: `cfc-frontend/pages/discover/index.vue` discover页保持preview模式,不需要调用API,但需要传入空dimensions数组保证组件正常渲染。 - [ ] **Step 1: 更新discover/index.vue中的wuxing-sandbox调用** 将: ```html ``` 改为: ```html ``` 空数组会触发组件内部的DEFAULT_DIMS fallback,显示图标和名称但能量值不显示。 - [ ] **Step 2: Commit** ```bash git add cfc-frontend/pages/discover/index.vue git commit -m "feat(energy): update discover page with empty energy props for preview mode" ``` --- ## Task 12: 后端编译验证 + 前端基础检查 **Files:** 无修改,纯验证 - [ ] **Step 1: 后端完整编译** Run: `cd cfc-backend && mvn clean compile -q` Expected: BUILD SUCCESS - [ ] **Step 2: 检查LSP诊断** Run: `lsp_diagnostics` on: - `cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java` - `cfc-backend/src/main/java/com/etotem/cfc/controller/energy/EnergyController.java` - `cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java` Expected: 无新增error - [ ] **Step 3: 如果有诊断问题,修复后再次验证** --- ## Self-Review Checklist **1. Spec Coverage:** - ✅ 4张表DDL (energy_dimension, energy_source_config, energy_log, energy_balance) → Task 1 - ✅ 维度种子数据(5条) → Task 1 - ✅ Product.domain→energy_source_config迁移 → Task 1 - ✅ 4个Entity → Task 2 - ✅ 4个Mapper → Task 3 - ✅ EnergyService 5个方法 → Task 4 - ✅ awardEnergy Fallback链(sourceConfig→Product.domain→行100%) → Task 4 - ✅ EnergyController 3个API → Task 5 - ✅ TaskService.completeTask()集成 → Task 6 - ✅ 前端api.js → Task 7 - ✅ wuxing-sandbox重写(三层视觉) → Task 8 - ✅ parent-index/child-index数据集成 → Task 9 - ✅ 能量详情页 → Task 10 - ✅ discover预览模式 → Task 11 - ✅ 编译验证 → Task 12 **2. Placeholder Scan:** - 无TBD/TODO/占位符 - 健康指数返回0是spec明确要求,不是占位符 **3. Type Consistency:** - Entity字段名与DDL列名一致(MyBatis-Plus驼峰映射) - EnergyService方法签名与Controller调用一致 - 前端props名称(dimensions/totalEnergy/totalHealthIndex)与组件定义和调用一致 - API路径 `/api/energy/overview` 和 `/api/energy/logs` 前后端一致 **4. Gaps Found:** - EnergyLog实体缺少`dimensionCode`字段便于前端展示 → 在getLogs API响应中通过dimensionId关联。前端detail页已知dimensionCode(从URL参数传入),无需在log中冗余。此为合理设计,不需要改动。