# 整型金额迁移计划 — 禁止多精度类型
> **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:** 消除全栈所有 decimal/double 类型,使用整型(分/千分比)替代,前端统一做展示转换
**架构:** 分 4 阶段推进。P0 核心电商(Product/ProductOrder)→ P1 套餐/活动/服务(GuidePackage/Activity/ServiceContent)→ P2 支付/佣金/会员(PaymentOrder/Commission/Membership)→ P3 评分/配置/先天属性(Teacher rating/Energy ratio/Zodiac/Bazi)。数据库逐表迁移(新增 INT 列→迁移数据→删除旧 DECIMAL 列→重命名)。
**核心原则:**
- 金额以 **分** 存储(`1元 = 100`),前端展示 `(cents / 100).toFixed(2)`
- 百分比以 **千分比** 存储(`23.5% = 235`),前端展示 `(permillage / 10) + '%'`
- 评分以 **百分制整数** 存储(`4.5 = 450`),前端展示 `(value / 100).toFixed(1)`
- 比例/权重以 **万分比** 存储(`0.15 = 1500`),计算时除以 10000
- DTO 层字段名不变,类型统一为 `Integer`,前端做转换
**Tech Stack:** Java 8, Spring Boot 2.7, MyBatis-Plus, MySQL 8.0, uni-app Vue 2
---
## 文件改动总览
| 文件 | 操作 | 任务 | 优先级 |
|------|------|------|--------|
| `Product.java` | 修改 | T1 | P0 |
| `ProductDTO.java` | 修改 | T1 | P0 |
| `ProductOrder.java` | 修改 | T2 | P0 |
| `ProductOrderDTO.java` | 修改 | T2 | P0 |
| `ProductOrderService.java` | 修改 | T2 | P0 |
| `utils/format.js` | **新建** | T3 | P0 |
| 前端价格展示位置(6 处) | 修改 | T3 | P0 |
| `DatabaseInitializer.java` | 修改 | T4 | P0 |
| `AdminCommissionController.java` | 修改 | T4 | P0 |
| SQL 迁移脚本 | 新增 | T4 | P0 |
| `Activity.java` / `ActivityDTO.java` | 修改 | T5 | P1 |
| `ServiceContent.java` / DTOs | 修改 | T5 | P1 |
| `GuidePackage.java` / DTOs | 修改 | T5 | P1 |
| 套餐/服务相关 Service | 修改 | T5 | P1 |
| `PaymentOrder.java` / `PaymentOrderDTO.java` | 修改 | T6 | P2 |
| `CommissionRecord.java` / DTOs | 修改 | T6 | P2 |
| `CommissionService.java` | 修改 | T6 | P2 |
| `WithdrawalRequest.java` / `WithdrawalService.java` | 修改 | T6 | P2 |
| `MembershipLevel.java` / `MembershipService.java` | 修改 | T6 | P2 |
| `Guide.java` / `Teacher.java` rating | 修改 | T7 | P3 |
| `EnergyDimensionConfig.java` / `EnergySourceConfig.java` | 修改 | T7 | P3 |
| `ZodiacConfig.java` / `BaziConfig.java` / `BloodTypeConfig.java` | 修改 | T8 | P3 |
| `FamilyMemberAttributes.java` | 修改 | T8 | P3 |
| `FamilyMemberAttributeService.java` | 修改 | T8 | P3 |
---
## Phase 0 — 工具函数 (P0)
### Task 0: 新建前端金额格式化工具函数
**Files:**
- Create: `cfc-frontend/utils/format.js`
- [ ] **Step 1: 创建 format.js**
```javascript
/**
* 金额格式化工具
* 后端统一使用整型(分)存储金额,前端在此转换展示
*/
/**
* 分 → 元,保留2位小数
* @param {number|null} cents 金额(分)
* @returns {string|null} "12.50" 或 null
*/
export function formatPrice(cents) {
if (cents == null || cents === undefined) return null
return (cents / 100).toFixed(2)
}
/**
* 分 → 元,带 ¥ 前缀
* @param {number|null} cents 金额(分)
* @returns {string} "¥12.50" 或 "登录查看"
*/
export function formatPriceWithSymbol(cents) {
if (cents == null || cents === undefined) return '登录查看'
return '¥' + (cents / 100).toFixed(2)
}
/**
* 千分比 → 百分比字符串
* @param {number|null} permillage 千分比(235 = 23.5%)
* @returns {string} "23.5%" 或 "--"
*/
export function formatRate(permillage) {
if (permillage == null || permillage === undefined) return '--'
return (permillage / 10) + '%'
}
/**
* 百分制评分 → 展示值
* @param {number|null} value 百分制(450 = 4.5分)
* @param {number} fixed 小数位数
* @returns {string} "4.5" 或 "--"
*/
export function formatScore(value, fixed) {
if (fixed === undefined) fixed = 1
if (value == null || value === undefined) return '--'
return (value / 100).toFixed(fixed)
}
```
- [ ] **Step 2: 编译验证**
运行 `lsp_diagnostics` 确认无语法错误。
---
## Phase 1 — 核心电商金额 (P0 🔴)
### Task 1: Product 实体 + DTO — BigDecimal → Integer(分)
**Files:**
- Modify: `cfc-backend/src/main/java/com/etotem/cfc/entity/Product.java`
- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductDTO.java`
- [ ] **Step 1: Product.java 字段类型修改**
```java
// 修改前
import java.math.BigDecimal;
// ...
private BigDecimal price;
private BigDecimal memberPrice;
private BigDecimal profitRate;
// 修改后 — 删除 import java.math.BigDecimal;
// ...
private Integer price; // 价格(分),展示时 /100
private Integer memberPrice; // 会员价(分)
private Integer profitRate; // 利润率(千分比),23.5% → 235
```
- [ ] **Step 2: ProductDTO.java 同步修改**
```java
// 删除 import java.math.BigDecimal;
private Integer price;
private Integer memberPrice;
private Integer profitRate;
```
检查 DTO 中的转换逻辑(如有 `d.price = p.getPrice()` 等),类型改为 `Integer` 后赋值不变(`Integer` → `Integer` 无需转换)。
- [ ] **Step 3: 编译验证**
```bash
cd cfc-backend && mvn clean compile -q
```
预期:`BUILD SUCCESS`(仅改 entity + dto 无编译依赖错误)
---
### Task 2: ProductOrder 实体 + DTO + Service — 整型金额
**Files:**
- Modify: `cfc-backend/src/main/java/com/etotem/cfc/entity/ProductOrder.java`
- Modify: `cfc-backend/src/main/java/com/etotem/cfc/dto/ProductOrderDTO.java`
- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java`
- [ ] **Step 1: ProductOrder.java 字段类型修改**
```java
// 删除 import java.math.BigDecimal;
private Integer unitPrice; // 单价(分)
private Integer totalAmount; // 总价(分)
```
- [ ] **Step 2: ProductOrderDTO.java 同步修改**
```java
// 删除 import java.math.BigDecimal;
private Integer unitPrice;
private Integer totalAmount;
```
- [ ] **Step 3: ProductOrderService.java 计算逻辑更新**
```java
// 修改前 (约第65行):
order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(dto.getQuantity())));
// 修改后:
order.setUnitPrice(product.getPrice());
order.setTotalAmount(product.getPrice() * dto.getQuantity());
```
- [ ] **Step 4: 编译验证**
```bash
cd cfc-backend && mvn clean compile -q
```
预期:`BUILD SUCCESS`
---
### Task 3: 前端价格展示 — 全部改用 formatPriceWithSymbol
**Files:**
- Modify: `cfc-frontend/pages/index/index.vue`(未登录态商品列表)
- Modify: `cfc-frontend/components/DimensionProducts.vue`
- Modify: `cfc-frontend/pages/body/index.vue`(如有直接价格渲染)
- Modify: `cfc-frontend/pages/mind/index.vue`(如有直接价格渲染)
- Modify: `cfc-frontend/pages/action/index.vue`(如有直接价格渲染)
- Modify: `cfc-frontend/pages/shop/index.vue`(商城价格)
- [ ] **Step 1: 在 utils/api.js 或相关位置引入 format 工具**
在 `main.js` 中全局注册(或在各页面分别 import):
```javascript
// main.js
import { formatPriceWithSymbol, formatPrice, formatRate } from '@/utils/format'
// 挂载到 Vue 原型,所有组件可通过 this.formatPriceWithSymbol() 访问
Vue.prototype.formatPriceWithSymbol = formatPriceWithSymbol
Vue.prototype.formatPrice = formatPrice
Vue.prototype.formatRate = formatRate
```
- [ ] **Step 2: 逐文件替换价格渲染**
对所有前端文件中形如 `¥{{ item.price }}` 的渲染,改为 `{{ formatPriceWithSymbol(item.price) }}`。
**搜索清单(grep 查找 `¥{{` 或 `¥ ` 在 .vue 文件中):**
```bash
grep -rn '¥' cfc-frontend/pages/ cfc-frontend/components/ --include='*.vue'
```
每个匹配位置都需修改:
```vue
¥{{ item.price }}
{{ formatPriceWithSymbol(item.price) }}
```
- [ ] **Step 3: 检查 v-if 条件**
```vue
¥{{ item.price }}
{{ formatPriceWithSymbol(item.price) }}
会员{{ formatPriceWithSymbol(item.memberPrice) }}
```
- [ ] **Step 4: LSP 诊断**
运行 `lsp_diagnostics` 确认无语法错误。
---
### Task 4: 后端 Controller + DatabaseInitializer + SQL 迁移
**Files:**
- Modify: `cfc-backend/.../controller/product/ProductController.java`
- Modify: `cfc-backend/.../config/DatabaseInitializer.java`
- Modify: `cfc-backend/.../controller/admin/AdminCommissionController.java`
- Create: `cfc-backend/src/main/resources/db/migration/V20260615__integer_price.sql`
- [ ] **Step 1: ProductController 价格置 null 逻辑**
```java
// 游客隐藏价格逻辑不变,类型已改为 Integer
public Result> list(@RequestBody(required = false) Map params,
@RequestAttribute(value = "userId", required = false) Long userId) {
// ... 正常查询逻辑
List productList = ...;
if (userId == null) {
for (ProductDTO dto : productList) {
dto.setPrice(null);
dto.setMemberPrice(null);
}
}
return Result.success(productList);
}
```
类型可赋值 `null`,逻辑无需改动(只需确认编译通过)。
- [ ] **Step 2: DatabaseInitializer — BigDecimal → Integer**
在 `DatabaseInitializer.java` 中找到种子数据插入(约第 982-986 行):
```java
// 修改前:
insertProductSeed("亲子关系工作坊", "...", "activity", new java.math.BigDecimal("388.00"), "action", "all");
// 修改后: 388.00元 = 38800分
insertProductSeed("亲子关系工作坊", "...", "activity", 38800, "action", "all");
```
检查 `insertProductSeed` 方法签名,将参数类型从 `java.math.BigDecimal` 改为 `Integer`(或确认重载版本)。
```java
// 方法签名修改前:
private void insertProductSeed(String name, String desc, String type, java.math.BigDecimal price, String domain, String budget) {
// 方法签名修改后:
private void insertProductSeed(String name, String desc, String type, Integer price, String domain, String budget) {
```
所有种子数据价格转换:
| 原值 | 分 |
|------|---|
| 388.00 | 38800 |
| 199.00 | 19900 |
| 599.00 | 59900 |
| 99.00 | 9900 |
| 500.00 | 50000 |
- [ ] **Step 3: AdminCommissionController 整型化**
```java
// 修改前 (约第61-71行):
BigDecimal profitRate = params.get("profitRate") != null
? new BigDecimal(params.get("profitRate").toString())
: null;
if (productId == null || profitRate == null) {
return Result.error("productId和profitRate不能为空");
}
// ...
product.setProfitRate(profitRate);
// 修改后:
Integer profitRate = params.get("profitRate") != null
? Integer.valueOf(params.get("profitRate").toString())
: null;
if (productId == null || profitRate == null) {
return Result.error("productId和profitRate不能为空");
}
// ...
product.setProfitRate(profitRate);
```
- [ ] **Step 4: 数据库迁移 SQL**
```sql
-- V20260615__integer_price.sql
-- ========================================
-- Phase 1: 核心电商表
-- ========================================
-- products: price, member_price, profit_rate
ALTER TABLE products
ADD COLUMN price_int INT DEFAULT 0 COMMENT '价格(分)',
ADD COLUMN member_price_int INT DEFAULT 0 COMMENT '会员价(分)',
ADD COLUMN profit_rate_int INT DEFAULT 0 COMMENT '利润率(千分比)';
UPDATE products SET price_int = ROUND(price * 100);
UPDATE products SET member_price_int = ROUND(member_price * 100);
UPDATE products SET profit_rate_int = ROUND(profit_rate * 10);
ALTER TABLE products
DROP COLUMN price,
DROP COLUMN member_price,
DROP COLUMN profit_rate;
ALTER TABLE products
CHANGE COLUMN price_int price INT DEFAULT 0 COMMENT '价格(分)',
CHANGE COLUMN member_price_int member_price INT DEFAULT 0 COMMENT '会员价(分)',
CHANGE COLUMN profit_rate_int profit_rate INT DEFAULT 0 COMMENT '利润率(千分比)';
-- product_orders: unit_price, total_amount
ALTER TABLE product_orders
ADD COLUMN unit_price_int INT DEFAULT 0 COMMENT '单价(分)',
ADD COLUMN total_amount_int INT DEFAULT 0 COMMENT '总价(分)';
UPDATE product_orders SET unit_price_int = ROUND(unit_price * 100);
UPDATE product_orders SET total_amount_int = ROUND(total_amount * 100);
ALTER TABLE product_orders
DROP COLUMN unit_price,
DROP COLUMN total_amount;
ALTER TABLE product_orders
CHANGE COLUMN unit_price_int unit_price INT DEFAULT 0 COMMENT '单价(分)',
CHANGE COLUMN total_amount_int total_amount INT DEFAULT 0 COMMENT '总价(分)';
```
- [ ] **Step 5: 编译验证**
```bash
cd cfc-backend && mvn clean compile -q
```
预期:`BUILD SUCCESS`
---
## Phase 2 — 活动/套餐/服务价格 (P1 🟠)
### Task 5: Activity + ServiceContent + GuidePackage 整型化
**Files:**
- Modify: `cfc-backend/.../entity/Activity.java`(price)
- Modify: `cfc-backend/.../dto/ActivityDTO.java`(price)
- Modify: `cfc-backend/.../entity/ServiceContent.java`(price)
- Modify: `cfc-backend/.../entity/GuidePackage.java`(price, commissionRate)
- Modify: `cfc-backend/.../entity/GuidePackageTemplate.java`(referencePrice, minPrice)
- Modify: `cfc-backend/.../entity/TaskTemplatePackage.java`(price, platformFee)
- Modify: `cfc-backend/.../entity/Guide.java`(commissionRate)
- Modify: `cfc-backend/.../entity/GuideFamily.java`(servicePrice)
- Modify: `cfc-backend/.../entity/GuideApplication.java`(paymentAmount)
- Modify: 上述实体对应的所有 DTO 文件
- Modify: 相关 Controller/Service(调整 `new BigDecimal()` → `Integer.valueOf()`)
- [ ] **Step 1: 实体字段逐类修改**
每个实体执行同样的模式:
```java
// Activity.java — 例
// 删除 import java.math.BigDecimal;
private Integer price; // 价格(分)
```
DTO 同步修改,注意 DTO 中的转换赋值不变(`Integer` ↔ `Integer`)。
- [ ] **Step 2: Controller 中 BigDecimal 构造替换**
搜索所有 Controller 中 `new BigDecimal(params.get("price").toString())` 模式:
```bash
grep -rn 'new BigDecimal' cfc-backend/src/main/java/com/etotem/cfc/controller/
```
将每个匹配位置改为 `Integer.valueOf()` 或 `Integer.parseInt()`:
```java
// 修改前:
java.math.BigDecimal price = new java.math.BigDecimal(params.get("price").toString());
// 修改后:
Integer price = params.get("price") != null
? Integer.valueOf(params.get("price").toString())
: null;
```
- [ ] **Step 3: Service 中的金额计算调整**
搜索 Service 中使用 `BigDecimal` 进行金额计算的代码:
```bash
grep -rn '\.multiply\|\.divide\|\.add\|\.subtract\|new BigDecimal' cfc-backend/src/main/java/com/etotem/cfc/service/ --include='*.java'
```
需要重点调整的位置:
**TaskTemplatePackageService.java:**
```java
// 修改前:
pkg.setPlatformFee(dto.getPrice().multiply(new BigDecimal("0.10")));
// 修改后: 平台服务费 = 价格 × 10%(结果以分存储)
pkg.setPlatformFee(dto.getPrice() * 10 / 100);
```
**GuidePackageService.java:**
```java
// 方法签名:
// 旧: boolean addGuidePackage(Long guideId, Long packageId, BigDecimal price, BigDecimal commissionRate)
// 新: boolean addGuidePackage(Long guideId, Long packageId, Integer price, Integer commissionRate)
```
**BindInviteService.java:**
```java
// 旧: gf.setServicePrice(BigDecimal.ZERO);
// 新:
gf.setServicePrice(0);
```
- [ ] **Step 4: 数据库迁移 SQL**
```sql
-- Phase 2 SQL
-- activities.price
ALTER TABLE activity ADD COLUMN price_int INT DEFAULT 0;
UPDATE activity SET price_int = ROUND(price * 100);
ALTER TABLE activity DROP COLUMN price;
ALTER TABLE activity CHANGE COLUMN price_int price INT DEFAULT 0;
-- guide_packages.price, commission_rate
ALTER TABLE guide_packages ADD COLUMN price_int INT DEFAULT 0, ADD COLUMN commission_rate_int INT DEFAULT 0;
UPDATE guide_packages SET price_int = ROUND(price * 100), commission_rate_int = ROUND(commission_rate * 1000);
ALTER TABLE guide_packages DROP COLUMN price, DROP COLUMN commission_rate;
ALTER TABLE guide_packages CHANGE COLUMN price_int price INT DEFAULT 0, CHANGE COLUMN commission_rate_int commission_rate INT DEFAULT 0;
-- 后续表同理...
```
- [ ] **Step 5: 编译验证**
```bash
cd cfc-backend && mvn clean compile -q
```
预期:`BUILD SUCCESS`
---
## Phase 3 — 支付/佣金/会员 (P2 🟡)
### Task 6: PaymentOrder + Commission + Membership + Withdrawal
**Files:**
- Modify: `PaymentOrder.java`, `CommissionRecord.java`, `WithdrawalRequest.java`, `MembershipLevel.java`, `FamilyMembership.java`, `FinanceCheckin.java`, `InsurancePolicy.java`
- Modify: 上述实体的 DTO
- Modify: `CommissionService.java`(整型化全部计算逻辑)
- Modify: `MembershipService.java`
- Modify: `WithdrawalService.java`
- Modify: `PackagePaymentService.java`
- Modify: `PackagePurchaseService.java`
> **注意:** Phase 3 涉及复杂的佣金计算(CommissionService 约 470 行,大量 BigDecimal 计算),**改造风险较高**。建议的改造策略:
> 1. 所有金额字段改为整型(分)
> 2. 百分比/费率字段改为万分比(1% = 100bps)
> 3. 计算时:`amount * rate_bps / 10000`(整型除法,避免精度问题)
> 4. 顺序:先改 Entity/DTO 保证编译通过 → 再逐个 Service 改造 → 最后改测试
- [ ] **Step 1: Entity + DTO 逐类修改**
按 Task 1/2 的模式,逐个文件将 `BigDecimal` 字段改为 `Integer`。
- [ ] **Step 2: 佣金计算逻辑重构(CommissionService.java)**
将浮动百分比改为 BPS(基点)模式:
```java
// 旧模式:BigDecimal 乘除
private static final BigDecimal PROFIT_COMMISSION_RATE = new BigDecimal("0.30");
l1Amount = orderAmount.multiply(profitRate).divide(new BigDecimal("100"));
// 新模式:整型 BPS(基点)
private static final int PROFIT_COMMISSION_RATE_BPS = 3000; // 30%
// 返利 = 订单金额(分) × 利润率(千分比) × 佣金比例(bps) / 10000 / 1000
// 简化: l1Amount = orderAmount * profitRatePermillage * rateBps / 10000000
```
> 注意:CommissionService 的计算涉及多层乘除,整型化后需特别注意溢出(用 `long` 中间值)。建议先在 `CommissionService.java` 中引入 `SafeMath` 辅助方法。
- [ ] **Step 3: 编译验证**
```bash
cd cfc-backend && mvn clean compile -q
```
预期:`BUILD SUCCESS`
- [ ] **Step 4: 数据库迁移 SQL**
```sql
-- Phase 3: payment_orders.amount
ALTER TABLE payment_orders ADD COLUMN amount_int INT DEFAULT 0;
UPDATE payment_orders SET amount_int = ROUND(amount * 100);
ALTER TABLE payment_orders DROP COLUMN amount;
ALTER TABLE payment_orders CHANGE COLUMN amount_int amount INT DEFAULT 0;
-- commission_records.order_amount, profit_rate, commission_amount
ALTER TABLE commission_records ADD COLUMN order_amount_int INT DEFAULT 0, ADD COLUMN profit_rate_int INT DEFAULT 0, ADD COLUMN commission_amount_int INT DEFAULT 0;
UPDATE commission_records SET order_amount_int = ROUND(order_amount * 100), profit_rate_int = ROUND(profit_rate * 1000), commission_amount_int = ROUND(commission_amount * 100);
ALTER TABLE commission_records DROP COLUMN order_amount, DROP COLUMN profit_rate, DROP COLUMN commission_amount;
ALTER TABLE commission_records CHANGE COLUMN order_amount_int order_amount INT DEFAULT 0, CHANGE COLUMN profit_rate_int profit_rate INT DEFAULT 0, CHANGE COLUMN commission_amount_int commission_amount INT DEFAULT 0;
-- 其他表同理...
```
---
## Phase 4 — 评分/配置/先天属性 (P3 🟢)
### Task 7: Teacher rating + Energy 配置
**Files:**
- Modify: `Teacher.java`, `TeacherDTO.java`(rating: 百分制整数, 4.5 = 450)
- Modify: `EnergyDimensionConfig.java`(weight: 万分比, 0.25 = 2500)
- Modify: `EnergySourceConfig.java`(ratio: 万分比)
- Modify: `EnergyService.java`(计算逻辑整型化)
- Modify: `AssessmentPointsConfig.java`(pointsValueYuan: 分)
- Modify: `AssessmentService.java`
- [ ] **Step 1: 实体字段整型化**
```java
// Teacher.java
// 旧: private java.math.BigDecimal rating; // 评分
// 新:
private Integer rating; // 评分(百分制),4.5分 → 450
// EnergyDimensionConfig.java
// 旧: private BigDecimal weight;
// 新:
private Integer weight; // 权重(万分比),0.25 → 2500
// EnergySourceConfig.java
// 旧: private BigDecimal ratio;
// 新:
private Integer ratio; // 比例(万分比)
// AssessmentPointsConfig.java
// 旧: private BigDecimal pointsValueYuan;
// 新:
private Integer pointsValueYuan; // 分值(分)
```
- [ ] **Step 2: EnergyService 计算逻辑重构**
`EnergyService.java` 中的 `resolveDimensionRatios` 和 `calculateAllocations` 使用 `BigDecimal` 进行比例计算:
```java
// 旧:
Map ratios = ...; //
BigDecimal totalRatio = ratios.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal share = BigDecimal.valueOf(total).multiply(entry.getValue()).divide(totalRatio, 0, RoundingMode.DOWN);
// 新:
Map ratios = ...; //
long totalRatio = ratios.values().stream().mapToLong(Integer::longValue).sum();
long share = (long) total * entry.getValue() / totalRatio; // 整型运算
```
- [ ] **Step 3: 编译验证**
```bash
cd cfc-backend && mvn clean compile -q
```
预期:`BUILD SUCCESS`
---
### Task 8: 先天属性 (Zodiac/Bazi/BloodType/FamilyMemberAttributes)
**Files:**
- Modify: `ZodiacConfig.java`, `BaziConfig.java`, `BloodTypeConfig.java`(所有 `BigDecimal` → `Integer` 万分比)
- Modify: `FamilyMemberAttributes.java`(mindBaseScore/wisdomBaseScore: 百分制整数)
- Modify: `FamilyMemberAttributeService.java`(`calcInnateScore` 整型化)
- [ ] **Step 1: 配置实体整型化**
```java
// ZodiacConfig.java / BaziConfig.java / BloodTypeConfig.java
// 旧: private BigDecimal mindBase; // 基数 0.85
// 新:
private Integer mindBase; // 万分比基数,0.85 → 8500
private Integer wisdomBase; // 万分比
private Integer bodyModifier; // 万分比
private Integer actionModifier;
private Integer wealthModifier;
```
- [ ] **Step 2: FamilyMemberAttributeService 计算重构**
`calcInnateScore` 方法中大量使用 `BigDecimal` 乘除:
```java
// 旧:
BigDecimal bloodScore = BigDecimal.ZERO;
BigDecimal result = zodiacScore.multiply(BigDecimal.valueOf(0.5))
.add(baziScore.multiply(BigDecimal.valueOf(0.3)))
.add(bloodScore.multiply(BigDecimal.valueOf(0.2)));
// 新: 权重使用千分比,结果使用百分制整数
// zodiac占比50% = 500‰, bazi 30% = 300‰, blood 20% = 200‰
int result = (zodiacScore * 500 + baziScore * 300 + bloodScore * 200) / 1000;
```
- [ ] **Step 3: 编译验证**
```bash
cd cfc-backend && mvn clean compile -q
```
预期:`BUILD SUCCESS`
---
## 验证总表
| # | 验证项 | 预期 | 检查方法 |
|---|--------|------|----------|
| 1 | 后端编译 | BUILD SUCCESS | `mvn clean compile -q` |
| 2 | entity 中无 BigDecimal | 0 个 import java.math.BigDecimal | `grep -r 'import java.math.BigDecimal' src/main/java/com/etotem/cfc/entity/ \| wc -l` |
| 3 | dto 中无 BigDecimal | 0 个 import | `grep -r 'import java.math.BigDecimal' src/main/java/com/etotem/cfc/dto/ \| wc -l` |
| 4 | 前端无 ¥{{ 直接渲染 | 0 个匹配 | `grep -rn '¥{{' cfc-frontend/pages/ cfc-frontend/components/` |
| 5 | 金额展示使用 formatPriceWithSymbol | 所有 ¥ 前缀位置使用函数 | 人工审查 |
| 6 | 数据库无 DECIMAL 列 | 0 个 DECIMAL | `SHOW COLUMNS FROM products;` 等 |
| 7 | 端到端价格展示 | 商品价格显示 ¥388.00 | 小程序截图对比 |
| 8 | 端到端游客价格 | 显示"登录查看" | 未登录状态浏览 |
---
## 外置参考 — 全部 BigDecimal 出现位置
执行以下命令确认最终清理状态:
```bash
# 后端全部 BigDecimal 引用(期望最终为 0)
grep -rn 'import java.math.BigDecimal' cfc-backend/src/main/java/ --include='*.java'
# 前端全部 ¥ 渲染(期望最终只剩通过 formatPriceWithSymbol 的调用)
grep -rn '¥' cfc-frontend/pages/ cfc-frontend/components/ --include='*.vue'
```
## 迁移风险矩阵
| 风险 | 影响范围 | 缓解措施 |
|------|----------|----------|
| Integer 溢出(金额*BPS 计算) | CommissionService | 中间值使用 `long`/`Long`,关键点加注释 |
| 精度损失(除不尽) | EnergyService 分配 | 大数优先分配,余数补到最大维度 |
| seed 数据价格不对 | DatabaseInitializer | 逐行双检(388.00→38800) |
| 前端漏改 | 所有 Tab 页 | `grep ¥{{` + 人工审查 |
| 历史数据迁移失败 | 生产库 | 迁移前备份,执行后 SELECT 抽样对比 |