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
| 文件 | 职责 |
|---|---|
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 |
维度详情页:能量值、流水记录 |
Files:
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java在 initSchema() 方法末尾(log.info("数据库迁移完成") 之前)添加能量系统4张表DDL和种子数据。
在 log.info("数据库迁移完成") 之前添加:
// ==================== 五维能量系统 ====================
// 维度定义表
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之后添加维度种子数据
// 维度种子数据
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单维度映射:
// 为已有商品插入默认比例配置(与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());
}
Run: cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 5: Commit
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"
Files:
cfc-backend/src/main/java/com/etotem/cfc/entity/EnergyDimension.javacfc-backend/src/main/java/com/etotem/cfc/entity/EnergySourceConfig.javacfc-backend/src/main/java/com/etotem/cfc/entity/EnergyLog.javacfc-backend/src/main/java/com/etotem/cfc/entity/EnergyBalance.java遵循现有entity模式:@Data @TableName @TableId(type=IdType.AUTO) implements Serializable,字段名用驼峰。
[ ] Step 1: 创建 EnergyDimension.java
package com.etotem.cfc.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("energy_dimension")
public class EnergyDimension implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String code;
private String name;
private String icon;
private String element;
private Integer sortOrder;
private Integer status;
private Date createdAt;
}
[ ] Step 2: 创建 EnergySourceConfig.java
package com.etotem.cfc.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("energy_source_config")
public class EnergySourceConfig implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private String sourceType;
private Long sourceId;
private String sourceName;
private Long dimensionId;
private BigDecimal ratio;
private Date createdAt;
}
[ ] Step 3: 创建 EnergyLog.java
package com.etotem.cfc.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("energy_log")
public class EnergyLog implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private Long childId;
private Long dimensionId;
private Integer amount;
private Integer balanceAfter;
private String sourceType;
private Long sourceId;
private String description;
private Date createdAt;
}
[ ] Step 4: 创建 EnergyBalance.java
package com.etotem.cfc.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
@TableName("energy_balance")
public class EnergyBalance implements Serializable {
@TableId(type = IdType.AUTO)
private Long id;
private Long childId;
private Long dimensionId;
private Integer balance;
private Integer totalEarned;
private Integer totalSpent;
private Date updatedAt;
}
[ ] Step 5: 验证编译
Run: cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 6: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/entity/Energy*.java
git commit -m "feat(energy): add 4 energy entity classes"
Files:
cfc-backend/src/main/java/com/etotem/cfc/mapper/EnergyDimensionMapper.javacfc-backend/src/main/java/com/etotem/cfc/mapper/EnergySourceConfigMapper.javacfc-backend/src/main/java/com/etotem/cfc/mapper/EnergyLogMapper.javacfc-backend/src/main/java/com/etotem/cfc/mapper/EnergyBalanceMapper.java遵循现有mapper模式:@Mapper interface XxxMapper extends BaseMapper<Xxx>,无自定义方法。
[ ] Step 1: 创建4个Mapper
// 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<EnergyDimension> {
}
// 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<EnergySourceConfig> {
}
// 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<EnergyLog> {
}
// 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<EnergyBalance> {
}
[ ] Step 2: 验证编译
Run: cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 3: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/mapper/Energy*Mapper.java
git commit -m "feat(energy): add 4 energy mapper interfaces"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java这是能量系统最核心的文件。实现5个核心方法:awardEnergy、deductEnergy、getOverview、getLogs、configureSourceRatios。
[ ] Step 1: 创建EnergyService.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<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 cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 3: Commit
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"
Files:
cfc-backend/src/main/java/com/etotem/cfc/controller/energy/EnergyController.java遵循现有controller模式:@RestController @RequestMapping,统一@PostMapping,@Resource注入Service,Result<T>响应。
[ ] Step 1: 创建EnergyController.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<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 cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 3: Commit
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"
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java在completeTask()方法的pointsLogMapper.insert(pointsLog)之后、Map<String, Object> result之前,添加一行energyService调用。
在现有@Resource注入区域添加:
@Resource
private EnergyService energyService;
在 pointsLogMapper.insert(pointsLog); 之后,Map<String, Object> result = new HashMap<>(); 之前,添加:
// 五维能量发放(与积分并行,独立系统)
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包裹,能量发放失败不影响积分逻辑(解耦原则)。
Run: cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
[ ] Step 4: Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java
git commit -m "feat(energy): integrate energy award into TaskService.completeTask()"
Files:
cfc-frontend/utils/api.js在文件末尾添加能量系统两个API调用。
[ ] Step 1: 在api.js末尾添加能量API
// 能量模块
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
git add cfc-frontend/utils/api.js
git commit -m "feat(energy): add energy API calls to frontend api.js"
Files:
cfc-frontend/components/wuxing-sandbox.vue完全重写组件,接收真实数据props,实现三层视觉结构(外层维度角、中层健康指数扇区、内层中心圆)。
[ ] Step 1: 重写wuxing-sandbox.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>
关键设计决策:
?.,用条件表达式代替)sortedDimensions computed按spec顺序排列badgeText和mode props,新增dimensions/totalEnergy/totalHealthIndex无数据时用DEFAULT_DIMS占位
[ ] Step 2: Commit
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"
Files:
cfc-frontend/pages/index/parent-index.vuecfc-frontend/pages/index/child-index.vue在两个首页的onShow或onLoad生命周期中调用getEnergyOverviewAPI,将结果传给wuxing-sandbox组件。
import区域:添加能量API导入
在现有 import WuxingSandbox from ... 之后添加:
import { getEnergyOverview } from '../../utils/api.js'
data区域:添加能量数据字段
energyDimensions: [],
totalEnergy: 0,
totalHealthIndex: 0,
methods区域:添加加载能量数据方法
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调用
将:
<wuxing-sandbox mode="parent" badgeText="综合成长力 0%" @point-click="goToDomain" />
改为:
<wuxing-sandbox mode="parent"
:badgeText="'综合成长力 ' + totalHealthIndex + '%'"
:dimensions="energyDimensions"
:totalEnergy="totalEnergy"
:totalHealthIndex="totalHealthIndex"
@point-click="goToDomain" />
同样模式:
import区域添加:
import { getEnergyOverview } from '../../utils/api.js'
data区域添加:
energyDimensions: [],
totalEnergy: 0,
totalHealthIndex: 0,
methods区域添加:
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区域将:
<wuxing-sandbox mode="child" badgeText="综合 0%" @point-click="goToDomain" />
改为:
<wuxing-sandbox mode="child"
:badgeText="'综合 ' + totalHealthIndex + '%'"
:dimensions="energyDimensions"
:totalEnergy="totalEnergy"
:totalHealthIndex="totalHealthIndex"
@point-click="goToDomain" />
[ ] Step 3: Commit
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"
Files:
cfc-frontend/pages/energy/detail.vuecfc-frontend/pages.json创建维度详情页:显示维度名称/图标/能量值/健康指数,下方展示流水记录。从wuxing-sandbox点击维度时navigateTo进入。
[ ] Step 1: 创建 pages/energy/detail.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 条目之后添加:
{
"path": "pages/energy/detail",
"style": {
"navigationBarTitleText": "维度详情"
}
}
在两个页面的 goToDomain 方法中,添加导航到能量详情页:
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
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"
Files:
cfc-frontend/pages/discover/index.vuediscover页保持preview模式,不需要调用API,但需要传入空dimensions数组保证组件正常渲染。
将:
<wuxing-sandbox mode="preview" badgeText="登录查看完整报告" @point-click="handleLogin" />
改为:
<wuxing-sandbox mode="preview"
badgeText="登录查看完整报告"
:dimensions="[]"
:totalEnergy="0"
:totalHealthIndex="0"
@point-click="handleLogin" />
空数组会触发组件内部的DEFAULT_DIMS fallback,显示图标和名称但能量值不显示。
[ ] Step 2: Commit
git add cfc-frontend/pages/discover/index.vue
git commit -m "feat(energy): update discover page with empty energy props for preview mode"
Files: 无修改,纯验证
Run: cd cfc-backend && mvn clean compile -q
Expected: BUILD SUCCESS
Run: lsp_diagnostics on:
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.javacfc-backend/src/main/java/com/etotem/cfc/controller/energy/EnergyController.javacfc-backend/src/main/java/com/etotem/cfc/service/TaskService.javaExpected: 无新增error
1. Spec Coverage:
2. Placeholder Scan:
3. Type Consistency:
/api/energy/overview 和 /api/energy/logs 前后端一致4. Gaps Found:
dimensionCode字段便于前端展示 → 在getLogs API响应中通过dimensionId关联。前端detail页已知dimensionCode(从URL参数传入),无需在log中冗余。此为合理设计,不需要改动。