2026-06-15-integer-price-migration.md 25 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: 消除全栈所有 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

    /**
    * 金额格式化工具
    * 后端统一使用整型(分)存储金额,前端在此转换展示
    */
    
    /**
    * 分 → 元,保留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 字段类型修改

    // 修改前
    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 同步修改

    // 删除 import java.math.BigDecimal;
    private Integer price;
    private Integer memberPrice;
    private Integer profitRate;
    

检查 DTO 中的转换逻辑(如有 d.price = p.getPrice() 等),类型改为 Integer 后赋值不变(IntegerInteger 无需转换)。

  • [ ] Step 3: 编译验证

    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 字段类型修改

    // 删除 import java.math.BigDecimal;
    private Integer unitPrice;    // 单价(分)
    private Integer totalAmount;  // 总价(分)
    
  • [ ] Step 2: ProductOrderDTO.java 同步修改

    // 删除 import java.math.BigDecimal;
    private Integer unitPrice;
    private Integer totalAmount;
    
  • [ ] Step 3: ProductOrderService.java 计算逻辑更新

    // 修改前 (约第65行):
    order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(dto.getQuantity())));
    
    // 修改后:
    order.setUnitPrice(product.getPrice());
    order.setTotalAmount(product.getPrice() * dto.getQuantity());
    
  • [ ] Step 4: 编译验证

    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):

// 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 文件中):

grep -rn '¥' cfc-frontend/pages/ cfc-frontend/components/ --include='*.vue'

每个匹配位置都需修改:

<!-- 修改前 -->
<text class="product-price">¥{{ item.price }}</text>

<!-- 修改后 -->
<text class="product-price">{{ formatPriceWithSymbol(item.price) }}</text>
  • [ ] Step 3: 检查 v-if 条件

    <!-- 修改前 — BigDecimal 用 v-if="item.price" 检查非零 -->
    <view v-if="item.price">¥{{ item.price }}</view>
    
    <!-- 修改后 — Integer 用 != null 检查,0 元商品显示 "¥0.00" 而不是隐藏 -->
    <view v-if="item.price != null">{{ formatPriceWithSymbol(item.price) }}</view>
    
    <!-- 会员价同理 -->
    <text v-if="item.memberPrice != null">会员{{ formatPriceWithSymbol(item.memberPrice) }}</text>
    
  • [ ] 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 逻辑

    // 游客隐藏价格逻辑不变,类型已改为 Integer
    public Result<?> list(@RequestBody(required = false) Map<String, Object> params,
                       @RequestAttribute(value = "userId", required = false) Long userId) {
    // ... 正常查询逻辑
        
    List<ProductDTO> 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 行):

// 修改前:
insertProductSeed("亲子关系工作坊", "...", "activity", new java.math.BigDecimal("388.00"), "action", "all");

// 修改后: 388.00元 = 38800分
insertProductSeed("亲子关系工作坊", "...", "activity", 38800, "action", "all");

检查 insertProductSeed 方法签名,将参数类型从 java.math.BigDecimal 改为 Integer(或确认重载版本)。

// 方法签名修改前:
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 整型化

    // 修改前 (约第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

    -- 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: 编译验证

    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: 实体字段逐类修改

每个实体执行同样的模式:

// Activity.java — 例
// 删除 import java.math.BigDecimal;
private Integer price;  // 价格(分)

DTO 同步修改,注意 DTO 中的转换赋值不变(IntegerInteger)。

  • Step 2: Controller 中 BigDecimal 构造替换

搜索所有 Controller 中 new BigDecimal(params.get("price").toString()) 模式:

grep -rn 'new BigDecimal' cfc-backend/src/main/java/com/etotem/cfc/controller/

将每个匹配位置改为 Integer.valueOf()Integer.parseInt()

// 修改前:
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 进行金额计算的代码:

grep -rn '\.multiply\|\.divide\|\.add\|\.subtract\|new BigDecimal' cfc-backend/src/main/java/com/etotem/cfc/service/ --include='*.java'

需要重点调整的位置:

TaskTemplatePackageService.java:

// 修改前:
pkg.setPlatformFee(dto.getPrice().multiply(new BigDecimal("0.10")));
// 修改后: 平台服务费 = 价格 × 10%(结果以分存储)
pkg.setPlatformFee(dto.getPrice() * 10 / 100);

GuidePackageService.java:

// 方法签名:
// 旧: boolean addGuidePackage(Long guideId, Long packageId, BigDecimal price, BigDecimal commissionRate)
// 新: boolean addGuidePackage(Long guideId, Long packageId, Integer price, Integer commissionRate)

BindInviteService.java:

// 旧: gf.setServicePrice(BigDecimal.ZERO);
// 新:
gf.setServicePrice(0);
  • [ ] Step 4: 数据库迁移 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: 编译验证

    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(基点)模式:

// 旧模式: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: 编译验证

    cd cfc-backend && mvn clean compile -q
    

预期:BUILD SUCCESS

  • [ ] Step 4: 数据库迁移 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: 实体字段整型化

    // 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 中的 resolveDimensionRatioscalculateAllocations 使用 BigDecimal 进行比例计算:

// 旧:
Map<Long, BigDecimal> ratios = ...;  // <dimId, weight>
BigDecimal totalRatio = ratios.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal share = BigDecimal.valueOf(total).multiply(entry.getValue()).divide(totalRatio, 0, RoundingMode.DOWN);

// 新:
Map<Long, Integer> ratios = ...;  // <dimId, weight-in-permyriad>
long totalRatio = ratios.values().stream().mapToLong(Integer::longValue).sum();
long share = (long) total * entry.getValue() / totalRatio;  // 整型运算
  • [ ] Step 3: 编译验证

    cd cfc-backend && mvn clean compile -q
    

预期:BUILD SUCCESS


Task 8: 先天属性 (Zodiac/Bazi/BloodType/FamilyMemberAttributes)

Files:

  • Modify: ZodiacConfig.java, BaziConfig.java, BloodTypeConfig.java(所有 BigDecimalInteger 万分比)
  • Modify: FamilyMemberAttributes.java(mindBaseScore/wisdomBaseScore: 百分制整数)
  • Modify: FamilyMemberAttributeService.javacalcInnateScore 整型化)

  • [ ] Step 1: 配置实体整型化

    // 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 乘除:

// 旧:
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: 编译验证

    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 出现位置

执行以下命令确认最终清理状态:

# 后端全部 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 抽样对比