前端适配:Money工具函数和Wallet API类型更新
核心改造: 1. Money工具函数:新增formatCent、formatCentWithSymbol、yuanToCent等函数 2. Wallet API类型:WalletAccount和WalletLedger改为*_cent字段 3. API请求函数:rechargeWallet和startWalletRechargePayment自动转换元为分 4. 文档:生成完整的前端适配指南 技术细节: - formatCent: 分转角并格式化(四舍五入到0.1元) - yuanToCent: 元转分(用于表单提交) - API函数自动处理转换,组件层面仍使用元 - 生成详细的适配指南供后续组件修改参考 下一步: - 按照前端适配指南修改所有Vue组件 - 测试金额显示和计算精度
This commit is contained in:
@@ -0,0 +1,453 @@
|
|||||||
|
# 前端金额字段适配指南
|
||||||
|
|
||||||
|
**项目**: HFB_SYS
|
||||||
|
**目标**: 适配后端金额分字段重构
|
||||||
|
**状态**: 后端已完成 ✅,前端待适配 📝
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、已完成的工作 ✅
|
||||||
|
|
||||||
|
### 1. Money 工具函数更新
|
||||||
|
**文件**: `frontend/src/shared/utils/money.ts`
|
||||||
|
|
||||||
|
新增函数:
|
||||||
|
```typescript
|
||||||
|
// 分转角(四舍五入到0.1元)
|
||||||
|
export function centToJiao(cent: number | undefined | null): number
|
||||||
|
|
||||||
|
// 格式化分为角字符串(保留1位小数)
|
||||||
|
export function formatCent(cent: number | undefined | null): string
|
||||||
|
|
||||||
|
// 格式化分为角字符串并添加货币符号
|
||||||
|
export function formatCentWithSymbol(cent: number | undefined | null): string
|
||||||
|
|
||||||
|
// 元转分(四舍五入)
|
||||||
|
export function yuanToCent(yuan: number | undefined | null): number
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Wallet API 类型定义更新
|
||||||
|
**文件**: `frontend/src/features/wallet/api/wallet.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 已修改
|
||||||
|
export interface WalletAccount {
|
||||||
|
user_id: number
|
||||||
|
available_balance_cent: number // 改为 _cent
|
||||||
|
frozen_balance_cent: number // 改为 _cent
|
||||||
|
status: WalletStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WalletLedger {
|
||||||
|
id: number
|
||||||
|
ledger_no: string
|
||||||
|
user_id: number
|
||||||
|
order_id?: number
|
||||||
|
direction: LedgerDirection
|
||||||
|
amount_cent: number // 改为 _cent
|
||||||
|
balance_after_cent: number // 改为 _cent
|
||||||
|
balance_type: BalanceType
|
||||||
|
biz_type: string
|
||||||
|
biz_no: string
|
||||||
|
remark: string
|
||||||
|
created_at: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已修改:发送时将元转为分
|
||||||
|
export async function rechargeWallet(amountYuan: number) {
|
||||||
|
const amount_cent = Math.round(amountYuan * 100)
|
||||||
|
const { data } = await apiClient.post<ApiResponse<WalletAccount>>('/wallet/recharge', { amount_cent })
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、待适配的文件清单
|
||||||
|
|
||||||
|
### 模块 1: Wallet(钱包)
|
||||||
|
|
||||||
|
#### 1.1 WalletView.vue ⚠️ 需要修改
|
||||||
|
**文件**: `frontend/src/features/wallet/views/WalletView.vue`
|
||||||
|
|
||||||
|
**需要修改的地方**:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 第 54 行:余额展示 -->
|
||||||
|
<!-- 旧代码 -->
|
||||||
|
value: formatMoney(account.value.available_balance),
|
||||||
|
|
||||||
|
<!-- 新代码 -->
|
||||||
|
value: formatCent(account.value.available_balance_cent),
|
||||||
|
|
||||||
|
<!-- 第 61 行:冻结余额展示 -->
|
||||||
|
<!-- 旧代码 -->
|
||||||
|
value: formatMoney(account.value.frozen_balance),
|
||||||
|
|
||||||
|
<!-- 新代码 -->
|
||||||
|
value: formatCent(account.value.frozen_balance_cent),
|
||||||
|
|
||||||
|
<!-- 第 282 行:可用余额展示 -->
|
||||||
|
<!-- 旧代码 -->
|
||||||
|
<strong>{{ account ? formatMoney(account.available_balance) : '¥0.00' }}</strong>
|
||||||
|
|
||||||
|
<!-- 新代码 -->
|
||||||
|
<strong>{{ account ? formatCentWithSymbol(account.available_balance_cent) : '¥0.0' }}</strong>
|
||||||
|
|
||||||
|
<!-- 第 380 行:流水金额展示 -->
|
||||||
|
<!-- 旧代码 -->
|
||||||
|
{{ amountPrefix(row.direction) }}{{ formatMoney(row.amount) }}
|
||||||
|
|
||||||
|
<!-- 新代码 -->
|
||||||
|
{{ amountPrefix(row.direction) }}{{ formatCent(row.amount_cent) }}
|
||||||
|
|
||||||
|
<!-- 第 383 行:余额展示 -->
|
||||||
|
<!-- 旧代码 -->
|
||||||
|
{{ formatMoney(row.balance_after) }}
|
||||||
|
|
||||||
|
<!-- 新代码 -->
|
||||||
|
{{ formatCent(row.balance_after_cent) }}
|
||||||
|
|
||||||
|
<!-- 第 408 行:支付金额展示 -->
|
||||||
|
<!-- 旧代码 -->
|
||||||
|
formatMoney(activeRechargePayment.amount_cent / 100)
|
||||||
|
|
||||||
|
<!-- 新代码 -->
|
||||||
|
formatCent(activeRechargePayment.amount_cent)
|
||||||
|
```
|
||||||
|
|
||||||
|
**需要添加的 import**:
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { formatCent, formatCentWithSymbol } from '@/shared/utils/money'
|
||||||
|
// ... 其他 imports
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
**删除的本地函数**:
|
||||||
|
```typescript
|
||||||
|
// 删除这个函数(第 224-226 行),使用全局的 formatCent
|
||||||
|
function formatMoney(value: number) {
|
||||||
|
return `¥${(Math.round(Number(value || 0) * 10) / 10).toFixed(1)}`
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 模块 2: Withdrawal(提现)
|
||||||
|
|
||||||
|
#### 2.1 withdrawal.ts API ⚠️ 需要修改
|
||||||
|
**文件**: `frontend/src/features/wallet/api/withdrawal.ts`
|
||||||
|
|
||||||
|
**需要修改的类型定义**:
|
||||||
|
```typescript
|
||||||
|
export interface WithdrawalRequest {
|
||||||
|
id: number
|
||||||
|
withdraw_no: string
|
||||||
|
user_id: number
|
||||||
|
amount_cent: number // 改为 _cent
|
||||||
|
fee_cent: number // 改为 _cent
|
||||||
|
actual_amount_cent: number // 改为 _cent
|
||||||
|
payment_account_id: number
|
||||||
|
// ... 其他字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建提现请求函数需要转换
|
||||||
|
export async function createWithdrawal(amountYuan: number, payment_account_id: number) {
|
||||||
|
const amount_cent = Math.round(amountYuan * 100)
|
||||||
|
const { data } = await apiClient.post<ApiResponse<WithdrawalRequest>>(
|
||||||
|
'/wallet/withdrawal',
|
||||||
|
{ amount_cent, payment_account_id }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.2 WithdrawalView.vue ⚠️ 需要修改
|
||||||
|
**文件**: `frontend/src/features/wallet/views/WithdrawalView.vue`
|
||||||
|
|
||||||
|
**需要修改的地方**:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 余额展示 -->
|
||||||
|
<!-- 旧代码 -->
|
||||||
|
<div class="balance-value">¥{{ formatMoney(account?.available_balance) }}</div>
|
||||||
|
<div class="balance-value frozen">¥{{ formatMoney(account?.frozen_balance) }}</div>
|
||||||
|
|
||||||
|
<!-- 新代码 -->
|
||||||
|
<div class="balance-value">{{ formatCentWithSymbol(account?.available_balance_cent) }}</div>
|
||||||
|
<div class="balance-value frozen">{{ formatCentWithSymbol(account?.frozen_balance_cent) }}</div>
|
||||||
|
|
||||||
|
<!-- 提现金额比较 -->
|
||||||
|
<!-- 旧代码(第 77 行)-->
|
||||||
|
account.value && withdrawForm.value.amount <= account.value.available_balance
|
||||||
|
|
||||||
|
<!-- 新代码 -->
|
||||||
|
account.value && withdrawForm.value.amount <= (account.value.available_balance_cent / 100)
|
||||||
|
|
||||||
|
<!-- 提现记录展示 -->
|
||||||
|
<!-- 需要找到所有 withdrawal.amount 的地方,改为 -->
|
||||||
|
{{ formatCent(withdrawal.amount_cent) }}
|
||||||
|
{{ formatCent(withdrawal.fee_cent) }}
|
||||||
|
{{ formatCent(withdrawal.actual_amount_cent) }}
|
||||||
|
```
|
||||||
|
|
||||||
|
**常量更新**:
|
||||||
|
```typescript
|
||||||
|
// 这些是用户输入的元,不需要改
|
||||||
|
const MIN_AMOUNT = 10
|
||||||
|
const MAX_AMOUNT = 5000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 模块 3: Order(订单)
|
||||||
|
|
||||||
|
#### 3.1 orders.ts API ⚠️ 需要修改
|
||||||
|
**文件**: `frontend/src/features/orders/api/orders.ts`
|
||||||
|
|
||||||
|
**需要修改的类型定义**:
|
||||||
|
```typescript
|
||||||
|
export interface Order {
|
||||||
|
id: number
|
||||||
|
order_no: string
|
||||||
|
// ... 其他字段
|
||||||
|
display_amount_cent: number
|
||||||
|
rent_amount_cent?: number
|
||||||
|
owner_rent_amount_cent?: number
|
||||||
|
deposit_amount_cent: number
|
||||||
|
deposit_original_amount_cent: number
|
||||||
|
deposit_waived_amount_cent: number
|
||||||
|
platform_fee_cent?: number
|
||||||
|
// ... 其他字段
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Checkout {
|
||||||
|
id: number
|
||||||
|
order_id: number
|
||||||
|
// ... 其他字段
|
||||||
|
display_amount_cent: number
|
||||||
|
rent_amount_cent?: number
|
||||||
|
owner_rent_amount_cent?: number
|
||||||
|
platform_fee_cent?: number
|
||||||
|
deposit_amount_cent: number
|
||||||
|
consumable_amount_cent: number
|
||||||
|
coin_consumed_m: number
|
||||||
|
other_amount_cent: number
|
||||||
|
deposit_deduct_amount_cent: number
|
||||||
|
renter_refund_amount_cent?: number
|
||||||
|
owner_income_amount_cent?: number
|
||||||
|
// ... 其他字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.2 OrderDetailView.vue ⚠️ 需要修改
|
||||||
|
**文件**: `frontend/src/features/orders/views/OrderDetailView.vue`
|
||||||
|
|
||||||
|
查找所有使用金额的地方,改为使用 `formatCent`:
|
||||||
|
```vue
|
||||||
|
<!-- 示例 -->
|
||||||
|
{{ formatCent(order.display_amount_cent) }}
|
||||||
|
{{ formatCent(order.rent_amount_cent) }}
|
||||||
|
{{ formatCent(order.deposit_amount_cent) }}
|
||||||
|
{{ formatCent(checkout.renter_refund_amount_cent) }}
|
||||||
|
{{ formatCent(checkout.owner_income_amount_cent) }}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3.3 MobileOrdersView.vue ⚠️ 需要修改
|
||||||
|
类似地修改所有金额展示。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 模块 4: Listing(商品)
|
||||||
|
|
||||||
|
#### 4.1 listings.ts API ⚠️ 需要修改
|
||||||
|
**文件**: `frontend/src/features/listings/api/listings.ts`
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
export interface Listing {
|
||||||
|
id: number
|
||||||
|
listing_no: string
|
||||||
|
// ... 其他字段
|
||||||
|
price_cent: number // 改为 _cent
|
||||||
|
deposit_amount_cent: number // 改为 _cent
|
||||||
|
// ... 其他字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建/更新 Listing 时需要转换
|
||||||
|
export async function createListing(data: {
|
||||||
|
title: string
|
||||||
|
price: number
|
||||||
|
deposit_amount: number
|
||||||
|
// ... 其他字段
|
||||||
|
}) {
|
||||||
|
const payload = {
|
||||||
|
...data,
|
||||||
|
price_cent: Math.round(data.price * 100),
|
||||||
|
deposit_amount_cent: Math.round(data.deposit_amount * 100),
|
||||||
|
}
|
||||||
|
// 删除旧字段
|
||||||
|
delete payload.price
|
||||||
|
delete payload.deposit_amount
|
||||||
|
|
||||||
|
const { data: response } = await apiClient.post<ApiResponse<Listing>>(
|
||||||
|
'/listings',
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4.2 ListingCard.vue ⚠️ 需要修改
|
||||||
|
查找所有显示价格和押金的地方:
|
||||||
|
```vue
|
||||||
|
{{ formatCent(listing.price_cent) }}
|
||||||
|
{{ formatCent(listing.deposit_amount_cent) }}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、批量替换建议
|
||||||
|
|
||||||
|
### 全局搜索替换模式
|
||||||
|
|
||||||
|
1. **API 响应字段名**:
|
||||||
|
```
|
||||||
|
available_balance → available_balance_cent
|
||||||
|
frozen_balance → frozen_balance_cent
|
||||||
|
amount → amount_cent
|
||||||
|
balance_after → balance_after_cent
|
||||||
|
price → price_cent
|
||||||
|
deposit_amount → deposit_amount_cent
|
||||||
|
rent_amount → rent_amount_cent
|
||||||
|
owner_rent_amount → owner_rent_amount_cent
|
||||||
|
platform_fee → platform_fee_cent
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **格式化函数调用**:
|
||||||
|
```
|
||||||
|
formatMoney(xxx.amount) → formatCent(xxx.amount_cent)
|
||||||
|
formatMoneyWithSymbol(xxx.amount) → formatCentWithSymbol(xxx.amount_cent)
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **金额比较**:
|
||||||
|
```typescript
|
||||||
|
// 旧代码
|
||||||
|
if (inputAmount > account.available_balance) { ... }
|
||||||
|
|
||||||
|
// 新代码
|
||||||
|
if (inputAmount > (account.available_balance_cent / 100)) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、测试检查清单
|
||||||
|
|
||||||
|
### 功能测试
|
||||||
|
|
||||||
|
- [ ] 钱包余额正确显示(角精度)
|
||||||
|
- [ ] 充值功能正常(输入元,后端收到分)
|
||||||
|
- [ ] 提现功能正常(输入元,后端收到分)
|
||||||
|
- [ ] 订单金额正确显示
|
||||||
|
- [ ] 订单结算金额计算正确
|
||||||
|
- [ ] 商品价格正确显示
|
||||||
|
- [ ] 商品发布价格输入正常
|
||||||
|
|
||||||
|
### 显示精度测试
|
||||||
|
|
||||||
|
测试用例:
|
||||||
|
```typescript
|
||||||
|
// 123.45 元 = 12345 分 → 显示 123.5 元
|
||||||
|
formatCent(12345) // 应该输出 "123.5"
|
||||||
|
|
||||||
|
// 123.44 元 = 12344 分 → 显示 123.4 元
|
||||||
|
formatCent(12344) // 应该输出 "123.4"
|
||||||
|
|
||||||
|
// 0.01 元 = 1 分 → 显示 0.0 元
|
||||||
|
formatCent(1) // 应该输出 "0.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 边界值测试
|
||||||
|
|
||||||
|
- [ ] 最小充值金额:0.01 元 = 1 分
|
||||||
|
- [ ] 最小提现金额:10 元 = 1000 分
|
||||||
|
- [ ] 最大提现金额:5000 元 = 500000 分
|
||||||
|
- [ ] 零金额显示:formatCent(0) → "0.0"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、常见问题
|
||||||
|
|
||||||
|
### Q1: 为什么显示是角精度而不是分精度?
|
||||||
|
**A**: 业务需求只展示到角(0.1元),分精度对用户来说太细致且无实际意义。
|
||||||
|
|
||||||
|
### Q2: 用户输入 123.45 元,后端收到多少?
|
||||||
|
**A**: `yuanToCent(123.45)` = 12345 分
|
||||||
|
|
||||||
|
### Q3: 后端返回 12345 分,前端显示什么?
|
||||||
|
**A**: `formatCent(12345)` = "123.5" 元(四舍五入到角)
|
||||||
|
|
||||||
|
### Q4: 如何处理浮点输入的精度?
|
||||||
|
**A**: 使用 `Math.round(yuan * 100)` 四舍五入到分,避免浮点误差。
|
||||||
|
|
||||||
|
### Q5: 旧的 formatMoney 函数还能用吗?
|
||||||
|
**A**: 可以,但仅用于已经是元的数值。对于后端返回的分字段,必须使用 formatCent。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、快速参考
|
||||||
|
|
||||||
|
### 常用代码片段
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { formatCent, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money'
|
||||||
|
|
||||||
|
// 显示金额
|
||||||
|
const displayAmount = formatCent(wallet.available_balance_cent)
|
||||||
|
const displayWithSymbol = formatCentWithSymbol(order.rent_amount_cent)
|
||||||
|
|
||||||
|
// 提交表单
|
||||||
|
async function submit() {
|
||||||
|
const amountCent = yuanToCent(form.amount)
|
||||||
|
await apiClient.post('/api/endpoint', { amount_cent: amountCent })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 金额比较
|
||||||
|
if (inputAmount > (account.available_balance_cent / 100)) {
|
||||||
|
console.log('余额不足')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- 显示金额 -->
|
||||||
|
<div>余额:{{ formatCentWithSymbol(account.available_balance_cent) }}</div>
|
||||||
|
|
||||||
|
<!-- 输入金额(元) -->
|
||||||
|
<el-input-number v-model="form.amount" :min="0.01" :precision="2" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、推荐的适配顺序
|
||||||
|
|
||||||
|
1. ✅ 工具函数(已完成)
|
||||||
|
2. ✅ Wallet API 类型(已完成)
|
||||||
|
3. ⚠️ Wallet 组件(进行中)
|
||||||
|
4. ⚠️ Withdrawal API 和组件
|
||||||
|
5. ⚠️ Order API 和组件
|
||||||
|
6. ⚠️ Listing API 和组件
|
||||||
|
7. ⚠️ AdminFinance 组件(如果有)
|
||||||
|
8. 🧪 完整测试
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**预计工作量**: 2-3 小时
|
||||||
|
**难度**: 中等(主要是查找替换和测试)
|
||||||
|
**风险**: 低(类型系统会帮助发现遗漏的地方)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档生成时间**: 2026-06-09
|
||||||
|
**作者**: Claude Code AI Assistant
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
# 金额统一重构完成报告
|
||||||
|
|
||||||
|
**项目**: HFB_SYS - 游戏账号租赁平台
|
||||||
|
**重构目标**: 统一金额存储和计算,从 float64(元)改为 int64(分)
|
||||||
|
**完成时间**: 2026-06-09
|
||||||
|
**状态**: ✅ 后端完成,前端待适配
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、重构目标
|
||||||
|
|
||||||
|
### 问题背景
|
||||||
|
- **浮点精度问题**: 使用 float64 存储金额导致精度丢失
|
||||||
|
- **角精度展示**: 业务需求仅展示到角(0.1元),但存储需要精确到分
|
||||||
|
- **计算误差**: 浮点运算在累加、结算时可能产生误差
|
||||||
|
|
||||||
|
### 解决方案
|
||||||
|
- **全链路整数存储**: 数据库 BIGINT(分) → Go int64 → API int64 → 前端 number
|
||||||
|
- **整数运算**: 所有金额计算使用整数,消除浮点误差
|
||||||
|
- **角精度展示**: 存储精确到分,展示时转换为角(除以10,保留1位小数)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、已完成的工作
|
||||||
|
|
||||||
|
### 1. 数据库迁移 ✅
|
||||||
|
|
||||||
|
**文件**: `backend/migrations/000006_add_money_cent_fields.sql`
|
||||||
|
|
||||||
|
新增字段:
|
||||||
|
```sql
|
||||||
|
-- rental_orders 表
|
||||||
|
ALTER TABLE rental_orders
|
||||||
|
ADD COLUMN rent_amount_cent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN deposit_amount_cent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN deposit_original_amount_cent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN deposit_waived_amount_cent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN platform_fee_cent BIGINT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- rental_listings 表
|
||||||
|
ALTER TABLE rental_listings
|
||||||
|
ADD COLUMN price_cent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN deposit_amount_cent BIGINT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- wallet_accounts 表
|
||||||
|
ALTER TABLE wallet_accounts
|
||||||
|
ADD COLUMN available_balance_cent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN frozen_balance_cent BIGINT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- wallet_ledger 表
|
||||||
|
ALTER TABLE wallet_ledger
|
||||||
|
ADD COLUMN amount_cent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN balance_after_cent BIGINT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- withdrawal_requests 表
|
||||||
|
ALTER TABLE withdrawal_requests
|
||||||
|
ADD COLUMN amount_cent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN fee_cent BIGINT NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN actual_amount_cent BIGINT NOT NULL DEFAULT 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
历史数据迁移:
|
||||||
|
```sql
|
||||||
|
UPDATE rental_orders SET
|
||||||
|
rent_amount_cent = CAST(ROUND(rent_amount * 100) AS SIGNED),
|
||||||
|
owner_rent_amount_cent = CAST(ROUND(owner_rent_amount * 100) AS SIGNED),
|
||||||
|
...;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Model 层 ✅
|
||||||
|
|
||||||
|
**文件**: `backend/internal/model/*.go`
|
||||||
|
|
||||||
|
为所有金额相关表新增 *Cent 字段:
|
||||||
|
```go
|
||||||
|
type RentalOrder struct {
|
||||||
|
RentAmount float64 `gorm:"type:decimal(12,2)" json:"-"`
|
||||||
|
RentAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
|
OwnerRentAmount float64 `gorm:"type:decimal(12,2)" json:"-"`
|
||||||
|
OwnerRentAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
|
// ... 其他字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Money 工具包 ✅
|
||||||
|
|
||||||
|
**文件**: `backend/pkg/money/money.go`
|
||||||
|
|
||||||
|
核心函数:
|
||||||
|
```go
|
||||||
|
// ToCent 元转分(四舍五入)
|
||||||
|
func ToCent(yuan float64) int64
|
||||||
|
|
||||||
|
// FromCent 分转元
|
||||||
|
func FromCent(cent int64) float64
|
||||||
|
|
||||||
|
// FormatCent 格式化分为元字符串(2位小数)
|
||||||
|
func FormatCent(cent int64) string
|
||||||
|
|
||||||
|
// FormatJiao 格式化分为角字符串(1位小数)
|
||||||
|
func FormatJiao(cent int64) string
|
||||||
|
|
||||||
|
// ToJiao 分转角(四舍五入)
|
||||||
|
func ToJiao(cent int64) int64
|
||||||
|
|
||||||
|
// FromJiao 角转分
|
||||||
|
func FromJiao(jiao int64) int64
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 核心业务模块 ✅
|
||||||
|
|
||||||
|
#### Wallet 模块 ✅
|
||||||
|
- **DTO**: 所有金额字段改为 `*Cent int64`
|
||||||
|
- **Entry结构**: `AmountCent int64` 替代 `Amount float64`
|
||||||
|
- **整数运算**: `applyEntry` 使用纯整数加减
|
||||||
|
- **删除**: `roundWalletMoney` 函数(不再需要)
|
||||||
|
|
||||||
|
#### Withdrawal 模块 ✅
|
||||||
|
- **DTO**: `WithdrawalDTO`, `CreateWithdrawalRequest` 改为 `*Cent`
|
||||||
|
- **手续费计算**: 改为分单位
|
||||||
|
- **最小/最大金额**: MinWithdrawalAmountCent = 1000 (10元)
|
||||||
|
|
||||||
|
#### Order 模块 ✅(最复杂)
|
||||||
|
- **DTO**: `OrderDTO`, `CheckoutDTO`, `RefundStatusDTO` 改为 `*Cent`
|
||||||
|
- **内部结构体**: `orderPricing`, `checkoutSettlement` 改为分字段
|
||||||
|
- **结算计算**: `calculateCheckoutSettlement` 内部用元计算保持兼容,返回分
|
||||||
|
- **钱包操作**: 所有 `wallet.AppendEntries` 调用改为 `AmountCent`
|
||||||
|
- **退款逻辑**: 3 处改为直接使用分字段相加
|
||||||
|
|
||||||
|
#### Dispute 模块 ✅
|
||||||
|
- **修复**: 2 处 `wallet.Entry` 调用改为 `AmountCent`
|
||||||
|
|
||||||
|
#### Listing 模块 ✅
|
||||||
|
- **DTO**: `PriceCent`, `DepositAmountCent`
|
||||||
|
- **筛选排序**: 改为使用分字段
|
||||||
|
- **价格视图**: `sanitizePriceForOwner` 使用 `PriceCent`
|
||||||
|
|
||||||
|
#### Payment 模块 ✅(已适配)
|
||||||
|
- **DTO**: `PaymentDTO`, `RefundDTO` 已使用 `AmountCent`
|
||||||
|
- **退款函数**: `StartRefund(orderID, refundAmountCent int64, ...)` 已适配
|
||||||
|
|
||||||
|
#### AdminFinance 模块 ✅(已适配)
|
||||||
|
- **DTO**: `FinanceSummaryDTO`, `FinanceDailyDTO` 已使用 `*Cent` 字段
|
||||||
|
- **查询**: 财务统计查询已适配
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、技术实现亮点
|
||||||
|
|
||||||
|
### 1. 数据双字段并存
|
||||||
|
新旧字段共存,便于渐进式迁移和回滚:
|
||||||
|
```go
|
||||||
|
type RentalOrder struct {
|
||||||
|
RentAmount float64 `gorm:"type:decimal(12,2)" json:"-"` // 旧字段
|
||||||
|
RentAmountCent int64 `gorm:"not null;default:0" json:"-"` // 新字段
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 整数运算消除精度问题
|
||||||
|
```go
|
||||||
|
// 旧代码(浮点运算)
|
||||||
|
account.AvailableBalance += amount
|
||||||
|
account.AvailableBalance = roundMoney(account.AvailableBalance)
|
||||||
|
|
||||||
|
// 新代码(整数运算)
|
||||||
|
account.AvailableBalanceCent += amountCent // 直接加减,无精度损失
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 复杂结算兼容性处理
|
||||||
|
Order 模块的 `calculateCheckoutSettlement` 函数:
|
||||||
|
```go
|
||||||
|
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmount float64,
|
||||||
|
coinConsumedM float64, depositDeductAmount float64) checkoutSettlement {
|
||||||
|
|
||||||
|
// 读取分字段并转为元(保持现有计算逻辑)
|
||||||
|
orderRentAmount := float64(order.RentAmountCent) / 100
|
||||||
|
orderOwnerRentAmount := float64(order.OwnerRentAmountCent) / 100
|
||||||
|
|
||||||
|
// 现有的复杂计算逻辑...
|
||||||
|
actualRentAmount := minMoney(roundMoney(usedBuyerCoinPrice+usedBuyerConsumablePrice), orderRentAmount)
|
||||||
|
|
||||||
|
// 最后转换为分返回
|
||||||
|
return checkoutSettlement{
|
||||||
|
ActualRentAmountCent: int64(math.Round(actualRentAmount * 100)),
|
||||||
|
OwnerRentIncomeCent: int64(math.Round(ownerRentIncome * 100)),
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 角精度展示
|
||||||
|
```go
|
||||||
|
// 后端返回分
|
||||||
|
dto.PriceCent = 12345 // 123.45 元
|
||||||
|
|
||||||
|
// 前端展示角(formatJiao)
|
||||||
|
formatJiao(12345) → "123.5" // 显示 123.5 元
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、Git 提交记录
|
||||||
|
|
||||||
|
```bash
|
||||||
|
eaa10d8 - 数据库迁移:新增金额分字段并迁移历史数据
|
||||||
|
b80719d - Model层:新增金额分字段定义
|
||||||
|
e2780ff - Money工具包:实现分↔角转换和格式化函数
|
||||||
|
01909ee - Wallet与Withdrawal模块:完成分字段重构
|
||||||
|
d803089 - Order模块:完成分字段重构
|
||||||
|
4f3be22 - Dispute模块:修复wallet.Entry调用
|
||||||
|
2185cd1 - Listing模块:完成分字段重构
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、编译验证
|
||||||
|
|
||||||
|
```bash
|
||||||
|
✅ go build -o /dev/null ./cmd/api
|
||||||
|
编译通过,无错误
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、下一步工作:前端适配
|
||||||
|
|
||||||
|
### 需要修改的文件
|
||||||
|
|
||||||
|
#### 1. TypeScript 类型定义
|
||||||
|
更新所有 API 接口类型定义,将金额字段改为 `*_cent: number`
|
||||||
|
|
||||||
|
#### 2. 工具函数
|
||||||
|
```typescript
|
||||||
|
// frontend/src/utils/money.ts
|
||||||
|
export function formatCent(cent: number): string {
|
||||||
|
return (cent / 100).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatJiao(cent: number): string {
|
||||||
|
return (Math.round(cent / 10) / 10).toFixed(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toCent(yuan: number): number {
|
||||||
|
return Math.round(yuan * 100);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 涉及的模块
|
||||||
|
- **Wallet**: 余额展示、充值输入
|
||||||
|
- **Withdrawal**: 提现金额输入、手续费显示
|
||||||
|
- **Order**: 订单金额展示、结算详情
|
||||||
|
- **Listing**: 商品价格展示、发布价格输入
|
||||||
|
- **AdminFinance**: 财务报表展示
|
||||||
|
|
||||||
|
### 改造原则
|
||||||
|
1. **表单输入**: 用户输入元 → 乘以100转为分 → 发送后端
|
||||||
|
2. **数据展示**: 后端返回分 → 除以10四舍五入 → 显示角(1位小数)
|
||||||
|
3. **内部计算**: 尽量使用分进行计算,避免浮点运算
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、测试建议
|
||||||
|
|
||||||
|
### 1. 单元测试
|
||||||
|
- Order 模块结算计算
|
||||||
|
- Money 工具函数边界值
|
||||||
|
|
||||||
|
### 2. 集成测试
|
||||||
|
- 完整订单流程
|
||||||
|
- 钱包充值提现流程
|
||||||
|
|
||||||
|
### 3. 手工测试场景
|
||||||
|
- 订单结算精度(租金+押金+手续费)
|
||||||
|
- 钱包余额累加(大量小额交易)
|
||||||
|
- 提现手续费计算
|
||||||
|
- 边界值(最小充值、最大提现)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、回滚方案
|
||||||
|
|
||||||
|
如需回滚,执行以下步骤:
|
||||||
|
|
||||||
|
1. **代码回滚**
|
||||||
|
```bash
|
||||||
|
git revert 2185cd1 # Listing
|
||||||
|
git revert 4f3be22 # Dispute
|
||||||
|
git revert d803089 # Order
|
||||||
|
git revert 01909ee # Wallet & Withdrawal
|
||||||
|
git revert e2780ff # Money工具包
|
||||||
|
git revert b80719d # Model
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **数据库回滚**
|
||||||
|
```sql
|
||||||
|
-- 不需要删除新字段,旧字段仍然存在
|
||||||
|
-- 如果需要,可以执行:
|
||||||
|
ALTER TABLE rental_orders DROP COLUMN rent_amount_cent;
|
||||||
|
-- ... 其他表
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、总结
|
||||||
|
|
||||||
|
✅ **后端重构完成度**: 100%
|
||||||
|
✅ **编译状态**: 通过
|
||||||
|
✅ **代码质量**: 保持原有逻辑,仅替换金额字段
|
||||||
|
✅ **兼容性**: 新旧字段并存,便于迁移
|
||||||
|
|
||||||
|
🎯 **下一阶段**: 前端适配(约需 2-3 小时)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**报告生成时间**: 2026-06-09
|
||||||
|
**作者**: Claude Code AI Assistant
|
||||||
@@ -6,8 +6,8 @@ import type { PaymentOrder } from '@/features/orders/api/orders'
|
|||||||
|
|
||||||
export interface WalletAccount {
|
export interface WalletAccount {
|
||||||
user_id: number
|
user_id: number
|
||||||
available_balance: number
|
available_balance_cent: number
|
||||||
frozen_balance: number
|
frozen_balance_cent: number
|
||||||
status: WalletStatus
|
status: WalletStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -17,8 +17,8 @@ export interface WalletLedger {
|
|||||||
user_id: number
|
user_id: number
|
||||||
order_id?: number
|
order_id?: number
|
||||||
direction: LedgerDirection
|
direction: LedgerDirection
|
||||||
amount: number
|
amount_cent: number
|
||||||
balance_after: number
|
balance_after_cent: number
|
||||||
balance_type: BalanceType
|
balance_type: BalanceType
|
||||||
biz_type: string
|
biz_type: string
|
||||||
biz_no: string
|
biz_no: string
|
||||||
@@ -41,14 +41,16 @@ export async function fetchWalletLedger(page = 1, pageSize = 20) {
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rechargeWallet(amount: number) {
|
export async function rechargeWallet(amountYuan: number) {
|
||||||
const { data } = await apiClient.post<ApiResponse<WalletAccount>>('/wallet/recharge', { amount })
|
const amount_cent = Math.round(amountYuan * 100)
|
||||||
|
const { data } = await apiClient.post<ApiResponse<WalletAccount>>('/wallet/recharge', { amount_cent })
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startWalletRechargePayment(amount: number) {
|
export async function startWalletRechargePayment(amountYuan: number) {
|
||||||
|
const amount_cent = Math.round(amountYuan * 100)
|
||||||
const { data } = await apiClient.post<ApiResponse<PaymentOrder>>('/wallet/recharge/pay', {
|
const { data } = await apiClient.post<ApiResponse<PaymentOrder>>('/wallet/recharge/pay', {
|
||||||
amount,
|
amount_cent,
|
||||||
})
|
})
|
||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* 统一金额处理工具函数
|
* 统一金额处理工具函数
|
||||||
* 全项目金额精度统一为角(0.1元)
|
* 后端存储:int64(分)
|
||||||
|
* 前端展示:角精度(0.1元)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,3 +30,47 @@ export function formatMoney(value: number | undefined | null): string {
|
|||||||
export function formatMoneyWithSymbol(value: number | undefined | null): string {
|
export function formatMoneyWithSymbol(value: number | undefined | null): string {
|
||||||
return `¥${formatMoney(value)}`
|
return `¥${formatMoney(value)}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分转元(保留2位小数)
|
||||||
|
* @example centToYuan(12345) -> 123.45
|
||||||
|
*/
|
||||||
|
export function centToYuan(cent: number | undefined | null): number {
|
||||||
|
return Number(cent || 0) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分转角(四舍五入到0.1元)
|
||||||
|
* @example centToJiao(12345) -> 123.5
|
||||||
|
* @example centToJiao(12344) -> 123.4
|
||||||
|
*/
|
||||||
|
export function centToJiao(cent: number | undefined | null): number {
|
||||||
|
return Math.round(Number(cent || 0) / 10) / 10
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化分为角字符串(保留1位小数)
|
||||||
|
* @example formatCent(12345) -> "123.5"
|
||||||
|
* @example formatCent(12344) -> "123.4"
|
||||||
|
*/
|
||||||
|
export function formatCent(cent: number | undefined | null): string {
|
||||||
|
return centToJiao(cent).toFixed(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化分为角字符串并添加货币符号
|
||||||
|
* @example formatCentWithSymbol(12345) -> "¥123.5"
|
||||||
|
*/
|
||||||
|
export function formatCentWithSymbol(cent: number | undefined | null): string {
|
||||||
|
return `¥${formatCent(cent)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 元转分(四舍五入)
|
||||||
|
* 用户输入金额时使用
|
||||||
|
* @example yuanToCent(123.45) -> 12345
|
||||||
|
* @example yuanToCent(123.456) -> 12346
|
||||||
|
*/
|
||||||
|
export function yuanToCent(yuan: number | undefined | null): number {
|
||||||
|
return Math.round(Number(yuan || 0) * 100)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user