|
|
@@ -0,0 +1,991 @@
|
|
|
+# 富维度(财富健康)实现计划
|
|
|
+
|
|
|
+> **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() 中添加建表语句**
|
|
|
+
|
|
|
+```java
|
|
|
+// 记账打卡表
|
|
|
+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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```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**
|
|
|
+
|
|
|
+```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**
|
|
|
+
|
|
|
+```java
|
|
|
+@Mapper
|
|
|
+public interface FinanceCheckinMapper extends BaseMapper<FinanceCheckin> {}
|
|
|
+
|
|
|
+@Mapper
|
|
|
+public interface InsurancePolicyMapper extends BaseMapper<InsurancePolicy> {}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```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**
|
|
|
+
|
|
|
+```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**
|
|
|
+
|
|
|
+```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**
|
|
|
+
|
|
|
+```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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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 日上限**
|
|
|
+
|
|
|
+```java
|
|
|
+// 原:DAILY_LIMIT_MAP.put("wealth", 30);
|
|
|
+// 改为:
|
|
|
+DAILY_LIMIT_MAP.put("wealth", 50);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 2: 调整默认比例(若已实现在 EnergyService 中)**
|
|
|
+
|
|
|
+找到 `CATEGORY_DIMENSION_MAP` 或其他默认比例定义:
|
|
|
+
|
|
|
+```java
|
|
|
+// 默认任务中 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**
|
|
|
+
|
|
|
+```bash
|
|
|
+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**
|
|
|
+
|
|
|
+```javascript
|
|
|
+// ===== 财富健康 =====
|
|
|
+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: 创建记账打卡页面**
|
|
|
+
|
|
|
+```vue
|
|
|
+<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 注册路径**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "path": "pages/wealth/checkin",
|
|
|
+ "style": {
|
|
|
+ "navigationBarTitleText": "记账打卡"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+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: 保单列表页**
|
|
|
+
|
|
|
+```vue
|
|
|
+<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: 添加保单页**
|
|
|
+
|
|
|
+```vue
|
|
|
+<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 注册路径**
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "path": "pages/wealth/insurance-list",
|
|
|
+ "style": { "navigationBarTitleText": "我的保单" }
|
|
|
+},
|
|
|
+{
|
|
|
+ "path": "pages/wealth/insurance-add",
|
|
|
+ "style": { "navigationBarTitleText": "添加保单" }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **Step 4: Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+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 诊断**: 所有文件无错误
|