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: Implement remaining high-priority items from 健康能量设计.md: wisdom/wealth dimension pages + backend multi-business energy ledger integration.
Architecture: Frontend uni-app Vue 2 pages following existing dimension page patterns (body/action/mind). Backend adds awardEnergy hook calls at existing completion points in GameRecordService, StreakService/FinanceCheckinService, and AssessmentAppointmentService — same pattern as TaskService already uses.
Tech Stack: uni-app Vue 2 (frontend), Spring Boot 2.7 + MyBatis-Plus (backend)
Files:
cfc-frontend/pages/wisdom/index.vuecfc-frontend/pages.json (register page in subPackages)Reference: cfc-frontend/pages/action/index.vue (pattern)
[ ] Step 1: Create pages/wisdom/index.vue
Follow the exact pattern of pages/action/index.vue:
theme="wisdom", tagline "学以启智 · 慧及未来", quote "知者不惑,仁者不忧"bottom-spacer
<template>
<view class="container">
<PageBanner theme="wisdom"
tagline="学以启智 · 慧及未来"
quote="知者不惑,仁者不忧" />
<view class="section" v-if="sectionVisible('today_tasks') && isLoggedIn">
<view class="section-header">
<text class="section-title">📋 我的任务</text>
<text class="section-more" @click="goTasks">全部 ›</text>
</view>
<view class="task-wrap" v-if="todayTasks.length > 0">
<view class="task-item" v-for="task in todayTasks" :key="task.id">
<text class="task-name">{{ task.title }}</text>
<text class="task-points">+{{ task.points || 0 }}分</text>
</view>
</view>
<view class="task-empty" v-else>
<text class="task-empty-text">今日暂无任务</text>
</view>
</view>
<view class="func-section" v-if="sectionVisible('func_entries')">
<view class="func-grid">
<view class="func-item" v-for="item in funcList" :key="item.label" @click="onFuncClick(item)">
<view class="func-icon-wrap">
<text class="func-icon">{{ item.icon }}</text>
</view>
<text class="func-label">{{ item.label }}</text>
</view>
</view>
</view>
<view class="section" v-if="sectionVisible('hot_activities')">
<view class="section-header">
<text class="section-title">🔥 热门活动</text>
<text class="section-more" @click="goMoreActivities">更多 ›</text>
</view>
<scroll-view scroll-x class="activity-scroll" show-scrollbar="false">
<view class="activity-card" v-for="act in hotActivities" :key="act.id" @click="goActivityDetail(act.id)">
<image class="activity-img" :src="act.image || '/static/default-activity.png'" mode="aspectFill" />
<view class="activity-info">
<text class="activity-name">{{ act.name }}</text>
<text class="activity-meta">{{ act.date }} · {{ act.location }}</text>
</view>
</view>
</scroll-view>
</view>
<view class="section" v-if="sectionVisible('recommended_products')">
<view class="section-header">
<text class="section-title">🛍️ 推荐商品</text>
<text class="section-more" @click="goMoreProducts">更多 ›</text>
</view>
<view class="product-list">
<view class="product-card" v-for="prod in recommendedProducts" :key="prod.id" @click="goProductDetail(prod.id)">
<image class="product-img" :src="prod.image || '/static/default-product.png'" mode="aspectFill" />
<text class="product-name line-clamp-2">{{ prod.name }}</text>
<text class="product-price">¥{{ prod.price }}</text>
</view>
</view>
</view>
<view class="bottom-spacer"></view>
</view>
</template>
<script>
import PageBanner from '../../components/PageBanner.vue'
import { getVisibleSections, getEnergyOverview, getTodayTasks } from '../../utils/api.js'
export default {
components: { PageBanner },
data() {
return {
isLoggedIn: false,
role: null,
currentChildId: null,
totalEnergy: null,
visibleSections: [],
funcList: [
{ icon: '\u{1F4DA}', label: '学习任务', needLogin: true, page: '/pages/tasks/tasks' },
{ icon: '\u{1F3AE}', label: '小游戏', needLogin: false, page: '/pages/games/list' },
{ icon: '\u{1F9E9}', label: '测评中心', needLogin: false, page: '/pages/assessment/apply' },
{ icon: '\u{1F4D6}', label: '阅读天地', needLogin: false, page: '/pages/mind/articles' },
{ icon: '\u{1F4A1}', label: '知识挑战', needLogin: false, page: '' }
],
hotActivities: [
{ id: 1, name: '少儿编程启蒙营', date: '周六', location: '线上', image: '' },
{ id: 2, name: '经典阅读分享会', date: '周日', location: '浠艾福中心', image: '' },
{ id: 3, name: '科学实验探索课', date: '下周六', location: '浠艾福中心', image: '' }
],
recommendedProducts: [
{ id: 1, name: '儿童百科全书套装', price: 199, image: '' },
{ id: 2, name: '益智棋类礼盒', price: 159, image: '' },
{ id: 3, name: '科学实验套装', price: 299, image: '' },
{ id: 4, name: '中外名著精选', price: 129, image: '' }
],
todayTasks: []
}
},
onShow() {
var token = uni.getStorageSync('token')
this.isLoggedIn = !!token
if (this.isLoggedIn) {
this.role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
this.currentChildId = uni.getStorageSync('currentChildId') || null
}
this.loadSectionConfig()
if (this.isLoggedIn) {
this.loadEnergyData()
this.loadTodayTasks()
}
},
methods: {
async loadSectionConfig() {
if (!uni.getStorageSync('token')) {
this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']
return
}
var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
try {
var res = await getVisibleSections({ pageKey: 'wisdom', role: role })
if (res.code === 200 && res.data) {
this.visibleSections = res.data.map(function(s) { return s.sectionKey })
}
} catch (e) {
this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']
}
},
async loadEnergyData() {
var childId = this.currentChildId
if (!childId) return
try {
var res = await getEnergyOverview(childId)
if (res && res.data) {
this.totalEnergy = res.data.totalEnergy || 0
}
} catch (e) { console.log('获取能量概览失败', e) }
},
async loadTodayTasks() {
var childId = this.currentChildId
if (!childId) return
try {
var res = await getTodayTasks(childId)
if (res && res.data) {
this.todayTasks = res.data.slice(0, 5)
}
} catch (e) { console.log('获取今日任务失败', e) }
},
sectionVisible(key) {
return this.visibleSections.length === 0 || this.visibleSections.indexOf(key) !== -1
},
onFuncClick(item) {
if (item.needLogin && !this.isLoggedIn) {
uni.showModal({
title: '提示',
content: '请先登录后使用此功能',
success: function(res) { if (res.confirm) uni.navigateTo({ url: '/pages/login/login' }) }
})
return
}
if (item.page) {
var nativeTabs = ['/pages/tasks/tasks', '/pages/index/index', '/pages/body/index', '/pages/mind/index', '/pages/action/index', '/pages/wisdom/index', '/pages/profile/profile']
if (nativeTabs.indexOf(item.page) !== -1) {
uni.switchTab({ url: item.page })
} else {
uni.navigateTo({ url: item.page })
}
}
},
goMoreActivities() {},
goActivityDetail(id) {},
goMoreProducts() {},
goProductDetail(id) {
var token = uni.getStorageSync('token')
if (token) {
uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })
} else {
uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/wisdom/index') })
}
},
goTasks() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
goLogin() { uni.navigateTo({ url: '/pages/login/login' }) }
}
}
</script>
<style scoped>
.container {
min-height: 100vh;
background: #f5f7fa;
padding-bottom: 120rpx;
}
.func-section {
margin: -40rpx 20rpx 0;
position: relative;
z-index: 2;
}
.func-grid {
background: #fff;
border-radius: 24rpx;
padding: 30rpx 16rpx;
display: flex;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
}
.func-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.func-item:active {
opacity: 0.7;
}
.func-icon-wrap {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: #FFF8E1;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
.func-icon {
font-size: 40rpx;
}
.func-label {
font-size: 24rpx;
color: #333;
font-weight: 500;
}
.section {
margin: 20rpx 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.section-more {
font-size: 24rpx;
color: #999;
}
.activity-scroll {
white-space: nowrap;
}
.activity-card {
display: inline-block;
width: 280rpx;
margin-right: 20rpx;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.activity-img {
width: 280rpx;
height: 180rpx;
background: #f0f0f0;
}
.activity-info {
padding: 16rpx;
}
.activity-name {
font-size: 26rpx;
font-weight: bold;
color: #333;
display: block;
}
.activity-meta {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
display: block;
}
.product-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.product-card {
width: calc(50% - 10rpx);
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.product-img {
width: 100%;
height: 240rpx;
background: #f0f0f0;
}
.product-name {
font-size: 26rpx;
color: #333;
padding: 12rpx 16rpx 0;
}
.product-price {
font-size: 28rpx;
color: #F97316;
font-weight: bold;
padding: 8rpx 16rpx 16rpx;
display: block;
}
.line-clamp-2 {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.task-wrap {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.task-item {
display: flex;
justify-content: space-between;
padding: 28rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.task-item:last-child {
border-bottom: none;
}
.task-name {
font-size: 26rpx;
color: #333;
}
.task-points {
font-size: 24rpx;
color: #F97316;
}
.task-empty {
background: #fff;
border-radius: 16rpx;
padding: 60rpx;
text-align: center;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.task-empty-text {
font-size: 26rpx;
color: #999;
}
.bottom-spacer {
height: 20rpx;
}
</style>
[ ] Step 2: Register wisdom page in pages.json
Add to the subPackages array in pages.json — create a new subpackage block for wisdom pages:
// ===== 智慧维度 =====
{
"root": "pages/wisdom",
"pages": [
{
"path": "index",
"style": { "navigationBarTitleText": "智慧" }
}
]
}
Insert this after the pages/energy subpackage block (around line 437) and before the pages/membership block (around line 438).
Run: lsp_diagnostics on cfc-frontend/pages/wisdom/index.vue — expect: clean (no errors/warnings)
Files:
cfc-frontend/pages/wealth/index.vueReference: cfc-frontend/pages/action/index.vue (pattern)
[ ] Step 1: Create pages/wealth/index.vue
Follow the pattern of pages/action/index.vue:
<template>
<view class="container">
<PageBanner theme="wealth"
tagline="财商培养 · 富足人生"
quote="君子爱财,取之有道" />
<view class="section" v-if="sectionVisible('today_tasks') && isLoggedIn">
<view class="section-header">
<text class="section-title">📋 我的任务</text>
<text class="section-more" @click="goTasks">全部 ›</text>
</view>
<view class="task-wrap" v-if="todayTasks.length > 0">
<view class="task-item" v-for="task in todayTasks" :key="task.id">
<text class="task-name">{{ task.title }}</text>
<text class="task-points">+{{ task.points || 0 }}分</text>
</view>
</view>
<view class="task-empty" v-else>
<text class="task-empty-text">今日暂无任务</text>
</view>
</view>
<view class="func-section" v-if="sectionVisible('func_entries')">
<view class="func-grid">
<view class="func-item" v-for="item in funcList" :key="item.label" @click="onFuncClick(item)">
<view class="func-icon-wrap">
<text class="func-icon">{{ item.icon }}</text>
</view>
<text class="func-label">{{ item.label }}</text>
</view>
</view>
</view>
<view class="section" v-if="sectionVisible('hot_activities')">
<view class="section-header">
<text class="section-title">🔥 热门活动</text>
<text class="section-more" @click="goMoreActivities">更多 ›</text>
</view>
<scroll-view scroll-x class="activity-scroll" show-scrollbar="false">
<view class="activity-card" v-for="act in hotActivities" :key="act.id" @click="goActivityDetail(act.id)">
<image class="activity-img" :src="act.image || '/static/default-activity.png'" mode="aspectFill" />
<view class="activity-info">
<text class="activity-name">{{ act.name }}</text>
<text class="activity-meta">{{ act.date }} · {{ act.location }}</text>
</view>
</view>
</scroll-view>
</view>
<view class="section" v-if="sectionVisible('recommended_products')">
<view class="section-header">
<text class="section-title">🛍️ 推荐商品</text>
<text class="section-more" @click="goMoreProducts">更多 ›</text>
</view>
<view class="product-list">
<view class="product-card" v-for="prod in recommendedProducts" :key="prod.id" @click="goProductDetail(prod.id)">
<image class="product-img" :src="prod.image || '/static/default-product.png'" mode="aspectFill" />
<text class="product-name line-clamp-2">{{ prod.name }}</text>
<text class="product-price">¥{{ prod.price }}</text>
</view>
</view>
</view>
<view class="bottom-spacer"></view>
</view>
</template>
<script>
import PageBanner from '../../components/PageBanner.vue'
import { getVisibleSections, getEnergyOverview, getTodayTasks } from '../../utils/api.js'
export default {
components: { PageBanner },
data() {
return {
isLoggedIn: false,
role: null,
currentChildId: null,
totalEnergy: null,
visibleSections: [],
funcList: [
{ icon: '\u{1F4B0}', label: '财商打卡', needLogin: true, page: '/pages/wealth/checkin' },
{ icon: '\u{1F4B5}', label: '零花钱管理', needLogin: true, page: '' },
{ icon: '\u{1F4C8}', label: '推广中心', needLogin: true, page: '/pages/promotion/index' },
{ icon: '\u{1F3E6}', label: '商城', needLogin: false, page: '/pages/shop/index' },
{ icon: '\u{1F4CB}', label: '保单管理', needLogin: true, page: '/pages/wealth/insurance-list' }
],
hotActivities: [
{ id: 1, name: '小小理财师体验营', date: '周六', location: '浠艾福中心', image: '' },
{ id: 2, name: '跳蚤市场亲子活动', date: '周日', location: '朝阳公园', image: '' },
{ id: 3, name: '财商启蒙绘本共读', date: '下周六', location: '线上', image: '' }
],
recommendedProducts: [
{ id: 1, name: '儿童理财启蒙套装', price: 199, image: '' },
{ id: 2, name: '亲子财商桌游', price: 159, image: '' },
{ id: 3, name: '零花钱记账本', price: 49, image: '' },
{ id: 4, name: '少儿财商课程', price: 399, image: '' }
],
todayTasks: []
}
},
onShow() {
var token = uni.getStorageSync('token')
this.isLoggedIn = !!token
if (this.isLoggedIn) {
this.role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
this.currentChildId = uni.getStorageSync('currentChildId') || null
}
this.loadSectionConfig()
if (this.isLoggedIn) {
this.loadEnergyData()
this.loadTodayTasks()
}
},
methods: {
async loadSectionConfig() {
if (!uni.getStorageSync('token')) {
this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']
return
}
var role = uni.getStorageSync('currentRole') || uni.getStorageSync('role') || null
try {
var res = await getVisibleSections({ pageKey: 'wealth', role: role })
if (res.code === 200 && res.data) {
this.visibleSections = res.data.map(function(s) { return s.sectionKey })
}
} catch (e) {
this.visibleSections = ['func_entries', 'hot_activities', 'recommended_products']
}
},
async loadEnergyData() {
var childId = this.currentChildId
if (!childId) return
try {
var res = await getEnergyOverview(childId)
if (res && res.data) {
this.totalEnergy = res.data.totalEnergy || 0
}
} catch (e) { console.log('获取能量概览失败', e) }
},
async loadTodayTasks() {
var childId = this.currentChildId
if (!childId) return
try {
var res = await getTodayTasks(childId)
if (res && res.data) {
this.todayTasks = res.data.slice(0, 5)
}
} catch (e) { console.log('获取今日任务失败', e) }
},
sectionVisible(key) {
return this.visibleSections.length === 0 || this.visibleSections.indexOf(key) !== -1
},
onFuncClick(item) {
if (item.needLogin && !this.isLoggedIn) {
uni.showModal({
title: '提示',
content: '请先登录后使用此功能',
success: function(res) { if (res.confirm) uni.navigateTo({ url: '/pages/login/login' }) }
})
return
}
if (item.page) {
var nativeTabs = ['/pages/tasks/tasks', '/pages/index/index', '/pages/body/index', '/pages/mind/index', '/pages/action/index', '/pages/wisdom/index', '/pages/profile/profile']
if (nativeTabs.indexOf(item.page) !== -1) {
uni.switchTab({ url: item.page })
} else {
uni.navigateTo({ url: item.page })
}
}
},
goMoreActivities() {},
goActivityDetail(id) {},
goMoreProducts() {},
goProductDetail(id) {
var token = uni.getStorageSync('token')
if (token) {
uni.navigateTo({ url: '/pages/discover/product-detail/product-detail?id=' + id })
} else {
uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/wealth/index') })
}
},
goTasks() { uni.switchTab({ url: '/pages/tasks/tasks' }) },
goLogin() { uni.navigateTo({ url: '/pages/login/login' }) }
}
}
</script>
<style scoped>
.container {
min-height: 100vh;
background: #f5f7fa;
padding-bottom: 120rpx;
}
.func-section {
margin: -40rpx 20rpx 0;
position: relative;
z-index: 2;
}
.func-grid {
background: #fff;
border-radius: 24rpx;
padding: 30rpx 16rpx;
display: flex;
box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.06);
}
.func-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
}
.func-item:active {
opacity: 0.7;
}
.func-icon-wrap {
width: 88rpx;
height: 88rpx;
border-radius: 50%;
background: #E3F2FD;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 10rpx;
}
.func-icon {
font-size: 40rpx;
}
.func-label {
font-size: 24rpx;
color: #333;
font-weight: 500;
}
.section {
margin: 20rpx 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
}
.section-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
}
.section-more {
font-size: 24rpx;
color: #999;
}
.activity-scroll {
white-space: nowrap;
}
.activity-card {
display: inline-block;
width: 280rpx;
margin-right: 20rpx;
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.activity-img {
width: 280rpx;
height: 180rpx;
background: #f0f0f0;
}
.activity-info {
padding: 16rpx;
}
.activity-name {
font-size: 26rpx;
font-weight: bold;
color: #333;
display: block;
}
.activity-meta {
font-size: 22rpx;
color: #999;
margin-top: 8rpx;
display: block;
}
.product-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.product-card {
width: calc(50% - 10rpx);
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.product-img {
width: 100%;
height: 240rpx;
background: #f0f0f0;
}
.product-name {
font-size: 26rpx;
color: #333;
padding: 12rpx 16rpx 0;
}
.product-price {
font-size: 28rpx;
color: #F97316;
font-weight: bold;
padding: 8rpx 16rpx 16rpx;
display: block;
}
.line-clamp-2 {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.task-wrap {
background: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.task-item {
display: flex;
justify-content: space-between;
padding: 28rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.task-item:last-child {
border-bottom: none;
}
.task-name {
font-size: 26rpx;
color: #333;
}
.task-points {
font-size: 24rpx;
color: #F97316;
}
.task-empty {
background: #fff;
border-radius: 16rpx;
padding: 60rpx;
text-align: center;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
}
.task-empty-text {
font-size: 26rpx;
color: #999;
}
.bottom-spacer {
height: 20rpx;
}
</style>
Add to the existing pages/wealth subpackage block:
// ===== 财富维度 =====
{
"root": "pages/wealth",
"pages": [
{
"path": "index",
"style": { "navigationBarTitleText": "财富" }
},
{
"path": "checkin",
"style": { "navigationBarTitleText": "财商打卡" }
},
// ... existing pages ...
]
}
Insert index as the FIRST entry in the pages/wealth pages array.
Run: lsp_diagnostics on both new files — clean.
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/GameRecordService.javaReference: cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java (lines 372-382, awardEnergy pattern)
[ ] Step 1: Add @Resource EnergyService and awardEnergy call in saveRecord
Inject EnergyService and add the same try-catch awardEnergy pattern after gameRecordMapper.insert(record):
import javax.annotation.Resource;
// ... existing imports ...
@Resource
private EnergyService energyService;
// Inside saveRecord method, after gameRecordMapper.insert(record); (line 45):
// 发放智能量
try {
int energyAmount = Math.abs(pointsEarned != null ? pointsEarned : 0);
if (energyAmount > 0) {
energyService.awardEnergy(childId, "game", record.getId(), energyAmount,
"完成游戏: " + gameCode, null);
}
} catch (Exception e) {
log.warn("游戏能量发放失败: childId={}, gameCode={}, error={}",
childId, gameCode, e.getMessage());
}
Run: mvn clean compile -f cfc-backend/pom.xml — expect: BUILD SUCCESS
Files:
cfc-backend/src/main/java/com/etotem/cfc/service/StreakService.javaReference: TaskService.java lines 372-382
[ ] Step 1: Add @Resource EnergyService and awardEnergy in checkAndAwardMilestone
Inject EnergyService and add awardEnergy call when a milestone is reached (after recordBonusPoints call), and for daily streak checkin:
@Resource
private EnergyService energyService;
// In checkAndAwardMilestone, before or after the recordBonusPoints call (around line 74):
// 发放行能量 - 达到里程碑
try {
energyService.awardEnergy(childId, "streak", milestone.getId().longValue(), rewardPoints,
"打卡里程碑-" + milestone.getDays() + "天奖励", null);
} catch (Exception e) {
log.warn("打卡里程碑能量发放失败: childId={}, error={}", childId, e.getMessage());
}
Also add a daily streak energy award (even without milestone, for continuing the streak). After milestone check loop ends (around line 81), before the return:
// 每日连续打卡能量奖励(即使没达到里程碑也奖励)
try {
energyService.awardEnergy(childId, "streak", 0L, 5,
"连续打卡第" + currentStreakDays + "天", null);
} catch (Exception e) {
log.warn("每日打卡能量发放失败: childId={}, error={}", childId, e.getMessage());
}
Run: mvn clean compile -f cfc-backend/pom.xml — expect: BUILD SUCCESS
Files:
Modify: cfc-backend/src/main/java/com/etotem/cfc/service/AssessmentAppointmentService.java
[ ] Step 1: Add @Resource EnergyService and awardEnergy in completeAppointment
import com.etotem.cfc.service.EnergyService;
import javax.annotation.Resource;
@Resource
private EnergyService energyService;
// In completeAppointment method, after setting status=2 (line 54), before return:
// 测评完成发放智能量
try {
if (appointment.getChildId() != null) {
energyService.awardEnergy(appointment.getChildId(), "assessment", id, 20,
"完成测评预约", null);
}
} catch (Exception e) {
log.warn("测评能量发放失败: appointmentId={}, error={}", id, e.getMessage());
}
[ ] Step 2: Verify compilation
Run: mvn clean compile -f cfc-backend/pom.xml — expect: BUILD SUCCESS