2026-06-12-health-energy-implementation.md 27 KB

五维健康能量系统实施计划

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)


Task 1: Create wisdom dimension home page

Files:

  • Create: cfc-frontend/pages/wisdom/index.vue
  • Modify: cfc-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:

  • PageBanner with theme="wisdom", tagline "学以启智 · 慧及未来", quote "知者不惑,仁者不忧"
  • Functional entries grid with 5 items: 学习任务, 小游戏, 测评中心, 阅读天地, 知识挑战
  • Today's tasks section (if logged in)
  • Hot activities section
  • Recommended products section
  • 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).

  • Step 3: Verify LSP diagnostics

Run: lsp_diagnostics on cfc-frontend/pages/wisdom/index.vue — expect: clean (no errors/warnings)


Task 2: Create wealth dimension home page

Files:

  • Create: cfc-frontend/pages/wealth/index.vue
  • Reference: 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>
  • Step 2: Register wealth index page in pages.json

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.

  • Step 3: Verify LSP diagnostics

Run: lsp_diagnostics on both new files — clean.


Task 3: Integrate MiniGame → awardEnergy

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/service/GameRecordService.java
  • Reference: 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());
}
  • Step 2: Verify compilation

Run: mvn clean compile -f cfc-backend/pom.xml — expect: BUILD SUCCESS


Task 4: Integrate Streak/Checkin → awardEnergy

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/service/StreakService.java
  • Reference: 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());
}
  • Step 2: Verify compilation

Run: mvn clean compile -f cfc-backend/pom.xml — expect: BUILD SUCCESS


Task 5: Integrate Assessment → awardEnergy

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