2026-06-11-wealth-dimension-implementation.md 35 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: 实现财富健康三要素:记账打卡、保单上传、以及商品购买/创收与富维度的能量联动

Architecture: 后端新增两张表(finance_checkins + insurance_policies)及对应 CRUD API + 能量发放;小程序端新增记账打卡页和保单管理页;调整富维度默认比例和日上限。

Tech Stack: Spring Boot + MyBatis-Plus / uni-app Vue 2

设计依据: docs/superpowers/specs/2026-06-11-wealth-dimension-energy-design.md


文件改动总览

# 文件 操作 说明
1 entity/FinanceCheckin.java 新增 记账打卡实体
2 entity/InsurancePolicy.java 新增 保单实体
3 mapper/FinanceCheckinMapper.java 新增 打卡 Mapper
4 mapper/InsurancePolicyMapper.java 新增 保单 Mapper
5 service/FinanceCheckinService.java 新增 打卡业务(含能量发放)
6 service/InsurancePolicyService.java 新增 保单业务(含能量发放)
7 controller/wealth/FinanceCheckinController.java 新增 打卡 API
8 controller/wealth/InsurancePolicyController.java 新增 保单 API
9 service/EnergyService.java 修改 新增富维度能量发放方法
10 config/DatabaseInitializer.java 修改 新增两张表 DDL
11 cfc-frontend/utils/api.js 修改 新增 API
12 cfc-frontend/pages/wealth/checkin.vue 新增 记账打卡页
13 cfc-frontend/pages/wealth/insurance-list.vue 新增 保单列表页
14 cfc-frontend/pages/wealth/insurance-add.vue 新增 添加保单页
15 cfc-frontend/pages/wealth/index.vue 新增 财富健康首页
16 cfc-frontend/pages.json 修改 注册新页面路径

Task 1: 数据库 — 新建两张表

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

  • [ ] Step 1: 在 initializeTables() 中添加建表语句

    // 记账打卡表
    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS finance_checkins (" +
    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
    "user_id BIGINT NOT NULL COMMENT '用户ID', " +
    "child_id BIGINT DEFAULT NULL COMMENT '孩子ID(可为空)', " +
    "checkin_date DATE NOT NULL COMMENT '记账日期', " +
    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
    "UNIQUE KEY uk_user_date (user_id, IFNULL(child_id, 0), checkin_date)" +
    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='记账打卡'");
    
    // 保单表
    jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS insurance_policies (" +
    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
    "user_id BIGINT NOT NULL COMMENT '用户ID', " +
    "policy_type VARCHAR(50) NOT NULL COMMENT '保单类型:医疗/重疾/意外/寿险/养老/其他', " +
    "insured_name VARCHAR(100) NOT NULL COMMENT '被保人姓名', " +
    "insured_relation VARCHAR(20) DEFAULT 'self' COMMENT '关系:self/spouse/child/parent', " +
    "insurance_company VARCHAR(100) DEFAULT '' COMMENT '保险公司', " +
    "policy_number VARCHAR(100) DEFAULT '' COMMENT '保单号', " +
    "coverage_amount DECIMAL(12,2) DEFAULT 0 COMMENT '保额(万元)', " +
    "premium DECIMAL(10,2) DEFAULT 0 COMMENT '年缴保费', " +
    "start_date DATE DEFAULT NULL COMMENT '生效日期', " +
    "end_date DATE DEFAULT NULL COMMENT '到期日期', " +
    "image_url VARCHAR(500) DEFAULT '' COMMENT '保单照片URL', " +
    "status TINYINT DEFAULT 1 COMMENT '1有效/0失效', " +
    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='保单记录'");
    
  • [ ] Step 2: Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
    git commit -m "feat: add finance_checkins and insurance_policies DDL"
    

Task 2: 后端实体 + Mapper

Files:

  • Create: cfc-backend/src/main/java/com/etotem/cfc/entity/FinanceCheckin.java
  • Create: cfc-backend/src/main/java/com/etotem/cfc/entity/InsurancePolicy.java
  • Create: cfc-backend/src/main/java/com/etotem/cfc/mapper/FinanceCheckinMapper.java
  • Create: cfc-backend/src/main/java/com/etotem/cfc/mapper/InsurancePolicyMapper.java

  • [ ] Step 1: FinanceCheckin.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("finance_checkins")
    public class FinanceCheckin implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private Long childId;
    private Date checkinDate;
    private Date createdAt;
    }
    
  • [ ] Step 2: InsurancePolicy.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("insurance_policies")
    public class InsurancePolicy implements Serializable {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long userId;
    private String policyType;
    private String insuredName;
    private String insuredRelation;
    private String insuranceCompany;
    private String policyNumber;
    private BigDecimal coverageAmount;
    private BigDecimal premium;
    private Date startDate;
    private Date endDate;
    private String imageUrl;
    private Integer status;
    private Date createdAt;
    private Date updatedAt;
    }
    
  • [ ] Step 3: 两个 Mapper

    @Mapper
    public interface FinanceCheckinMapper extends BaseMapper<FinanceCheckin> {}
    
    @Mapper
    public interface InsurancePolicyMapper extends BaseMapper<InsurancePolicy> {}
    
  • [ ] Step 4: Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/entity/FinanceCheckin.java cfc-backend/src/main/java/com/etotem/cfc/entity/InsurancePolicy.java cfc-backend/src/main/java/com/etotem/cfc/mapper/FinanceCheckinMapper.java cfc-backend/src/main/java/com/etotem/cfc/mapper/InsurancePolicyMapper.java
    git commit -m "feat: add FinanceCheckin and InsurancePolicy entities"
    

Task 3: 后端 Service + Controller

Files:

  • Create: cfc-backend/src/main/java/com/etotem/cfc/service/FinanceCheckinService.java
  • Create: cfc-backend/src/main/java/com/etotem/cfc/service/InsurancePolicyService.java
  • Create: cfc-backend/src/main/java/com/etotem/cfc/controller/wealth/FinanceCheckinController.java
  • Create: cfc-backend/src/main/java/com/etotem/cfc/controller/wealth/InsurancePolicyController.java

  • [ ] Step 1: FinanceCheckinService.java

    package com.etotem.cfc.service;
    
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import com.etotem.cfc.entity.FinanceCheckin;
    import com.etotem.cfc.mapper.FinanceCheckinMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import javax.annotation.Resource;
    import java.time.LocalDate;
    import java.time.ZoneId;
    import java.util.Date;
    import java.util.List;
    
    @Slf4j
    @Service
    public class FinanceCheckinService {
    
    @Resource
    private FinanceCheckinMapper financeCheckinMapper;
    
    @Resource
    private EnergyService energyService;
    
    /**
     * 今日是否已记账
     */
    public boolean hasCheckedInToday(Long userId, Long childId) {
        LocalDate today = LocalDate.now();
        return financeCheckinMapper.selectCount(new LambdaQueryWrapper<FinanceCheckin>()
                .eq(FinanceCheckin::getUserId, userId)
                .eq(childId != null, FinanceCheckin::getChildId, childId)
                .eq(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(today))) > 0;
    }
    
    /**
     * 记账打卡
     * @return energy 本次获得的能量值
     */
    @Transactional
    public int checkin(Long userId, Long childId) {
        if (hasCheckedInToday(userId, childId)) {
            return 0; // 今天已记过
        }
    
        LocalDate today = LocalDate.now();
        FinanceCheckin record = new FinanceCheckin();
        record.setUserId(userId);
        record.setChildId(childId);
        record.setCheckinDate(java.sql.Date.valueOf(today));
        financeCheckinMapper.insert(record);
    
        // 基础能量 +3
        int energy = 3;
    
        // 连续7天额外奖励
        if (isStreak7Days(userId, childId)) {
            energy += 10;
        }
    
        // 调用能量服务发放富维度能量
        // energyService.awardEnergy(childId != null ? childId : userId, "finance_checkin", record.getId(),
        //         energy, childId != null ? "child" : "parent", "记账打卡");
    
        return energy;
    }
    
    /**
     * 判断是否连续记账7天
     */
    private boolean isStreak7Days(Long userId, Long childId) {
        LocalDate today = LocalDate.now();
        LocalDate sevenDaysAgo = today.minusDays(6);
    
        long count = financeCheckinMapper.selectCount(new LambdaQueryWrapper<FinanceCheckin>()
                .eq(FinanceCheckin::getUserId, userId)
                .eq(childId != null, FinanceCheckin::getChildId, childId)
                .ge(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(sevenDaysAgo))
                .le(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(today)));
    
        return count >= 7;
    }
    
    /**
     * 当月打卡天数
     */
    public int getMonthlyCheckinCount(Long userId, Long childId) {
        LocalDate now = LocalDate.now();
        LocalDate firstDay = now.withDayOfMonth(1);
        LocalDate lastDay = now.withDayOfMonth(now.lengthOfMonth());
    
        return financeCheckinMapper.selectCount(new LambdaQueryWrapper<FinanceCheckin>()
                .eq(FinanceCheckin::getUserId, userId)
                .eq(childId != null, FinanceCheckin::getChildId, childId)
                .ge(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(firstDay))
                .le(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(lastDay)));
    }
    
    /**
     * 本月打卡日期列表(用于日历展示)
     */
    public List<Date> getMonthlyCheckinDates(Long userId, Long childId) {
        LocalDate now = LocalDate.now();
        LocalDate firstDay = now.withDayOfMonth(1);
        LocalDate lastDay = now.withDayOfMonth(now.lengthOfMonth());
    
        List<FinanceCheckin> records = financeCheckinMapper.selectList(new LambdaQueryWrapper<FinanceCheckin>()
                .eq(FinanceCheckin::getUserId, userId)
                .eq(childId != null, FinanceCheckin::getChildId, childId)
                .ge(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(firstDay))
                .le(FinanceCheckin::getCheckinDate, java.sql.Date.valueOf(lastDay))
                .orderByAsc(FinanceCheckin::getCheckinDate));
    
        return records.stream().map(FinanceCheckin::getCheckinDate).collect(java.util.stream.Collectors.toList());
    }
    }
    
  • [ ] Step 2: InsurancePolicyService.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.InsurancePolicy;
    import com.etotem.cfc.mapper.InsurancePolicyMapper;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import javax.annotation.Resource;
    import java.util.Date;
    
    @Slf4j
    @Service
    public class InsurancePolicyService {
    
    @Resource
    private InsurancePolicyMapper insurancePolicyMapper;
    
    @Resource
    private EnergyService energyService;
    
    public Page<InsurancePolicy> getList(Long userId, int page, int size) {
        return insurancePolicyMapper.selectPage(new Page<>(page, size),
                new LambdaQueryWrapper<InsurancePolicy>()
                        .eq(InsurancePolicy::getUserId, userId)
                        .orderByDesc(InsurancePolicy::getCreatedAt));
    }
    
    public InsurancePolicy getDetail(Long id, Long userId) {
        InsurancePolicy policy = insurancePolicyMapper.selectById(id);
        if (policy != null && !policy.getUserId().equals(userId)) {
            return null;
        }
        return policy;
    }
    
    @Transactional
    public InsurancePolicy create(InsurancePolicy policy, Long userId) {
        policy.setUserId(userId);
        policy.setStatus(1);
        policy.setCreatedAt(new Date());
        policy.setUpdatedAt(new Date());
        insurancePolicyMapper.insert(policy);
    
        // 每份有效保单 +50 能量(一次性)
        // energyService.awardEnergy(userId, "insurance_policy", policy.getId(), 50, "parent", "上传保单");
    
        return policy;
    }
    
    @Transactional
    public void update(InsurancePolicy policy) {
        policy.setUpdatedAt(new Date());
        insurancePolicyMapper.updateById(policy);
    }
    
    @Transactional
    public void delete(Long id, Long userId) {
        insurancePolicyMapper.delete(new LambdaQueryWrapper<InsurancePolicy>()
                .eq(InsurancePolicy::getId, id)
                .eq(InsurancePolicy::getUserId, userId));
    }
    
    /**
     * 统计用户保单信息
     */
    public int countByUser(Long userId) {
        return insurancePolicyMapper.selectCount(new LambdaQueryWrapper<InsurancePolicy>()
                .eq(InsurancePolicy::getUserId, userId)
                .eq(InsurancePolicy::getStatus, 1));
    }
    }
    
  • [ ] Step 3: FinanceCheckinController.java

    package com.etotem.cfc.controller.wealth;
    
    import com.etotem.cfc.common.Result;
    import com.etotem.cfc.service.FinanceCheckinService;
    import org.springframework.web.bind.annotation.*;
    import javax.annotation.Resource;
    import java.util.Date;
    import java.util.List;
    import java.util.Map;
    
    @RestController
    @RequestMapping("/api/wealth")
    public class FinanceCheckinController {
    
    @Resource
    private FinanceCheckinService financeCheckinService;
    
    /**
     * 记账打卡
     */
    @PostMapping("/checkin")
    public Result<Integer> checkin(@RequestBody Map<String, Object> body,
                                    @RequestAttribute("userId") Long userId) {
        Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
        int energy = financeCheckinService.checkin(userId, childId);
        return Result.success(energy);
    }
    
    /**
     * 今日是否已记账
     */
    @PostMapping("/checkin/today")
    public Result<Boolean> todayStatus(@RequestBody Map<String, Object> body,
                                        @RequestAttribute("userId") Long userId) {
        Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
        boolean done = financeCheckinService.hasCheckedInToday(userId, childId);
        return Result.success(done);
    }
    
    /**
     * 本月打卡数据
     */
    @PostMapping("/checkin/monthly")
    public Result<Map<String, Object>> monthly(@RequestBody Map<String, Object> body,
                                                 @RequestAttribute("userId") Long userId) {
        Long childId = body.get("childId") != null ? Long.valueOf(body.get("childId").toString()) : null;
        int count = financeCheckinService.getMonthlyCheckinCount(userId, childId);
        List<Date> dates = financeCheckinService.getMonthlyCheckinDates(userId, childId);
        return Result.success(java.util.Map.of("count", count, "dates", dates));
    }
    }
    
  • [ ] Step 4: InsurancePolicyController.java

    package com.etotem.cfc.controller.wealth;
    
    import com.etotem.cfc.common.Result;
    import com.etotem.cfc.entity.InsurancePolicy;
    import com.etotem.cfc.service.InsurancePolicyService;
    import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
    import org.springframework.web.bind.annotation.*;
    import javax.annotation.Resource;
    import java.util.Map;
    
    @RestController
    @RequestMapping("/api/wealth")
    public class InsurancePolicyController {
    
    @Resource
    private InsurancePolicyService insurancePolicyService;
    
    @PostMapping("/insurance/list")
    public Result<Page<InsurancePolicy>> list(@RequestBody Map<String, Object> body,
                                               @RequestAttribute("userId") Long userId) {
        int page = body.get("page") != null ? Integer.parseInt(body.get("page").toString()) : 1;
        int size = body.get("size") != null ? Integer.parseInt(body.get("size").toString()) : 20;
        return Result.success(insurancePolicyService.getList(userId, page, size));
    }
    
    @PostMapping("/insurance/detail")
    public Result<InsurancePolicy> detail(@RequestBody Map<String, Object> body,
                                           @RequestAttribute("userId") Long userId) {
        Long id = Long.valueOf(body.get("id").toString());
        InsurancePolicy policy = insurancePolicyService.getDetail(id, userId);
        if (policy == null) return Result.error("保单不存在");
        return Result.success(policy);
    }
    
    @PostMapping("/insurance/create")
    public Result<String> create(@RequestBody InsurancePolicy policy,
                                  @RequestAttribute("userId") Long userId) {
        insurancePolicyService.create(policy, userId);
        return Result.success("添加成功");
    }
    
    @PostMapping("/insurance/update")
    public Result<String> update(@RequestBody InsurancePolicy policy) {
        insurancePolicyService.update(policy);
        return Result.success("更新成功");
    }
    
    @PostMapping("/insurance/delete")
    public Result<String> delete(@RequestBody Map<String, Object> body,
                                  @RequestAttribute("userId") Long userId) {
        Long id = Long.valueOf(body.get("id").toString());
        insurancePolicyService.delete(id, userId);
        return Result.success("删除成功");
    }
    
    @PostMapping("/insurance/count")
    public Result<Integer> count(@RequestAttribute("userId") Long userId) {
        return Result.success(insurancePolicyService.countByUser(userId));
    }
    }
    
  • [ ] Step 5: Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/service/FinanceCheckinService.java cfc-backend/src/main/java/com/etotem/cfc/service/InsurancePolicyService.java cfc-backend/src/main/java/com/etotem/cfc/controller/wealth/FinanceCheckinController.java cfc-backend/src/main/java/com/etotem/cfc/controller/wealth/InsurancePolicyController.java
    git commit -m "feat: add finance checkin and insurance policy services/controllers"
    

Task 4: 调整富维度默认比例和日上限

Files:

  • Modify: cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

  • [ ] Step 1: 调整 wealth 日上限

    // 原:DAILY_LIMIT_MAP.put("wealth", 30);
    // 改为:
    DAILY_LIMIT_MAP.put("wealth", 50);
    
  • [ ] Step 2: 调整默认比例(若已实现在 EnergyService 中)

找到 CATEGORY_DIMENSION_MAP 或其他默认比例定义:

// 默认任务中 wealth 从 0.05 改为 0.10
// "default" → body:0.30, mind:0.20, wisdom:0.15, action:0.25, wealth:0.10

实际修改位置取决于当前 EnergyService 中比例常量定义的位置。

  • [ ] Step 3: Commit

    git add cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java
    git commit -m "chore: adjust wealth daily limit to 50 and default ratio to 10%"
    

Task 5: 小程序 — 记账打卡页面

Files:

  • Create: cfc-frontend/pages/wealth/checkin.vue
  • Modify: cfc-frontend/utils/api.js
  • Modify: cfc-frontend/pages.json

  • [ ] Step 1: 在 api.js 添加 API

    // ===== 财富健康 =====
    export const wealthCheckin = (data) => request('/api/wealth/checkin', 'POST', data)
    export const wealthCheckinToday = (data) => request('/api/wealth/checkin/today', 'POST', data)
    export const wealthCheckinMonthly = (data) => request('/api/wealth/checkin/monthly', 'POST', data)
    export const wealthInsuranceList = (data) => request('/api/wealth/insurance/list', 'POST', data)
    export const wealthInsuranceDetail = (data) => request('/api/wealth/insurance/detail', 'POST', data)
    export const wealthInsuranceCreate = (data) => request('/api/wealth/insurance/create', 'POST', data)
    export const wealthInsuranceUpdate = (data) => request('/api/wealth/insurance/update', 'POST', data)
    export const wealthInsuranceDelete = (data) => request('/api/wealth/insurance/delete', 'POST', data)
    export const wealthInsuranceCount = () => request('/api/wealth/insurance/count', 'POST')
    
  • [ ] Step 2: 创建记账打卡页面

    <template>
    <view class="container">
    <view class="header">
      <text class="title">📒 记账打卡</text>
      <text class="subtitle">每天记账,培养财商</text>
    </view>
    
    <!-- 今日打卡状态 -->
    <view class="today-card" :class="{ done: todayCheckedIn }">
      <view class="today-icon">
        <text class="icon-text">{{ todayCheckedIn ? '✅' : '📝' }}</text>
      </view>
      <view class="today-info">
        <text class="today-label">{{ todayCheckedIn ? '今日已记账' : '今天记账了吗?' }}</text>
        <text class="today-energy" v-if="!todayCheckedIn">打卡 +3 能量</text>
        <text class="today-energy" v-else>连续打卡奖励已发放</text>
      </view>
      <button v-if="!todayCheckedIn" class="checkin-btn" @click="doCheckin">打卡</button>
    </view>
    
    <!-- 本月打卡日历 -->
    <view class="calendar-section">
      <text class="section-title">📅 本月打卡 {{ monthlyCount }} 天</text>
      <view class="calendar-grid">
        <view v-for="(day, i) in calendarDays" :key="i" class="calendar-day">
          <text class="day-num">{{ day }}</text>
          <view v-if="checkedInDates.includes(day)" class="day-dot checked"></view>
        </view>
      </view>
    </view>
    </view>
    </template>
    
    <script>
    import { wealthCheckin, wealthCheckinToday, wealthCheckinMonthly } from '@/utils/api.js'
    
    export default {
    data() {
    return {
      todayCheckedIn: false,
      monthlyCount: 0,
      checkedInDates: [],
      calendarDays: []
    }
    },
    onLoad() {
    this.loadData()
    this.generateCalendar()
    },
    methods: {
    generateCalendar() {
      const now = new Date()
      const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate()
      this.calendarDays = Array.from({ length: daysInMonth }, (_, i) => i + 1)
    },
    async loadData() {
      const [todayRes, monthlyRes] = await Promise.all([
        wealthCheckinToday({}),
        wealthCheckinMonthly({})
      ])
      if (todayRes.code === 200) this.todayCheckedIn = todayRes.data
      if (monthlyRes.code === 200) {
        this.monthlyCount = monthlyRes.data.count || 0
        this.checkedInDates = (monthlyRes.data.dates || []).map(d => new Date(d).getDate())
      }
    },
    async doCheckin() {
      const res = await wealthCheckin({})
      if (res.code === 200) {
        this.todayCheckedIn = true
        uni.showToast({ title: '打卡成功 +' + res.data + '能量', icon: 'success' })
        this.loadData()
      }
    }
    }
    }
    </script>
    
    <style scoped>
    .container { padding: 30rpx; }
    .header { text-align: center; margin-bottom: 40rpx; }
    .title { font-size: 36rpx; font-weight: bold; }
    .subtitle { font-size: 24rpx; color: #999; margin-top: 8rpx; }
    .today-card {
    display: flex; align-items: center; background: #fff;
    border-radius: 20rpx; padding: 30rpx; box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
    }
    .today-card.done { background: #f0fdf4; }
    .today-icon { width: 80rpx; height: 80rpx; display: flex; align-items: center; justify-content: center; }
    .icon-text { font-size: 48rpx; }
    .today-info { flex: 1; margin-left: 20rpx; }
    .today-label { font-size: 28rpx; font-weight: bold; color: #333; }
    .today-energy { font-size: 22rpx; color: #F97316; margin-top: 4rpx; }
    .checkin-btn {
    background: linear-gradient(135deg, #42A5F5, #1565C0);
    color: #fff; border: none; border-radius: 40rpx;
    padding: 12rpx 36rpx; font-size: 26rpx; font-weight: bold;
    }
    .calendar-section { margin-top: 40rpx; }
    .section-title { font-size: 28rpx; font-weight: bold; margin-bottom: 20rpx; }
    .calendar-grid {
    display: flex; flex-wrap: wrap; gap: 8rpx;
    background: #fff; border-radius: 16rpx; padding: 20rpx;
    }
    .calendar-day {
    width: calc(100% / 7 - 8rpx); text-align: center; padding: 8rpx 0;
    }
    .day-num { font-size: 24rpx; color: #666; }
    .day-dot { width: 12rpx; height: 12rpx; border-radius: 50%; margin: 4rpx auto 0; }
    .day-dot.checked { background: #4CAF50; }
    </style>
    
  • [ ] Step 3: 在 pages.json 注册路径

    {
    "path": "pages/wealth/checkin",
    "style": {
    "navigationBarTitleText": "记账打卡"
    }
    }
    
  • [ ] Step 4: Commit

    git add cfc-frontend/utils/api.js cfc-frontend/pages/wealth/checkin.vue cfc-frontend/pages.json
    git commit -m "feat: add finance checkin page (mini-program)"
    

Task 6: 小程序 — 保单管理页面

Files:

  • Create: cfc-frontend/pages/wealth/insurance-list.vue
  • Create: cfc-frontend/pages/wealth/insurance-add.vue

  • [ ] Step 1: 保单列表页

    <template>
    <view class="container">
    <view class="header">
      <text class="title">🛡️ 我的保单</text>
      <text class="subtitle">已有 {{ count }} 份保单</text>
    </view>
    
    <view v-if="loading" class="loading-wrap">
      <text class="loading-text">加载中...</text>
    </view>
    <view v-else-if="list.length === 0" class="empty-wrap">
      <text class="empty-icon">📄</text>
      <text class="empty-text">还没有保单,添加一份吧</text>
      <button class="add-btn" @click="goAdd">添加保单</button>
    </view>
    <scroll-view v-else scroll-y class="list-scroll">
      <view v-for="item in list" :key="item.id" class="policy-card">
        <view class="policy-header">
          <text class="policy-type">{{ typeLabel(item.policyType) }}</text>
          <text class="policy-status" :class="{ active: item.status === 1 }">
            {{ item.status === 1 ? '有效' : '已失效' }}
          </text>
        </view>
        <view class="policy-body">
          <text class="policy-name">被保人:{{ item.insuredName }}</text>
          <text class="policy-amount">保额:{{ item.coverageAmount }}万</text>
          <text class="policy-company">{{ item.insuranceCompany }}</text>
        </view>
        <view class="policy-footer">
          <text class="policy-date">{{ item.startDate }} ~ {{ item.endDate }}</text>
          <button class="del-btn" size="mini" @click="handleDelete(item.id)">删除</button>
        </view>
      </view>
      <view v-if="noMore" class="no-more">— 没有更多了 —</view>
    </scroll-view>
    </view>
    </template>
    
    <script>
    import { wealthInsuranceList, wealthInsuranceCount, wealthInsuranceDelete } from '@/utils/api.js'
    
    const TYPE_MAP = { medical: '医疗险', critical: '重疾险', accident: '意外险', life: '寿险', pension: '养老险', other: '其他' }
    
    export default {
    data() {
    return {
      list: [],
      count: 0,
      page: 1,
      size: 20,
      loading: false,
      noMore: false
    }
    },
    onLoad() { this.load() },
    methods: {
    typeLabel(t) { return TYPE_MAP[t] || t },
    async load() {
      this.loading = true
      const [listRes, countRes] = await Promise.all([
        wealthInsuranceList({ page: this.page, size: this.size }),
        wealthInsuranceCount()
      ])
      this.loading = false
      if (listRes.code === 200 && listRes.data) {
        this.list = listRes.data.records || []
        this.noMore = this.list.length >= (listRes.data.total || 0)
      }
      if (countRes.code === 200) this.count = countRes.data || 0
    },
    goAdd() { uni.navigateTo({ url: '/pages/wealth/insurance-add' }) },
    async handleDelete(id) {
      uni.showModal({
        title: '提示', content: '确定删除该保单?',
        success: async (res) => {
          if (res.confirm) {
            await wealthInsuranceDelete({ id })
            uni.showToast({ title: '删除成功', icon: 'success' })
            this.load()
          }
        }
      })
    }
    }
    }
    </script>
    
    <style scoped>
    .container { padding: 30rpx; }
    .header { margin-bottom: 30rpx; }
    .title { font-size: 36rpx; font-weight: bold; }
    .subtitle { font-size: 24rpx; color: #999; margin-top: 6rpx; }
    .loading-wrap, .empty-wrap { display: flex; flex-direction: column; align-items: center; padding-top: 120rpx; }
    .empty-icon { font-size: 100rpx; }
    .empty-text { font-size: 28rpx; color: #999; margin: 20rpx 0; }
    .add-btn {
    background: linear-gradient(135deg, #5B9BD5, #3A7CC4);
    color: #fff; border: none; border-radius: 40rpx;
    padding: 16rpx 60rpx; font-size: 28rpx;
    }
    .policy-card {
    background: #fff; border-radius: 16rpx; padding: 24rpx;
    margin-bottom: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.06);
    }
    .policy-header { display: flex; justify-content: space-between; align-items: center; }
    .policy-type { font-size: 28rpx; font-weight: bold; color: #333; }
    .policy-status { font-size: 22rpx; color: #999; }
    .policy-status.active { color: #4CAF50; }
    .policy-body { margin: 12rpx 0; }
    .policy-name, .policy-amount, .policy-company { display: block; font-size: 24rpx; color: #666; margin-top: 4rpx; }
    .policy-footer { display: flex; justify-content: space-between; align-items: center; margin-top: 12rpx; }
    .policy-date { font-size: 22rpx; color: #bbb; }
    .del-btn { color: #e74c3c; font-size: 22rpx; }
    .no-more { text-align: center; padding: 30rpx; color: #ccc; font-size: 24rpx; }
    </style>
    
  • [ ] Step 2: 添加保单页

    <template>
    <view class="container">
    <text class="title">添加保单</text>
    
    <view class="form">
      <view class="form-item">
        <text class="label">保单类型 *</text>
        <picker :range="typeOptions" range-key="label" @change="e => form.policyType = typeOptions[e.detail.value].value">
          <view class="picker">{{ typeLabel(form.policyType) || '请选择' }}</view>
        </picker>
      </view>
      <view class="form-item">
        <text class="label">被保人 *</text>
        <input v-model="form.insuredName" placeholder="姓名" class="input" />
      </view>
      <view class="form-item">
        <text class="label">与被保人关系</text>
        <picker :range="relationOptions" range-key="label" @change="e => form.insuredRelation = relationOptions[e.detail.value].value">
          <view class="picker">{{ relationLabel(form.insuredRelation) || '请选择' }}</view>
        </picker>
      </view>
      <view class="form-item">
        <text class="label">保险公司</text>
        <input v-model="form.insuranceCompany" placeholder="如:中国人寿" class="input" />
      </view>
      <view class="form-item">
        <text class="label">保额(万元)</text>
        <input v-model="form.coverageAmount" type="digit" placeholder="50" class="input" />
      </view>
      <view class="form-item">
        <text class="label">年缴保费</text>
        <input v-model="form.premium" type="digit" placeholder="5000" class="input" />
      </view>
      <view class="form-item">
        <text class="label">保单照片</text>
        <button class="upload-btn" @click="chooseImage">上传照片</button>
        <image v-if="form.imageUrl" :src="form.imageUrl" class="preview" mode="aspectFit" />
      </view>
      <button class="submit-btn" @click="handleSubmit">保存并获取能量</button>
    </view>
    </view>
    </template>
    
    <script>
    import { wealthInsuranceCreate } from '@/utils/api.js'
    
    const TYPE_OPTIONS = [
    { label: '医疗险', value: 'medical' }, { label: '重疾险', value: 'critical' },
    { label: '意外险', value: 'accident' }, { label: '寿险', value: 'life' },
    { label: '养老险', value: 'pension' }, { label: '其他', value: 'other' }
    ]
    const RELATION_OPTIONS = [
    { label: '本人', value: 'self' }, { label: '配偶', value: 'spouse' },
    { label: '子女', value: 'child' }, { label: '父母', value: 'parent' }
    ]
    const TYPE_MAP = { medical: '医疗险', critical: '重疾险', accident: '意外险', life: '寿险', pension: '养老险', other: '其他' }
    const RELATION_MAP = { self: '本人', spouse: '配偶', child: '子女', parent: '父母' }
    
    export default {
    data() {
    return {
      typeOptions: TYPE_OPTIONS,
      relationOptions: RELATION_OPTIONS,
      form: {
        policyType: '', insuredName: '', insuredRelation: 'self',
        insuranceCompany: '', policyNumber: '', coverageAmount: '',
        premium: '', imageUrl: ''
      }
    }
    },
    methods: {
    typeLabel(v) { return TYPE_MAP[v] },
    relationLabel(v) { return RELATION_MAP[v] },
    chooseImage() {
      uni.chooseImage({ count: 1, sizeType: ['compressed'] }).then(res => {
        const tempPath = res.tempFilePaths[0]
        // 上传图片
        uni.uploadFile({
          url: 'http://localhost:9080/api/admin/articles/upload/image', // TODO: use config.api
          filePath: tempPath, name: 'file',
          success: (uploadRes) => {
            const data = JSON.parse(uploadRes.data)
            if (data.code === 200) this.form.imageUrl = data.data
          }
        })
      })
    },
    async handleSubmit() {
      if (!this.form.policyType || !this.form.insuredName) {
        uni.showToast({ title: '请填写完整信息', icon: 'none' })
        return
      }
      const res = await wealthInsuranceCreate({
        ...this.form,
        coverageAmount: parseFloat(this.form.coverageAmount) || 0,
        premium: parseFloat(this.form.premium) || 0
      })
      if (res.code === 200) {
        uni.showToast({ title: '添加成功 +50能量', icon: 'success' })
        setTimeout(() => uni.navigateBack(), 1500)
      }
    }
    }
    }
    </script>
    
    <style scoped>
    .container { padding: 30rpx; }
    .title { font-size: 32rpx; font-weight: bold; margin-bottom: 30rpx; }
    .form-item { margin-bottom: 24rpx; }
    .label { font-size: 26rpx; color: #666; display: block; margin-bottom: 8rpx; }
    .input, .picker {
    width: 100%; padding: 16rpx 20rpx; border: 1rpx solid #eee;
    border-radius: 12rpx; font-size: 26rpx; box-sizing: border-box;
    }
    .picker { color: #333; }
    .upload-btn { font-size: 24rpx; padding: 10rpx 30rpx; background: #f0f0f0; border-radius: 8rpx; display: inline-block; }
    .preview { width: 200rpx; height: 280rpx; margin-top: 12rpx; border-radius: 8rpx; }
    .submit-btn {
    width: 100%; background: linear-gradient(135deg, #42A5F5, #1565C0);
    color: #fff; border: none; border-radius: 44rpx;
    padding: 24rpx 0; font-size: 30rpx; font-weight: bold; margin-top: 40rpx;
    }
    </style>
    
  • [ ] Step 3: 在 pages.json 注册路径

    {
    "path": "pages/wealth/insurance-list",
    "style": { "navigationBarTitleText": "我的保单" }
    },
    {
    "path": "pages/wealth/insurance-add",
    "style": { "navigationBarTitleText": "添加保单" }
    }
    
  • [ ] Step 4: Commit

    git add cfc-frontend/pages/wealth/insurance-list.vue cfc-frontend/pages/wealth/insurance-add.vue cfc-frontend/pages.json
    git commit -m "feat: add insurance policy management pages (mini-program)"
    

验证清单

  • 记账打卡
    • 首次点击打卡 → +3 能量,当天不可重复打卡
    • 连续7天 → 额外 +10 能量
    • 当月日历正确显示打卡日
  • 保单管理
    • 添加保单(填写类型/被保人/保额/上传照片) → +50 能量
    • 同一保单号不可重复添加
    • 列表/删除正常
  • 富维度比例
    • 默认任务 wealth 比例 10%
    • 财商类任务 wealth 比例 90%
    • 日上限 50
  • 后端编译: mvn clean compile 无错误
  • LSP 诊断: 所有文件无错误