瀏覽代碼

docs(energy): add five-dimension energy system design spec and implementation plan

- 设计规范:4张表DDL + 后端架构 + 前端可视化 + DB迁移
- 实施计划:12个任务,从DB迁移到前端集成完整流程
- 健康指数算法待定,先返回0占位
User 3 月之前
父節點
當前提交
d636093b3d

+ 1703 - 0
docs/superpowers/plans/2026-06-05-five-dimension-energy-system.md

@@ -0,0 +1,1703 @@
+# 五维能量系统实施计划
+
+> **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
+
+### 后端新建文件
+| 文件 | 职责 |
+|------|------|
+| `zxyj-backend/src/main/java/com/zxyj/entity/EnergyDimension.java` | 维度定义实体 |
+| `zxyj-backend/src/main/java/com/zxyj/entity/EnergySourceConfig.java` | 服务-维度比例配置实体 |
+| `zxyj-backend/src/main/java/com/zxyj/entity/EnergyLog.java` | 能量流水实体 |
+| `zxyj-backend/src/main/java/com/zxyj/entity/EnergyBalance.java` | 维度余额实体 |
+| `zxyj-backend/src/main/java/com/zxyj/mapper/EnergyDimensionMapper.java` | 维度CRUD |
+| `zxyj-backend/src/main/java/com/zxyj/mapper/EnergySourceConfigMapper.java` | 比例配置CRUD |
+| `zxyj-backend/src/main/java/com/zxyj/mapper/EnergyLogMapper.java` | 流水查询 |
+| `zxyj-backend/src/main/java/com/zxyj/mapper/EnergyBalanceMapper.java` | 余额查询/更新 |
+| `zxyj-backend/src/main/java/com/zxyj/service/EnergyService.java` | 核心业务:发放/扣除/查概览/查流水/配比例 |
+| `zxyj-backend/src/main/java/com/zxyj/controller/energy/EnergyController.java` | API入口 |
+
+### 后端修改文件
+| 文件 | 修改内容 |
+|------|----------|
+| `zxyj-backend/src/main/java/com/zxyj/config/DatabaseInitializer.java` | 添加4张表DDL + 种子数据 + 比例配置迁移 |
+| `zxyj-backend/src/main/java/com/zxyj/service/TaskService.java` | completeTask()末尾添加energyService.awardEnergy()调用 |
+
+### 前端修改文件
+| 文件 | 修改内容 |
+|------|----------|
+| `zxyj-frontend/components/wuxing-sandbox.vue` | 完全重写:接收dimensions/totalEnergy/totalHealthIndex props,显示真实数据,三层视觉结构 |
+| `zxyj-frontend/utils/api.js` | 添加getEnergyOverview/getEnergyLogs |
+| `zxyj-frontend/pages/index/parent-index.vue` | 传入API获取的dimensions/totalEnergy/totalHealthIndex |
+| `zxyj-frontend/pages/index/child-index.vue` | 同上 |
+| `zxyj-frontend/pages.json` | 注册energy/detail页面 |
+
+### 前端新建文件
+| 文件 | 职责 |
+|------|------|
+| `zxyj-frontend/pages/energy/detail.vue` | 维度详情页:能量值、流水记录 |
+
+---
+
+## Task 1: 数据库迁移 — 4张新表 + 种子数据
+
+**Files:**
+- Modify: `zxyj-backend/src/main/java/com/zxyj/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 zxyj-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add zxyj-backend/src/main/java/com/zxyj/config/DatabaseInitializer.java
+git commit -m "feat(energy): add 4 energy tables + seed data to DatabaseInitializer"
+```
+
+---
+
+## Task 2: 后端实体类 — 4个Entity
+
+**Files:**
+- Create: `zxyj-backend/src/main/java/com/zxyj/entity/EnergyDimension.java`
+- Create: `zxyj-backend/src/main/java/com/zxyj/entity/EnergySourceConfig.java`
+- Create: `zxyj-backend/src/main/java/com/zxyj/entity/EnergyLog.java`
+- Create: `zxyj-backend/src/main/java/com/zxyj/entity/EnergyBalance.java`
+
+遵循现有entity模式:`@Data @TableName @TableId(type=IdType.AUTO) implements Serializable`,字段名用驼峰。
+
+- [ ] **Step 1: 创建 EnergyDimension.java**
+
+```java
+package com.zxyj.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.zxyj.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.zxyj.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.zxyj.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 zxyj-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add zxyj-backend/src/main/java/com/zxyj/entity/Energy*.java
+git commit -m "feat(energy): add 4 energy entity classes"
+```
+
+---
+
+## Task 3: 后端Mapper接口 — 4个Mapper
+
+**Files:**
+- Create: `zxyj-backend/src/main/java/com/zxyj/mapper/EnergyDimensionMapper.java`
+- Create: `zxyj-backend/src/main/java/com/zxyj/mapper/EnergySourceConfigMapper.java`
+- Create: `zxyj-backend/src/main/java/com/zxyj/mapper/EnergyLogMapper.java`
+- Create: `zxyj-backend/src/main/java/com/zxyj/mapper/EnergyBalanceMapper.java`
+
+遵循现有mapper模式:`@Mapper interface XxxMapper extends BaseMapper<Xxx>`,无自定义方法。
+
+- [ ] **Step 1: 创建4个Mapper**
+
+```java
+// EnergyDimensionMapper.java
+package com.zxyj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zxyj.entity.EnergyDimension;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface EnergyDimensionMapper extends BaseMapper<EnergyDimension> {
+}
+```
+
+```java
+// EnergySourceConfigMapper.java
+package com.zxyj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zxyj.entity.EnergySourceConfig;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface EnergySourceConfigMapper extends BaseMapper<EnergySourceConfig> {
+}
+```
+
+```java
+// EnergyLogMapper.java
+package com.zxyj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zxyj.entity.EnergyLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface EnergyLogMapper extends BaseMapper<EnergyLog> {
+}
+```
+
+```java
+// EnergyBalanceMapper.java
+package com.zxyj.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.zxyj.entity.EnergyBalance;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface EnergyBalanceMapper extends BaseMapper<EnergyBalance> {
+}
+```
+
+- [ ] **Step 2: 验证编译**
+
+Run: `cd zxyj-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add zxyj-backend/src/main/java/com/zxyj/mapper/Energy*Mapper.java
+git commit -m "feat(energy): add 4 energy mapper interfaces"
+```
+
+---
+
+## Task 4: 后端核心服务 — EnergyService
+
+**Files:**
+- Create: `zxyj-backend/src/main/java/com/zxyj/service/EnergyService.java`
+
+这是能量系统最核心的文件。实现5个核心方法:awardEnergy、deductEnergy、getOverview、getLogs、configureSourceRatios。
+
+- [ ] **Step 1: 创建EnergyService.java**
+
+```java
+package com.zxyj.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zxyj.entity.*;
+import com.zxyj.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<EnergyDimension> dimensionCache;
+    private Map<String, EnergyDimension> codeToDimension;
+
+    /**
+     * 发放能量 — 查比例配置,按比例分配到各维度
+     * @param childId 孩子ID
+     * @param sourceType 来源类型(task/activity/course/product/consultation)
+     * @param sourceId 来源ID
+     * @param totalAmount 总能量值
+     * @param description 描述
+     * @return 各维度实际发放量 Map<dimensionCode, amount>
+     */
+    @Transactional
+    public Map<String, Integer> awardEnergy(Long childId, String sourceType, Long sourceId,
+                                             Integer totalAmount, String description) {
+        if (totalAmount <= 0) {
+            return new HashMap<>();
+        }
+
+        // 1. 查比例配置
+        List<EnergySourceConfig> configs = sourceConfigMapper.selectList(
+            new LambdaQueryWrapper<EnergySourceConfig>()
+                .eq(EnergySourceConfig::getSourceType, sourceType)
+                .eq(EnergySourceConfig::getSourceId, sourceId)
+        );
+
+        // 2. Fallback链
+        Map<Long, BigDecimal> 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<Long, Integer> allocations = calculateAllocations(totalAmount, dimensionRatios);
+
+        // 4. 对每个维度:更新余额 + 写流水
+        Map<String, Integer> result = new LinkedHashMap<>();
+        for (Map.Entry<Long, Integer> 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<String, Object> getOverview(Long childId) {
+        List<EnergyDimension> allDimensions = getAllDimensions();
+
+        List<Map<String, Object>> dimensionList = new ArrayList<>();
+        int totalEnergy = 0;
+
+        for (EnergyDimension dim : allDimensions) {
+            if (dim.getStatus() != 1) continue;
+
+            EnergyBalance balance = balanceMapper.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> 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<String, Object> result = new LinkedHashMap<>();
+        result.put("dimensions", dimensionList);
+        result.put("totalEnergy", totalEnergy);
+        result.put("totalHealthIndex", 0); // 算法待定,先返回0
+        return result;
+    }
+
+    /**
+     * 查询流水 — 按维度筛选,分页
+     */
+    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 = 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<Map<String, Object>> ratios) {
+        // 删除旧配置
+        sourceConfigMapper.delete(
+            new LambdaQueryWrapper<EnergySourceConfig>()
+                .eq(EnergySourceConfig::getSourceType, sourceType)
+                .eq(EnergySourceConfig::getSourceId, sourceId)
+        );
+
+        // 插入新配置
+        for (Map<String, Object> 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<EnergyBalance>()
+                .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<Long, BigDecimal> resolveFromProductDomain(Long productId) {
+        Map<Long, BigDecimal> 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<Long, Integer> calculateAllocations(Integer totalAmount, Map<Long, BigDecimal> dimensionRatios) {
+        Map<Long, Integer> allocations = new LinkedHashMap<>();
+        int allocated = 0;
+        Long maxRatioDimId = null;
+        BigDecimal maxRatio = BigDecimal.ZERO;
+
+        for (Map.Entry<Long, BigDecimal> 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<EnergyDimension> getAllDimensions() {
+        if (dimensionCache == null) {
+            dimensionCache = dimensionMapper.selectList(
+                new LambdaQueryWrapper<EnergyDimension>().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 zxyj-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add zxyj-backend/src/main/java/com/zxyj/service/EnergyService.java
+git commit -m "feat(energy): add EnergyService with award/deduct/overview/logs/configure"
+```
+
+---
+
+## Task 5: 后端API入口 — EnergyController
+
+**Files:**
+- Create: `zxyj-backend/src/main/java/com/zxyj/controller/energy/EnergyController.java`
+
+遵循现有controller模式:`@RestController @RequestMapping`,统一`@PostMapping`,`@Resource`注入Service,`Result<T>`响应。
+
+- [ ] **Step 1: 创建EnergyController.java**
+
+```java
+package com.zxyj.controller.energy;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.zxyj.common.Result;
+import com.zxyj.entity.EnergyLog;
+import com.zxyj.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<Map<String, Object>> getOverview(@RequestBody Map<String, Object> params) {
+        Long childId = params.get("childId") != null
+            ? Long.valueOf(params.get("childId").toString()) : null;
+        if (childId == null) {
+            return Result.error("childId不能为空");
+        }
+        Map<String, Object> overview = energyService.getOverview(childId);
+        return Result.success(overview);
+    }
+
+    @PostMapping("/logs")
+    public Result<Page<EnergyLog>> getLogs(@RequestBody Map<String, Object> 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<EnergyLog> logs = energyService.getLogs(childId, dimensionCode, page, size);
+        return Result.success(logs);
+    }
+
+    @PostMapping("/configure")
+    public Result<String> configureSourceRatios(@RequestBody Map<String, Object> 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<Map<String, Object>> ratios =
+            (java.util.List<Map<String, Object>>) 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 zxyj-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add zxyj-backend/src/main/java/com/zxyj/controller/energy/EnergyController.java
+git commit -m "feat(energy): add EnergyController with overview/logs/configure endpoints"
+```
+
+---
+
+## Task 6: 集成到TaskService — 完成任务时发放能量
+
+**Files:**
+- Modify: `zxyj-backend/src/main/java/com/zxyj/service/TaskService.java`
+
+在`completeTask()`方法的`pointsLogMapper.insert(pointsLog)`之后、`Map<String, Object> result`之前,添加一行energyService调用。
+
+- [ ] **Step 1: 在TaskService中注入EnergyService**
+
+在现有`@Resource`注入区域添加:
+
+```java
+@Resource
+private EnergyService energyService;
+```
+
+- [ ] **Step 2: 在completeTask()末尾添加energy发放调用**
+
+在 `pointsLogMapper.insert(pointsLog);` 之后,`Map<String, Object> 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 zxyj-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add zxyj-backend/src/main/java/com/zxyj/service/TaskService.java
+git commit -m "feat(energy): integrate energy award into TaskService.completeTask()"
+```
+
+---
+
+## Task 7: 前端API封装 — api.js添加能量接口
+
+**Files:**
+- Modify: `zxyj-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 zxyj-frontend/utils/api.js
+git commit -m "feat(energy): add energy API calls to frontend api.js"
+```
+
+---
+
+## Task 8: 前端wuxing-sandbox组件重写
+
+**Files:**
+- Modify: `zxyj-frontend/components/wuxing-sandbox.vue`
+
+完全重写组件,接收真实数据props,实现三层视觉结构(外层维度角、中层健康指数扇区、内层中心圆)。
+
+- [ ] **Step 1: 重写wuxing-sandbox.vue**
+
+```vue
+<template>
+  <view class="wuxing-sandbox">
+    <view class="sandbox-header">
+      <text class="sandbox-title">五行能量沙盘</text>
+      <text class="sandbox-badge">{{ badgeText }}</text>
+    </view>
+    <view class="sandbox-star">
+      <!-- 顶: 心·火 -->
+      <view class="star-point star-point-top" @click="onPointClick(0)">
+        <text class="point-icon">{{ getDimIcon(0) }}</text>
+        <text class="point-label">{{ getDimLabel(0) }}</text>
+        <text class="point-energy" v-if="mode !== 'preview'">{{ getDimEnergy(0) }}</text>
+        <text class="point-status" v-if="mode === 'preview'">点此登录</text>
+      </view>
+      <!-- 左上: 行·木 -->
+      <view class="star-point star-point-left" @click="onPointClick(1)">
+        <text class="point-icon">{{ getDimIcon(1) }}</text>
+        <text class="point-label">{{ getDimLabel(1) }}</text>
+        <text class="point-energy" v-if="mode !== 'preview'">{{ getDimEnergy(1) }}</text>
+        <text class="point-status" v-if="mode === 'preview'">点此登录</text>
+      </view>
+      <!-- 右上: 富·水 -->
+      <view class="star-point star-point-right" @click="onPointClick(2)">
+        <text class="point-icon">{{ getDimIcon(2) }}</text>
+        <text class="point-label">{{ getDimLabel(2) }}</text>
+        <text class="point-energy" v-if="mode !== 'preview'">{{ getDimEnergy(2) }}</text>
+        <text class="point-status" v-if="mode === 'preview'">点此登录</text>
+      </view>
+      <!-- 左下: 智·金 -->
+      <view class="star-point star-point-bl" @click="onPointClick(3)">
+        <text class="point-icon">{{ getDimIcon(3) }}</text>
+        <text class="point-label">{{ getDimLabel(3) }}</text>
+        <text class="point-energy" v-if="mode !== 'preview'">{{ getDimEnergy(3) }}</text>
+        <text class="point-status" v-if="mode === 'preview'">点此登录</text>
+      </view>
+      <!-- 右下: 身·土 -->
+      <view class="star-point star-point-br" @click="onPointClick(4)">
+        <text class="point-icon">{{ getDimIcon(4) }}</text>
+        <text class="point-label">{{ getDimLabel(4) }}</text>
+        <text class="point-energy" v-if="mode !== 'preview'">{{ getDimEnergy(4) }}</text>
+        <text class="point-status" v-if="mode === 'preview'">点此登录</text>
+      </view>
+
+      <!-- 中层: 健康指数扇区(角底边→中心的三角形区域外围) -->
+      <view class="health-sector health-top" v-if="mode !== 'preview'">
+        <text class="health-index">{{ getHealthIndex(0) }}</text>
+      </view>
+      <view class="health-sector health-left" v-if="mode !== 'preview'">
+        <text class="health-index">{{ getHealthIndex(1) }}</text>
+      </view>
+      <view class="health-sector health-right" v-if="mode !== 'preview'">
+        <text class="health-index">{{ getHealthIndex(2) }}</text>
+      </view>
+      <view class="health-sector health-bl" v-if="mode !== 'preview'">
+        <text class="health-index">{{ getHealthIndex(3) }}</text>
+      </view>
+      <view class="health-sector health-br" v-if="mode !== 'preview'">
+        <text class="health-index">{{ getHealthIndex(4) }}</text>
+      </view>
+
+      <!-- 内层: 中心圆 -->
+      <view class="star-center">
+        <text class="center-energy" v-if="mode !== 'preview'">{{ totalEnergy }}</text>
+        <text class="center-text" v-else>五行平衡</text>
+        <text class="center-index" v-if="mode !== 'preview'">指数 {{ totalHealthIndex }}</text>
+        <text class="center-sub" v-else>共同成长</text>
+      </view>
+    </view>
+    <view class="shenke-hint" v-if="mode === 'preview'">
+      <text class="hint-text">🌱 完成测评,点亮「身·土」</text>
+    </view>
+    <!-- 维度能量条 -->
+    <view class="dimension-bar" v-if="mode !== 'preview'">
+      <view class="dim-item" v-for="(dim, idx) in dimensions" :key="idx"
+            :class="{ inactive: !dim || dim.energy === 0 }">
+        {{ dim ? dim.icon : '' }}{{ dim ? dim.name : '' }}
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+// 默认维度数据(预览模式/无数据时使用)
+var DEFAULT_DIMS = [
+  { code: 'mind', name: '心', icon: '🔥', element: '火', energy: 0, healthIndex: 0 },
+  { code: 'action', name: '行', icon: '🌿', element: '木', energy: 0, healthIndex: 0 },
+  { code: 'wealth', name: '富', icon: '💧', element: '水', energy: 0, healthIndex: 0 },
+  { code: 'wisdom', name: '智', icon: '⚔️', element: '金', energy: 0, healthIndex: 0 },
+  { code: 'body', name: '身', icon: '🌏', element: '土', energy: 0, healthIndex: 0 }
+]
+
+export default {
+  props: {
+    mode: {
+      type: String,
+      default: 'preview'
+    },
+    badgeText: {
+      type: String,
+      default: '综合成长力 0%'
+    },
+    dimensions: {
+      type: Array,
+      default: function () { return [] }
+    },
+    totalEnergy: {
+      type: Number,
+      default: 0
+    },
+    totalHealthIndex: {
+      type: Number,
+      default: 0
+    }
+  },
+  computed: {
+    sortedDimensions: function () {
+      if (!this.dimensions || this.dimensions.length === 0) {
+        return DEFAULT_DIMS
+      }
+      // 按spec规定的顺序排列: 心/行/富/智/身
+      var order = ['mind', 'action', 'wealth', 'wisdom', 'body']
+      var dimMap = {}
+      var i
+      for (i = 0; i < this.dimensions.length; i++) {
+        dimMap[this.dimensions[i].code] = this.dimensions[i]
+      }
+      var result = []
+      for (i = 0; i < order.length; i++) {
+        if (dimMap[order[i]]) {
+          result.push(dimMap[order[i]])
+        } else {
+          var def = DEFAULT_DIMS[i]
+          result.push(def)
+        }
+      }
+      return result
+    }
+  },
+  methods: {
+    getDimIcon: function (idx) {
+      var dim = this.sortedDimensions[idx]
+      return dim ? dim.icon : ''
+    },
+    getDimLabel: function (idx) {
+      var dim = this.sortedDimensions[idx]
+      return dim ? (dim.name + '·' + (dim.element || '')) : ''
+    },
+    getDimEnergy: function (idx) {
+      var dim = this.sortedDimensions[idx]
+      return dim ? dim.energy : 0
+    },
+    getHealthIndex: function (idx) {
+      var dim = this.sortedDimensions[idx]
+      return dim ? dim.healthIndex : 0
+    },
+    onPointClick: function (idx) {
+      var dim = this.sortedDimensions[idx]
+      if (dim) {
+        this.$emit('point-click', dim.code)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.wuxing-sandbox {
+  margin: 20rpx 30rpx;
+  padding: 30rpx;
+  background: linear-gradient(135deg, #1a1a2e, #16213e);
+  border-radius: 24rpx;
+  position: relative;
+  overflow: hidden;
+}
+.sandbox-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 30rpx;
+}
+.sandbox-title {
+  font-size: 28rpx;
+  font-weight: bold;
+  color: #FFD700;
+}
+.sandbox-badge {
+  font-size: 22rpx;
+  color: #aaa;
+  background: rgba(255,255,255,0.1);
+  padding: 6rpx 16rpx;
+  border-radius: 20rpx;
+}
+.sandbox-star {
+  position: relative;
+  height: 440rpx;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+.star-point {
+  position: absolute;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  width: 120rpx;
+  z-index: 2;
+}
+.star-point-top { top: 0; left: 50%; transform: translateX(-50%); }
+.star-point-left { top: 120rpx; left: 20rpx; }
+.star-point-right { top: 120rpx; right: 20rpx; }
+.star-point-bl { bottom: 40rpx; left: 60rpx; }
+.star-point-br { bottom: 40rpx; right: 60rpx; }
+
+.point-icon { font-size: 50rpx; margin-bottom: 6rpx; }
+.point-label { font-size: 22rpx; color: #fff; font-weight: bold; }
+.point-energy {
+  font-size: 28rpx;
+  color: #FFD700;
+  font-weight: bold;
+  margin-top: 4rpx;
+}
+.point-status { font-size: 18rpx; color: #FF6B6B; margin-top: 4rpx; }
+
+/* 健康指数扇区 */
+.health-sector {
+  position: absolute;
+  z-index: 1;
+}
+.health-index {
+  font-size: 20rpx;
+  color: rgba(255,215,0,0.6);
+  background: rgba(255,215,0,0.08);
+  padding: 4rpx 10rpx;
+  border-radius: 10rpx;
+}
+.health-top { top: 70rpx; left: 50%; transform: translateX(-50%); }
+.health-left { top: 180rpx; left: 80rpx; }
+.health-right { top: 180rpx; right: 80rpx; }
+.health-bl { bottom: 80rpx; left: 120rpx; }
+.health-br { bottom: 80rpx; right: 120rpx; }
+
+/* 中心圆 */
+.star-center {
+  width: 140rpx;
+  height: 140rpx;
+  background: radial-gradient(circle, rgba(255,215,0,0.2), rgba(255,215,0,0.05));
+  border: 2rpx solid rgba(255,215,0,0.3);
+  border-radius: 50%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+  align-items: center;
+  z-index: 3;
+}
+.center-energy {
+  font-size: 32rpx;
+  color: #FFD700;
+  font-weight: bold;
+}
+.center-text {
+  font-size: 22rpx;
+  color: #FFD700;
+  font-weight: bold;
+}
+.center-index {
+  font-size: 18rpx;
+  color: #aaa;
+}
+.center-sub {
+  font-size: 18rpx;
+  color: #aaa;
+}
+
+.shenke-hint { text-align: center; margin-top: 10rpx; }
+.hint-text { font-size: 22rpx; color: rgba(255,215,0,0.7); }
+
+.dimension-bar {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  margin-top: 20rpx;
+  gap: 8rpx;
+  flex-wrap: wrap;
+}
+.dim-item {
+  font-size: 22rpx;
+  color: #FFD700;
+  background: rgba(255,215,0,0.15);
+  padding: 6rpx 14rpx;
+  border-radius: 12rpx;
+}
+.dim-item.inactive {
+  color: #666;
+  background: rgba(255,255,255,0.05);
+}
+</style>
+```
+
+关键设计决策:
+- Vue 2语法(不用可选链`?.`,用条件表达式代替)
+- `sortedDimensions` computed按spec顺序排列
+- 三层视觉结构:外层角(能量值)、中层扇区(健康指数)、内层圆(总能量+总指数)
+- 向后兼容:保留`badgeText`和`mode` props,新增`dimensions`/`totalEnergy`/`totalHealthIndex`
+- 无数据时用DEFAULT_DIMS占位
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add zxyj-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: `zxyj-frontend/pages/index/parent-index.vue`
+- Modify: `zxyj-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
+<wuxing-sandbox mode="parent" badgeText="综合成长力 0%" @point-click="goToDomain" />
+```
+改为:
+```html
+<wuxing-sandbox mode="parent"
+  :badgeText="'综合成长力 ' + totalHealthIndex + '%'"
+  :dimensions="energyDimensions"
+  :totalEnergy="totalEnergy"
+  :totalHealthIndex="totalHealthIndex"
+  @point-click="goToDomain" />
+```
+
+- [ ] **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
+<wuxing-sandbox mode="child" badgeText="综合 0%" @point-click="goToDomain" />
+```
+改为:
+```html
+<wuxing-sandbox mode="child"
+  :badgeText="'综合 ' + totalHealthIndex + '%'"
+  :dimensions="energyDimensions"
+  :totalEnergy="totalEnergy"
+  :totalHealthIndex="totalHealthIndex"
+  @point-click="goToDomain" />
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add zxyj-frontend/pages/index/parent-index.vue zxyj-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: `zxyj-frontend/pages/energy/detail.vue`
+- Modify: `zxyj-frontend/pages.json`
+
+创建维度详情页:显示维度名称/图标/能量值/健康指数,下方展示流水记录。从wuxing-sandbox点击维度时navigateTo进入。
+
+- [ ] **Step 1: 创建 pages/energy/detail.vue**
+
+```vue
+<template>
+  <view class="energy-detail">
+    <!-- 维度头部 -->
+    <view class="dim-header">
+      <text class="dim-icon">{{ dimension.icon }}</text>
+      <view class="dim-info">
+        <text class="dim-name">{{ dimension.name }}·{{ dimension.element }}</text>
+        <text class="dim-energy">能量值: {{ dimension.energy }}</text>
+        <text class="dim-health">健康指数: {{ dimension.healthIndex }}</text>
+      </view>
+    </view>
+
+    <!-- 流水记录 -->
+    <view class="log-section">
+      <view class="log-tabs">
+        <text class="log-tab" :class="{ active: !filterType }" @click="filterType = ''">全部</text>
+        <text class="log-tab" :class="{ active: filterType === 'earn' }" @click="filterType = 'earn'">获得</text>
+        <text class="log-tab" :class="{ active: filterType === 'spend' }" @click="filterType = 'spend'">消耗</text>
+      </view>
+      <view class="log-list">
+        <view class="log-item" v-for="(item, idx) in filteredLogs" :key="idx">
+          <view class="log-left">
+            <text class="log-desc">{{ item.description }}</text>
+            <text class="log-time">{{ formatTime(item.createdAt) }}</text>
+          </view>
+          <text class="log-amount" :class="{ positive: item.amount > 0, negative: item.amount < 0 }">
+            {{ item.amount > 0 ? '+' : '' }}{{ item.amount }}
+          </text>
+        </view>
+        <view class="log-empty" v-if="filteredLogs.length === 0">
+          <text class="empty-text">暂无能量记录</text>
+        </view>
+      </view>
+      <view class="load-more" v-if="hasMore" @click="loadMore">
+        <text class="more-text">加载更多</text>
+      </view>
+    </view>
+  </view>
+</template>
+
+<script>
+import { getEnergyLogs } from '../../utils/api.js'
+
+export default {
+  data: function () {
+    return {
+      childId: null,
+      dimensionCode: '',
+      dimension: {
+        code: '', name: '', icon: '', element: '',
+        energy: 0, healthIndex: 0
+      },
+      logs: [],
+      page: 1,
+      size: 20,
+      hasMore: true,
+      filterType: ''
+    }
+  },
+  computed: {
+    filteredLogs: function () {
+      if (!this.filterType) return this.logs
+      if (this.filterType === 'earn') {
+        return this.logs.filter(function (l) { return l.amount > 0 })
+      }
+      return this.logs.filter(function (l) { return l.amount < 0 })
+    }
+  },
+  onLoad: function (options) {
+    this.childId = options.childId ? parseInt(options.childId) : null
+    this.dimensionCode = options.code || ''
+    if (options.dimData) {
+      try {
+        this.dimension = JSON.parse(decodeURIComponent(options.dimData))
+      } catch (e) {
+        // 解析失败用默认值
+      }
+    }
+    this.loadLogs()
+  },
+  methods: {
+    loadLogs: function () {
+      var that = this
+      if (!that.childId) return
+      getEnergyLogs(that.childId, that.dimensionCode, that.page, that.size).then(function (res) {
+        if (res.data && res.data.records) {
+          if (that.page === 1) {
+            that.logs = res.data.records
+          } else {
+            that.logs = that.logs.concat(res.data.records)
+          }
+          that.hasMore = res.data.records.length >= that.size
+        }
+      }).catch(function () {})
+    },
+    loadMore: function () {
+      this.page++
+      this.loadLogs()
+    },
+    formatTime: function (time) {
+      if (!time) return ''
+      var d = new Date(time)
+      var m = d.getMonth() + 1
+      var day = d.getDate()
+      var h = d.getHours()
+      var min = d.getMinutes()
+      return m + '/' + day + ' ' + (h < 10 ? '0' + h : h) + ':' + (min < 10 ? '0' + min : min)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.energy-detail { min-height: 100vh; background: #f5f5f5; }
+.dim-header {
+  display: flex;
+  align-items: center;
+  padding: 40rpx 30rpx;
+  background: linear-gradient(135deg, #1a1a2e, #16213e);
+}
+.dim-icon { font-size: 80rpx; margin-right: 30rpx; }
+.dim-info { flex: 1; }
+.dim-name { font-size: 36rpx; color: #FFD700; font-weight: bold; display: block; }
+.dim-energy { font-size: 28rpx; color: #fff; display: block; margin-top: 10rpx; }
+.dim-health { font-size: 24rpx; color: #aaa; display: block; margin-top: 6rpx; }
+
+.log-section { margin: 20rpx; }
+.log-tabs {
+  display: flex;
+  gap: 20rpx;
+  margin-bottom: 20rpx;
+}
+.log-tab {
+  font-size: 26rpx;
+  color: #666;
+  padding: 10rpx 30rpx;
+  border-radius: 30rpx;
+  background: #fff;
+}
+.log-tab.active { color: #FFD700; background: rgba(255,215,0,0.1); }
+
+.log-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 24rpx 20rpx;
+  background: #fff;
+  border-radius: 16rpx;
+  margin-bottom: 12rpx;
+}
+.log-left { flex: 1; }
+.log-desc { font-size: 28rpx; color: #333; display: block; }
+.log-time { font-size: 22rpx; color: #999; display: block; margin-top: 6rpx; }
+.log-amount { font-size: 32rpx; font-weight: bold; }
+.log-amount.positive { color: #4CAF50; }
+.log-amount.negative { color: #FF6B6B; }
+
+.log-empty { text-align: center; padding: 60rpx 0; }
+.empty-text { font-size: 28rpx; color: #999; }
+
+.load-more { text-align: center; padding: 20rpx; }
+.more-text { font-size: 26rpx; color: #666; }
+</style>
+```
+
+- [ ] **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 zxyj-frontend/pages/energy/detail.vue zxyj-frontend/pages.json zxyj-frontend/pages/index/parent-index.vue zxyj-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: `zxyj-frontend/pages/discover/index.vue`
+
+discover页保持preview模式,不需要调用API,但需要传入空dimensions数组保证组件正常渲染。
+
+- [ ] **Step 1: 更新discover/index.vue中的wuxing-sandbox调用**
+
+将:
+```html
+<wuxing-sandbox mode="preview" badgeText="登录查看完整报告" @point-click="handleLogin" />
+```
+改为:
+```html
+<wuxing-sandbox mode="preview"
+  badgeText="登录查看完整报告"
+  :dimensions="[]"
+  :totalEnergy="0"
+  :totalHealthIndex="0"
+  @point-click="handleLogin" />
+```
+
+空数组会触发组件内部的DEFAULT_DIMS fallback,显示图标和名称但能量值不显示。
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add zxyj-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 zxyj-backend && mvn clean compile -q`
+Expected: BUILD SUCCESS
+
+- [ ] **Step 2: 检查LSP诊断**
+
+Run: `lsp_diagnostics` on:
+- `zxyj-backend/src/main/java/com/zxyj/service/EnergyService.java`
+- `zxyj-backend/src/main/java/com/zxyj/controller/energy/EnergyController.java`
+- `zxyj-backend/src/main/java/com/zxyj/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中冗余。此为合理设计,不需要改动。

+ 337 - 0
docs/superpowers/specs/2026-06-05-five-dimension-energy-design.md

@@ -0,0 +1,337 @@
+# 五维能量系统设计
+
+**日期:** 2026-06-05
+**状态:** 已确认,待实施
+**分支:** cfclub
+
+## 概述
+
+本项目服务于身、心、智、行、富五个维度,服务内容包括活动、课程、商品、任务、咨询五大类。每个服务实例可影响多个维度,能量值按比例分配。能量系统为全新独立系统,与现有积分系统并行但解耦。
+
+### 关键决策
+
+| 决策项 | 结论 |
+|--------|------|
+| 维度比例配置方式 | 按服务实例配置(每个具体任务/课程/商品创建时可指定维度比例) |
+| 能量值性质 | 可消耗值(类似积分,可增可减) |
+| 健康指数算法 | 待定,先搭框架返回0占位 |
+| 与积分系统关系 | 全新独立系统,不复用积分 |
+| 架构方案 | 方案B:维度定义表+流水表+余额表 |
+
+## 1. 数据模型
+
+### 1.1 energy_dimension — 维度定义表
+
+| 列名 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT AUTO_INCREMENT PK | |
+| code | VARCHAR(16) UNIQUE | body/mind/wisdom/action/wealth |
+| name | VARCHAR(20) | 身/心/智/行/富 |
+| icon | VARCHAR(16) | 🌏/🔥/⚔️/🌿/💧 |
+| element | VARCHAR(10) | 土/火/金/木/水 |
+| sort_order | INT | 显示顺序 |
+| status | TINYINT DEFAULT 1 | 1启用/0禁用 |
+
+种子数据:
+
+| code | name | icon | element | sort_order |
+|------|------|------|---------|------------|
+| mind | 心 | 🔥 | 火 | 1 |
+| action | 行 | 🌿 | 木 | 2 |
+| wealth | 富 | 💧 | 水 | 3 |
+| wisdom | 智 | ⚔️ | 金 | 4 |
+| body | 身 | 🌏 | 土 | 5 |
+
+### 1.2 energy_source_config — 服务-维度比例配置表
+
+| 列名 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT AUTO_INCREMENT PK | |
+| source_type | VARCHAR(32) | task/activity/course/product/consultation |
+| source_id | BIGINT | 对应业务表ID |
+| source_name | VARCHAR(200) | 冗余名称,方便展示 |
+| dimension_id | BIGINT | 维度ID |
+| ratio | DECIMAL(5,4) | 该维度占比,如0.6000=60% |
+| created_at | DATETIME DEFAULT CURRENT_TIMESTAMP | |
+
+约束:同一 source_type+source_id 的所有 ratio 之和应 = 1.0。一个服务实例配多行记录。
+
+示例:任务#42 → 行60%+心40% = 2行记录。
+
+### 1.3 energy_log — 能量流水表
+
+| 列名 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT AUTO_INCREMENT PK | |
+| child_id | BIGINT | 孩子ID |
+| dimension_id | BIGINT | 维度ID |
+| amount | INT | 变动量(正=获得,负=消耗) |
+| balance_after | INT | 变动后该维度余额 |
+| source_type | VARCHAR(32) | 来源类型 |
+| source_id | BIGINT | 来源ID |
+| description | VARCHAR(500) | 描述 |
+| created_at | DATETIME DEFAULT CURRENT_TIMESTAMP | |
+
+索引:`idx_child_dim (child_id, dimension_id)`,`idx_created (created_at)`
+
+### 1.4 energy_balance — 各维度当前余额表
+
+| 列名 | 类型 | 说明 |
+|------|------|------|
+| id | BIGINT AUTO_INCREMENT PK | |
+| child_id | BIGINT | 孩子ID |
+| dimension_id | BIGINT | 维度ID |
+| balance | INT DEFAULT 0 | 当前能量值 |
+| total_earned | INT DEFAULT 0 | 累计获得 |
+| total_spent | INT DEFAULT 0 | 累计消耗 |
+| updated_at | DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | |
+
+UNIQUE(child_id, dimension_id)
+
+### 1.5 API 响应结构
+
+```json
+GET /api/energy/overview?childId=123
+{
+  "code": 0,
+  "data": {
+    "dimensions": [
+      {
+        "code": "mind",
+        "name": "心",
+        "icon": "🔥",
+        "element": "火",
+        "energy": 280,
+        "healthIndex": 0
+      },
+      {
+        "code": "action",
+        "name": "行",
+        "icon": "🌿",
+        "element": "木",
+        "energy": 350,
+        "healthIndex": 0
+      },
+      {
+        "code": "wealth",
+        "name": "富",
+        "icon": "💧",
+        "element": "水",
+        "energy": 200,
+        "healthIndex": 0
+      },
+      {
+        "code": "wisdom",
+        "name": "智",
+        "icon": "⚔️",
+        "element": "金",
+        "energy": 310,
+        "healthIndex": 0
+      },
+      {
+        "code": "body",
+        "name": "身",
+        "icon": "🌏",
+        "element": "土",
+        "energy": 380,
+        "healthIndex": 0
+      }
+    ],
+    "totalEnergy": 1520,
+    "totalHealthIndex": 0
+  }
+}
+```
+
+healthIndex 和 totalHealthIndex 算法待定,先返回0。
+
+## 2. 后端服务架构
+
+### 2.1 新增文件清单
+
+| 文件 | 位置 | 职责 |
+|------|------|------|
+| EnergyDimension | entity/ | 维度定义实体 |
+| EnergySourceConfig | entity/ | 服务-维度比例配置实体 |
+| EnergyLog | entity/ | 能量流水实体 |
+| EnergyBalance | entity/ | 维度余额实体 |
+| EnergyDimensionMapper | mapper/ | 维度CRUD |
+| EnergySourceConfigMapper | mapper/ | 比例配置CRUD |
+| EnergyLogMapper | mapper/ | 流水查询 |
+| EnergyBalanceMapper | mapper/ | 余额查询/更新 |
+| EnergyService | service/ | 核心业务:发放/扣除能量、查余额、算指数 |
+| EnergyController | controller/energy/ | API入口:概览、流水、配置 |
+
+### 2.2 EnergyService 核心方法
+
+```java
+/**
+ * 发放能量 — 指定source_type+source_id,自动查比例配置,按比例分配到各维度
+ * @return 各维度实际发放量 Map<dimensionCode, amount>
+ */
+Map<String, Integer> awardEnergy(Long childId, String sourceType, Long sourceId, Integer totalAmount, String description)
+
+/**
+ * 扣除能量 — 指定维度,检查余额,余额不足返回-1
+ * @return 扣除后余额,-1表示余额不足
+ */
+int deductEnergy(Long childId, Long dimensionId, Integer amount, String reason)
+
+/**
+ * 查询概览 — 返回5维度能量值+健康指数+总能量+总指数
+ */
+Map<String, Object> getOverview(Long childId)
+
+/**
+ * 查询流水 — 按维度筛选,分页
+ */
+Page<EnergyLog> getLogs(Long childId, String dimensionCode, Integer page, Integer size)
+
+/**
+ * 配置比例 — 为某服务实例设置维度比例
+ * ratios: [{dimensionCode:"action", ratio:0.6}, {dimensionCode:"mind", ratio:0.4}]
+ */
+void configureSourceRatios(String sourceType, Long sourceId, String sourceName, List<Map<String, Object>> ratios)
+```
+
+### 2.3 与现有系统的集成点
+
+| 业务场景 | 调用位置 | 调用方式 |
+|----------|----------|----------|
+| 完成任务 | TaskService.completeTask() | energyService.awardEnergy(childId, "task", taskId, energyAmount, "完成任务: "+title) |
+| 购买商品 | 商品订单支付成功回调 | energyService.awardEnergy(childId, "product", productId, energyAmount, "购买商品: "+name) |
+| 参加活动 | 活动报名/签到 | energyService.awardEnergy(childId, "activity", activityId, energyAmount, "参加活动: "+title) |
+| 课程学习 | 课程完成/打卡 | energyService.awardEnergy(childId, "course", courseId, energyAmount, "完成课程: "+title) |
+| 咨询完成 | 咨询结束后 | energyService.awardEnergy(childId, "consultation", consultId, energyAmount, "完成咨询") |
+
+能量发放额度由各业务方自行决定,EnergyService只负责按比例分配。
+
+### 2.4 awardEnergy 核心流程
+
+```
+1. 根据 sourceType+sourceId 查 energy_source_config 获取比例列表
+2. 若无配置,查 Product.domain 作为 fallback(单维度100%)
+3. 若仍无配置,默认分配到"行"维度100%
+4. 按 ratio * totalAmount 计算各维度发放量(整数,余数加到最大比例维度)
+5. 对每个维度:
+   a. 查 energy_balance 获取当前余额
+   b. 计算新余额
+   c. 更新 energy_balance
+   d. 写入 energy_log
+6. 返回各维度实际发放量
+```
+
+## 3. 前端可视化设计
+
+### 3.1 五角形布局(从顶点顺时针)
+
+```
+        顶: 心·火
+    左上: 行·木  右上: 富·水
+    左下: 智·金  右下: 身·土
+```
+
+### 3.2 三层视觉结构
+
+**外层 — 维度角(五角形顶点)**
+每个角显示:
+- 图标 + 名称(如 🔥心·火)
+- 能量值:绝对数字,如 `320`
+- 点击可跳转该维度详情页
+
+**中层 — 健康指数扇区(角底边→中心的三角形区域)**
+每个维度的三角形区域外围显示该维度的健康指数:
+- 数字显示,如 `78`
+- 背景色按指数高低渐变(绿→黄→红)
+- 指数算法待定,先显示0占位
+
+**内层 — 中心圆**
+中央圆形区域显示:
+- 总体能量值:五维度能量值之和,如 `1520`
+- 总体健康指数:综合指数,如 `75`
+- 算法待定,先显示0占位
+
+### 3.3 wuxing-sandbox.vue Props 接口
+
+```js
+props: {
+  mode: String,        // 'preview' | 'parent' | 'child'
+  dimensions: {        // API返回的维度数据
+    type: Array,
+    default: () => []
+  },
+  totalEnergy: {
+    type: Number,
+    default: 0
+  },
+  totalHealthIndex: {
+    type: Number,
+    default: 0
+  }
+}
+```
+
+dimensions 数组元素结构:
+```json
+{ "code": "body", "name": "身", "icon": "🌏", "element": "土", "energy": 320, "healthIndex": 0 }
+```
+
+### 3.4 新增页面
+
+| 页面 | 路径 | 说明 |
+|------|------|------|
+| 维度详情页 | pages/energy/detail.vue | 某维度能量值、流水记录、提升建议 |
+
+在 pages.json 中注册(非TabBar页),从沙盘点位点击 navigateTo 进入。
+
+### 3.5 API 调用
+
+```js
+// utils/api.js 新增
+getEnergyOverview(childId)      // POST /api/energy/overview
+getEnergyLogs(childId, dimensionCode, page, size)  // POST /api/energy/logs
+```
+
+### 3.6 现有引用更新
+
+| 页面 | 当前用法 | 更新为 |
+|------|----------|--------|
+| parent-index.vue | `<wuxing-sandbox mode="parent" badgeText="综合成长力 0%" />` | 传入从API获取的 dimensions/totalEnergy/totalHealthIndex |
+| child-index.vue | `<wuxing-sandbox mode="child" badgeText="综合 0%" />` | 同上 |
+| discover/index.vue | `<wuxing-sandbox mode="preview" badgeText="登录查看完整报告" />` | 保持预览模式,用空数据 |
+
+## 4. 数据库迁移与兼容性
+
+### 4.1 DatabaseInitializer 迁移步骤
+
+1. 创建4张新表(IF NOT EXISTS)
+2. 插入5条维度种子数据
+3. 为现有Product种子数据插入默认 energy_source_config(1:1单维度映射,与当前Product.domain一致)
+4. 为任务模板插入默认比例配置(任务默认→行100%)
+
+### 4.2 现有系统兼容
+
+| 项目 | 处理方式 |
+|------|----------|
+| 积分系统(PointsService) | 完全不动,能量系统独立运行 |
+| TaskService.completeTask() | 新增一行 energyService.awardEnergy(...) 调用,积分和能量并行发放 |
+| Product.domain 字段 | 保留不删除,作为无 energy_source_config 时的 fallback |
+| wuxing-sandbox.vue | 组件接口从硬编码改为接收 props 数据 |
+| 前端3处引用 | parent-index/child-index/discover 传入实际数据 |
+
+### 4.3 无比例配置时的 Fallback 链
+
+```
+1. 查 energy_source_config → 有 → 使用配置比例
+2. 无 → source_type 为 "product" 时查 Product.domain → 有 → 单维度100%
+3. 无 → 默认分配到"行"维度100%
+```
+
+## 5. 待定事项
+
+| 事项 | 说明 | 影响 |
+|------|------|------|
+| 健康指数算法 | 各维度健康指数 + 总体健康指数的计算公式 | API返回0占位,不影响数据结构 |
+| 能量过期/衰减 | 是否需要能量值过期机制 | 暂不实现,energy_log.created_at 可支撑后续时间窗口计算 |
+| Web管理端 | 管理端维度比例配置页面 | 本次只做后端API,管理端UI后续迭代 |
+| 能量兑换/消耗 | 扣除能量的具体业务场景 | deductEnergy 方法已预留,具体场景待定 |