chore: finalize amount migration fixes
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
FROM golang:1.26-alpine AS build
|
FROM golang:1.26-alpine AS build
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
ARG GOOSE_VERSION=v3.27.1
|
||||||
ARG GOPROXY=https://goproxy.cn,direct
|
ARG GOPROXY=https://goproxy.cn,direct
|
||||||
ARG GOSUMDB=sum.golang.google.cn
|
ARG GOSUMDB=sum.golang.google.cn
|
||||||
ENV GOPROXY=${GOPROXY} \
|
ENV GOPROXY=${GOPROXY} \
|
||||||
@@ -11,6 +12,9 @@ ENV GOPROXY=${GOPROXY} \
|
|||||||
COPY go.mod go.sum* ./
|
COPY go.mod go.sum* ./
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
RUN --mount=type=cache,target=/go/pkg/mod go mod download
|
||||||
COPY . .
|
COPY . .
|
||||||
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
|
go install github.com/pressly/goose/v3/cmd/goose@${GOOSE_VERSION}
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
CGO_ENABLED=0 GOOS=linux go build -o /out/hfb-api ./cmd/api
|
CGO_ENABLED=0 GOOS=linux go build -o /out/hfb-api ./cmd/api
|
||||||
@@ -21,5 +25,7 @@ WORKDIR /app
|
|||||||
ENV TZ=Asia/Shanghai
|
ENV TZ=Asia/Shanghai
|
||||||
RUN apk add --no-cache tzdata
|
RUN apk add --no-cache tzdata
|
||||||
COPY --from=build /out/hfb-api /app/hfb-api
|
COPY --from=build /out/hfb-api /app/hfb-api
|
||||||
|
COPY --from=build /go/bin/goose /app/goose
|
||||||
|
COPY --from=build /src/migrations /app/migrations
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
CMD ["/app/hfb-api"]
|
CMD ["/app/hfb-api"]
|
||||||
|
|||||||
@@ -30,27 +30,27 @@ func TestCalculateCheckoutSettlementRefundsUnusedRent(t *testing.T) {
|
|||||||
settlement := calculateCheckoutSettlement(order, 7, 90, 0)
|
settlement := calculateCheckoutSettlement(order, 7, 90, 0)
|
||||||
|
|
||||||
// 角精度:243.7 = roundMoney(236.7 + 7)
|
// 角精度:243.7 = roundMoney(236.7 + 7)
|
||||||
if float64(settlement.ActualRentAmountCent)/100 != 243.7 {
|
if settlement.ActualRentAmountCent != 24370 {
|
||||||
t.Fatalf("ActualRentAmount = %.1f, want 243.7", float64(settlement.ActualRentAmountCent)/100)
|
t.Fatalf("ActualRentAmountCent = %d, want 24370", settlement.ActualRentAmountCent)
|
||||||
}
|
}
|
||||||
// 角精度:216.7 = roundMoney(209.7 + 7) 卖家币价+消耗品
|
// 角精度:216.7 = roundMoney(209.7 + 7) 卖家币价+消耗品
|
||||||
if float64(settlement.OwnerRentIncomeCent)/100 != 216.7 {
|
if settlement.OwnerRentIncomeCent != 21670 {
|
||||||
t.Fatalf("OwnerRentIncome = %.1f, want 216.7", float64(settlement.OwnerRentIncomeCent)/100)
|
t.Fatalf("OwnerRentIncomeCent = %d, want 21670", settlement.OwnerRentIncomeCent)
|
||||||
}
|
}
|
||||||
// 角精度:27.0 = roundMoney(27) 平台费
|
// 角精度:27.0 = roundMoney(27) 平台费
|
||||||
if float64(settlement.PlatformFeeCent)/100 != 27.0 {
|
if settlement.PlatformFeeCent != 2700 {
|
||||||
t.Fatalf("PlatformFee = %.1f, want 27.0", float64(settlement.PlatformFeeCent)/100)
|
t.Fatalf("PlatformFeeCent = %d, want 2700", settlement.PlatformFeeCent)
|
||||||
}
|
}
|
||||||
// 角精度:139.3 = roundMoney(383 - 243.7)
|
// 角精度:139.3 = roundMoney(383 - 243.7)
|
||||||
if float64(settlement.RentRefundCent)/100 != 139.3 {
|
if settlement.RentRefundCent != 13930 {
|
||||||
t.Fatalf("RentRefund = %.1f, want 139.3", float64(settlement.RentRefundCent)/100)
|
t.Fatalf("RentRefundCent = %d, want 13930", settlement.RentRefundCent)
|
||||||
}
|
}
|
||||||
if float64(settlement.DepositRefundCent)/100 != 150 {
|
if settlement.DepositRefundCent != 15000 {
|
||||||
t.Fatalf("DepositRefund = %.1f, want 150.0", float64(settlement.DepositRefundCent)/100)
|
t.Fatalf("DepositRefundCent = %d, want 15000", settlement.DepositRefundCent)
|
||||||
}
|
}
|
||||||
// 角精度:289.3 = roundMoney(139.3 + 150)
|
// 角精度:289.3 = roundMoney(139.3 + 150)
|
||||||
if float64(settlement.RenterRefundCent)/100 != 289.3 {
|
if settlement.RenterRefundCent != 28930 {
|
||||||
t.Fatalf("RenterRefund = %.1f, want 289.3", float64(settlement.RenterRefundCent)/100)
|
t.Fatalf("RenterRefundCent = %d, want 28930", settlement.RenterRefundCent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,16 +74,16 @@ func TestCalculateCheckoutSettlementUsesBuyerAndSellerRatiosSeparately(t *testin
|
|||||||
settlement := calculateCheckoutSettlement(order, 0, 50, 0)
|
settlement := calculateCheckoutSettlement(order, 0, 50, 0)
|
||||||
|
|
||||||
// 角精度:131.5 = roundMoney(131.5)
|
// 角精度:131.5 = roundMoney(131.5)
|
||||||
if float64(settlement.ActualRentAmountCent)/100 != 131.5 {
|
if settlement.ActualRentAmountCent != 13150 {
|
||||||
t.Fatalf("租客侧实际租金 = %.1f, want 131.5", float64(settlement.ActualRentAmountCent)/100)
|
t.Fatalf("租客侧实际租金 = %d, want 13150", settlement.ActualRentAmountCent)
|
||||||
}
|
}
|
||||||
// 角精度:116.5 = roundMoney(116.5)
|
// 角精度:116.5 = roundMoney(116.5)
|
||||||
if float64(settlement.OwnerRentIncomeCent)/100 != 116.5 {
|
if settlement.OwnerRentIncomeCent != 11650 {
|
||||||
t.Fatalf("卖家侧租金收入 = %.1f, want 116.5", float64(settlement.OwnerRentIncomeCent)/100)
|
t.Fatalf("卖家侧租金收入 = %d, want 11650", settlement.OwnerRentIncomeCent)
|
||||||
}
|
}
|
||||||
// 角精度:15.0 = roundMoney(15)
|
// 角精度:15.0 = roundMoney(15)
|
||||||
if float64(settlement.PlatformFeeCent)/100 != 15.0 {
|
if settlement.PlatformFeeCent != 1500 {
|
||||||
t.Fatalf("平台差价 = %.1f, want 15.0", float64(settlement.PlatformFeeCent)/100)
|
t.Fatalf("平台差价 = %d, want 1500", settlement.PlatformFeeCent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,20 +106,20 @@ func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) {
|
|||||||
|
|
||||||
settlement := calculateCheckoutSettlement(order, 120, 100, 30)
|
settlement := calculateCheckoutSettlement(order, 120, 100, 30)
|
||||||
|
|
||||||
if float64(settlement.ActualRentAmountCent)/100 != 383 {
|
if settlement.ActualRentAmountCent != 38300 {
|
||||||
t.Fatalf("ActualRentAmount = %.2f, want 383.00", float64(settlement.ActualRentAmountCent)/100)
|
t.Fatalf("ActualRentAmountCent = %d, want 38300", settlement.ActualRentAmountCent)
|
||||||
}
|
}
|
||||||
if float64(settlement.OwnerRentIncomeCent)/100 != 353 {
|
if settlement.OwnerRentIncomeCent != 35300 {
|
||||||
t.Fatalf("OwnerRentIncome = %.2f, want 353.00", float64(settlement.OwnerRentIncomeCent)/100)
|
t.Fatalf("OwnerRentIncomeCent = %d, want 35300", settlement.OwnerRentIncomeCent)
|
||||||
}
|
}
|
||||||
if float64(settlement.DepositCompensationCent)/100 != 30 {
|
if settlement.DepositCompensationCent != 3000 {
|
||||||
t.Fatalf("DepositCompensation = %.2f, want 30.00", float64(settlement.DepositCompensationCent)/100)
|
t.Fatalf("DepositCompensationCent = %d, want 3000", settlement.DepositCompensationCent)
|
||||||
}
|
}
|
||||||
if float64(settlement.OwnerIncomeCent)/100 != 383 {
|
if settlement.OwnerIncomeCent != 38300 {
|
||||||
t.Fatalf("OwnerIncome = %.2f, want 383.00", float64(settlement.OwnerIncomeCent)/100)
|
t.Fatalf("OwnerIncomeCent = %d, want 38300", settlement.OwnerIncomeCent)
|
||||||
}
|
}
|
||||||
if float64(settlement.RenterRefundCent)/100 != 120 {
|
if settlement.RenterRefundCent != 12000 {
|
||||||
t.Fatalf("RenterRefund = %.2f, want 120.00", float64(settlement.RenterRefundCent)/100)
|
t.Fatalf("RenterRefundCent = %d, want 12000", settlement.RenterRefundCent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
-- 创建时间: 2026-06-05
|
-- 创建时间: 2026-06-05
|
||||||
-- ============================================
|
-- ============================================
|
||||||
|
|
||||||
|
-- +goose Up
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
SET FOREIGN_KEY_CHECKS = 0;
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
@@ -21,6 +23,7 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
risk_status VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '风控状态: normal正常, warning警告, frozen冻结',
|
risk_status VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '风控状态: normal正常, warning警告, frozen冻结',
|
||||||
credit_score INT NOT NULL DEFAULT 100 COMMENT '信用分',
|
credit_score INT NOT NULL DEFAULT 100 COMMENT '信用分',
|
||||||
deposit_free_quota DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '免押总额度',
|
deposit_free_quota DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '免押总额度',
|
||||||
|
deposit_free_quota_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押总额度(分)',
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '账号状态: active活跃, inactive停用, banned封禁',
|
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '账号状态: active活跃, inactive停用, banned封禁',
|
||||||
last_login_at DATETIME NULL COMMENT '最后登录时间',
|
last_login_at DATETIME NULL COMMENT '最后登录时间',
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
@@ -80,7 +83,9 @@ CREATE TABLE IF NOT EXISTS rental_listings (
|
|||||||
account_id BIGINT UNSIGNED NOT NULL COMMENT '关联的游戏账号ID',
|
account_id BIGINT UNSIGNED NOT NULL COMMENT '关联的游戏账号ID',
|
||||||
owner_id BIGINT UNSIGNED NOT NULL COMMENT '号主ID',
|
owner_id BIGINT UNSIGNED NOT NULL COMMENT '号主ID',
|
||||||
price DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租金(元/小时)',
|
price DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租金(元/小时)',
|
||||||
|
price_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金(分/小时)',
|
||||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '押金金额',
|
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '押金金额',
|
||||||
|
deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金金额(分)',
|
||||||
in_transaction TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否正在交易中: 0否, 1是',
|
in_transaction TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否正在交易中: 0否, 1是',
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT '商品状态: draft草稿, active上架, offline下架, deleted删除',
|
status VARCHAR(32) NOT NULL DEFAULT 'draft' COMMENT '商品状态: draft草稿, active上架, offline下架, deleted删除',
|
||||||
review_status VARCHAR(32) NOT NULL DEFAULT 'none' COMMENT '审核状态: none无需审核, pending待审核, approved通过, rejected拒绝',
|
review_status VARCHAR(32) NOT NULL DEFAULT 'none' COMMENT '审核状态: none无需审核, pending待审核, approved通过, rejected拒绝',
|
||||||
@@ -118,11 +123,17 @@ CREATE TABLE IF NOT EXISTS rental_orders (
|
|||||||
rented_at DATETIME NULL COMMENT '租用开始时间',
|
rented_at DATETIME NULL COMMENT '租用开始时间',
|
||||||
estimated_duration_hours INT NOT NULL DEFAULT 24 COMMENT '预计租用时长(小时)',
|
estimated_duration_hours INT NOT NULL DEFAULT 24 COMMENT '预计租用时长(小时)',
|
||||||
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租金总额',
|
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租金总额',
|
||||||
|
rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金总额(分)',
|
||||||
owner_rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '号主实得租金',
|
owner_rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '号主实得租金',
|
||||||
|
owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主实得租金(分)',
|
||||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '实际收取押金金额',
|
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '实际收取押金金额',
|
||||||
|
deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '实际收取押金(分)',
|
||||||
deposit_original_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '商品原始押金金额',
|
deposit_original_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '商品原始押金金额',
|
||||||
|
deposit_original_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '商品原始押金(分)',
|
||||||
deposit_waived_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '本单免押抵扣金额',
|
deposit_waived_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '本单免押抵扣金额',
|
||||||
|
deposit_waived_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押抵扣金额(分)',
|
||||||
platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '平台手续费',
|
platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '平台手续费',
|
||||||
|
platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)',
|
||||||
account_snapshot JSON NULL COMMENT '账号快照(下单时的账号状态)',
|
account_snapshot JSON NULL COMMENT '账号快照(下单时的账号状态)',
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'pending_payment' COMMENT '订单状态: pending_payment待支付, active进行中, completed已完成, cancelled已取消, closed已关闭',
|
status VARCHAR(32) NOT NULL DEFAULT 'pending_payment' COMMENT '订单状态: pending_payment待支付, active进行中, completed已完成, cancelled已取消, closed已关闭',
|
||||||
handoff_status VARCHAR(32) NOT NULL DEFAULT 'none' COMMENT '交接状态: none未开始, owner_delivered号主已交, renter_confirmed租客已确认, renter_returned租客已还, owner_received号主已收',
|
handoff_status VARCHAR(32) NOT NULL DEFAULT 'none' COMMENT '交接状态: none未开始, owner_delivered号主已交, renter_confirmed租客已确认, renter_returned租客已还, owner_received号主已收',
|
||||||
@@ -151,15 +162,24 @@ CREATE TABLE IF NOT EXISTS order_checkouts (
|
|||||||
initiated_by BIGINT UNSIGNED NOT NULL COMMENT '发起者ID',
|
initiated_by BIGINT UNSIGNED NOT NULL COMMENT '发起者ID',
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'submitted' COMMENT '结算状态: submitted已提交, renter_confirmed租客确认, renter_rejected租客拒绝, completed完成',
|
status VARCHAR(32) NOT NULL DEFAULT 'submitted' COMMENT '结算状态: submitted已提交, renter_confirmed租客确认, renter_rejected租客拒绝, completed完成',
|
||||||
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租金',
|
rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租金',
|
||||||
|
rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金(分)',
|
||||||
owner_rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '号主实得租金',
|
owner_rent_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '号主实得租金',
|
||||||
|
owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主实得租金(分)',
|
||||||
platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '平台手续费',
|
platform_fee DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '平台手续费',
|
||||||
|
platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)',
|
||||||
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '押金',
|
deposit_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '押金',
|
||||||
|
deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金(分)',
|
||||||
consumable_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '消耗品扣费',
|
consumable_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '消耗品扣费',
|
||||||
|
consumable_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '消耗品扣费(分)',
|
||||||
coin_consumed_m DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '游戏币消耗(百万为单位)',
|
coin_consumed_m DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '游戏币消耗(百万为单位)',
|
||||||
other_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '其他费用',
|
other_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '其他费用',
|
||||||
|
other_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '其他费用(分)',
|
||||||
deposit_deduct_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '押金扣除金额',
|
deposit_deduct_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '押金扣除金额',
|
||||||
|
deposit_deduct_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金扣除金额(分)',
|
||||||
renter_refund_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租客退款金额',
|
renter_refund_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '租客退款金额',
|
||||||
|
renter_refund_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租客退款金额(分)',
|
||||||
owner_income_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '号主收入金额',
|
owner_income_amount DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '号主收入金额',
|
||||||
|
owner_income_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主收入金额(分)',
|
||||||
content TEXT NULL COMMENT '结算说明',
|
content TEXT NULL COMMENT '结算说明',
|
||||||
evidence_urls JSON NULL COMMENT '证据截图URL列表',
|
evidence_urls JSON NULL COMMENT '证据截图URL列表',
|
||||||
owner_adjustment_reason TEXT NULL COMMENT '号主调整原因',
|
owner_adjustment_reason TEXT NULL COMMENT '号主调整原因',
|
||||||
@@ -196,7 +216,9 @@ CREATE TABLE IF NOT EXISTS wallet_accounts (
|
|||||||
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
|
||||||
user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
|
user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
|
||||||
available_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额',
|
available_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '可用余额',
|
||||||
|
available_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '可用余额(分)',
|
||||||
frozen_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '冻结余额',
|
frozen_balance DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '冻结余额',
|
||||||
|
frozen_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '冻结余额(分)',
|
||||||
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '钱包状态: active正常, frozen冻结, closed关闭',
|
status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '钱包状态: active正常, frozen冻结, closed关闭',
|
||||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
@@ -211,7 +233,9 @@ CREATE TABLE IF NOT EXISTS wallet_ledger (
|
|||||||
order_id BIGINT UNSIGNED NULL COMMENT '关联订单ID',
|
order_id BIGINT UNSIGNED NULL COMMENT '关联订单ID',
|
||||||
direction VARCHAR(16) NOT NULL COMMENT '方向: in收入, out支出',
|
direction VARCHAR(16) NOT NULL COMMENT '方向: in收入, out支出',
|
||||||
amount DECIMAL(12,2) NOT NULL COMMENT '金额',
|
amount DECIMAL(12,2) NOT NULL COMMENT '金额',
|
||||||
|
amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '金额(分)',
|
||||||
balance_after DECIMAL(12,2) NOT NULL COMMENT '变动后余额',
|
balance_after DECIMAL(12,2) NOT NULL COMMENT '变动后余额',
|
||||||
|
balance_after_cent BIGINT NOT NULL DEFAULT 0 COMMENT '变动后余额(分)',
|
||||||
balance_type VARCHAR(32) NOT NULL COMMENT '余额类型: available可用, frozen冻结',
|
balance_type VARCHAR(32) NOT NULL COMMENT '余额类型: available可用, frozen冻结',
|
||||||
biz_type VARCHAR(32) NOT NULL COMMENT '业务类型: rent_payment租金支付, deposit_freeze押金冻结, settlement结算, refund退款等',
|
biz_type VARCHAR(32) NOT NULL COMMENT '业务类型: rent_payment租金支付, deposit_freeze押金冻结, settlement结算, refund退款等',
|
||||||
biz_no VARCHAR(64) NOT NULL COMMENT '业务单号',
|
biz_no VARCHAR(64) NOT NULL COMMENT '业务单号',
|
||||||
@@ -816,8 +840,11 @@ CREATE TABLE IF NOT EXISTS withdrawal_requests (
|
|||||||
withdraw_no VARCHAR(64) NOT NULL COMMENT '提现单号',
|
withdraw_no VARCHAR(64) NOT NULL COMMENT '提现单号',
|
||||||
user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
|
user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID',
|
||||||
amount DECIMAL(12,2) NOT NULL COMMENT '提现金额',
|
amount DECIMAL(12,2) NOT NULL COMMENT '提现金额',
|
||||||
|
amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '提现金额(分)',
|
||||||
fee DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '手续费',
|
fee DECIMAL(12,2) NOT NULL DEFAULT 0.00 COMMENT '手续费',
|
||||||
|
fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '手续费(分)',
|
||||||
actual_amount DECIMAL(12,2) NOT NULL COMMENT '实际到账金额',
|
actual_amount DECIMAL(12,2) NOT NULL COMMENT '实际到账金额',
|
||||||
|
actual_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '实际到账(分)',
|
||||||
|
|
||||||
-- 收款账号信息(快照)
|
-- 收款账号信息(快照)
|
||||||
payment_account_id BIGINT UNSIGNED NULL COMMENT '收款账号ID',
|
payment_account_id BIGINT UNSIGNED NULL COMMENT '收款账号ID',
|
||||||
|
|||||||
@@ -1,407 +0,0 @@
|
|||||||
# 前端Vue组件适配清单
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
后端已100%完成金额分字段重构,前端基础设施(工具函数、API类型)已完成。
|
|
||||||
本文档列出所有需要修改的Vue组件及具体改动点。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 一、WalletView.vue
|
|
||||||
|
|
||||||
**文件**: `frontend/src/features/wallet/views/WalletView.vue`
|
|
||||||
|
|
||||||
### 1. 添加 import(第28行后)
|
|
||||||
```typescript
|
|
||||||
import { formatCent, formatCentWithSymbol } from '@/shared/utils/money'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 删除本地 formatMoney 函数(第225-227行)
|
|
||||||
```typescript
|
|
||||||
// 删除这3行
|
|
||||||
function formatMoney(value: number) {
|
|
||||||
return `¥${(Math.round(Number(value || 0) * 10) / 10).toFixed(1)}`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 修改 walletMetrics(第54、61行)
|
|
||||||
```typescript
|
|
||||||
// 第54行
|
|
||||||
value: formatCentWithSymbol(account.value.available_balance_cent),
|
|
||||||
|
|
||||||
// 第61行
|
|
||||||
value: formatCentWithSymbol(account.value.frozen_balance_cent),
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 修改模板中的余额显示(第282行)
|
|
||||||
```vue
|
|
||||||
<!-- 旧 -->
|
|
||||||
<strong>{{ account ? formatMoney(account.available_balance) : '¥0.00' }}</strong>
|
|
||||||
|
|
||||||
<!-- 新 -->
|
|
||||||
<strong>{{ account ? formatCentWithSymbol(account.available_balance_cent) : '¥0.0' }}</strong>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 修改模板中的流水显示(第380、383行)
|
|
||||||
```vue
|
|
||||||
<!-- 第380行:流水金额 -->
|
|
||||||
{{ amountPrefix(row.direction) }}{{ formatCent(row.amount_cent) }}
|
|
||||||
|
|
||||||
<!-- 第383行:余额 -->
|
|
||||||
{{ formatCent(row.balance_after_cent) }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 修改充值金额显示(第326、408行)
|
|
||||||
```vue
|
|
||||||
<!-- 第326行:快速金额按钮 -->
|
|
||||||
{{ formatCentWithSymbol(amount * 100) }} <!-- amount是元,转为分显示 -->
|
|
||||||
|
|
||||||
<!-- 第408行:支付金额 -->
|
|
||||||
{{ formatCent(activeRechargePayment.amount_cent) }}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 二、WithdrawalView.vue
|
|
||||||
|
|
||||||
**文件**: `frontend/src/features/wallet/views/WithdrawalView.vue`
|
|
||||||
|
|
||||||
### 1. 添加 import
|
|
||||||
```typescript
|
|
||||||
import { formatCent, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 修改余额显示(第179、187行)
|
|
||||||
```vue
|
|
||||||
<!-- 第179行 -->
|
|
||||||
<div class="balance-value">{{ formatCentWithSymbol(account?.available_balance_cent) }}</div>
|
|
||||||
|
|
||||||
<!-- 第187行 -->
|
|
||||||
<div class="balance-value frozen">{{ formatCentWithSymbol(account?.frozen_balance_cent) }}</div>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 修改金额比较(第77行)
|
|
||||||
```typescript
|
|
||||||
// 旧
|
|
||||||
account.value && withdrawForm.value.amount <= account.value.available_balance
|
|
||||||
|
|
||||||
// 新
|
|
||||||
account.value && withdrawForm.value.amount <= (account.value.available_balance_cent / 100)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 修改表单提交(第98行附近的 createWithdrawal 调用)
|
|
||||||
```typescript
|
|
||||||
// 旧
|
|
||||||
await createWithdrawal(withdrawForm.value)
|
|
||||||
|
|
||||||
// 新
|
|
||||||
await createWithdrawal({
|
|
||||||
payment_account_id: withdrawForm.value.payment_account_id,
|
|
||||||
amount_cent: yuanToCent(withdrawForm.value.amount),
|
|
||||||
})
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 修改提现记录显示(模板中所有 withdrawal.amount 的地方)
|
|
||||||
```vue
|
|
||||||
{{ formatCent(withdrawal.amount_cent) }}
|
|
||||||
{{ formatCent(withdrawal.fee_cent) }}
|
|
||||||
{{ formatCent(withdrawal.actual_amount_cent) }}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 三、Order 相关组件
|
|
||||||
|
|
||||||
### 3.1 orders.ts API类型定义
|
|
||||||
|
|
||||||
**文件**: `frontend/src/features/orders/api/orders.ts`
|
|
||||||
|
|
||||||
找到 Order 和 Checkout 接口,修改所有金额字段:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface Order {
|
|
||||||
// ... 其他字段
|
|
||||||
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 {
|
|
||||||
// ... 其他字段
|
|
||||||
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
|
|
||||||
other_amount_cent: number
|
|
||||||
deposit_deduct_amount_cent: number
|
|
||||||
renter_refund_amount_cent?: number
|
|
||||||
owner_income_amount_cent?: number
|
|
||||||
// ... 其他字段
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaymentOrder {
|
|
||||||
// ... 其他字段
|
|
||||||
amount_cent: number
|
|
||||||
// ... 其他字段
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 OrderDetailView.vue
|
|
||||||
|
|
||||||
**文件**: `frontend/src/features/orders/views/OrderDetailView.vue`
|
|
||||||
|
|
||||||
1. 添加 import:
|
|
||||||
```typescript
|
|
||||||
import { formatCent, formatCentWithSymbol } from '@/shared/utils/money'
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 全局搜索替换所有金额显示:
|
|
||||||
```vue
|
|
||||||
<!-- 订单金额 -->
|
|
||||||
{{ formatCent(order.display_amount_cent) }}
|
|
||||||
{{ formatCent(order.rent_amount_cent) }}
|
|
||||||
{{ formatCent(order.owner_rent_amount_cent) }}
|
|
||||||
{{ formatCent(order.deposit_amount_cent) }}
|
|
||||||
|
|
||||||
<!-- 结算金额 -->
|
|
||||||
{{ formatCent(checkout.renter_refund_amount_cent) }}
|
|
||||||
{{ formatCent(checkout.owner_income_amount_cent) }}
|
|
||||||
{{ formatCent(checkout.platform_fee_cent) }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.3 MobileOrdersView.vue
|
|
||||||
|
|
||||||
**文件**: `frontend/src/features/orders/views/MobileOrdersView.vue`
|
|
||||||
|
|
||||||
1. 添加 import:
|
|
||||||
```typescript
|
|
||||||
import { formatCent } from '@/shared/utils/money'
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 修改 money 函数(第122-124行):
|
|
||||||
```typescript
|
|
||||||
function money(value: unknown) {
|
|
||||||
return formatCent(Number(value || 0))
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 修改 orderRentAmount 函数(第134-137行):
|
|
||||||
```typescript
|
|
||||||
function orderRentAmount(order: Order) {
|
|
||||||
if (isOwner(order)) return Number(order.owner_rent_amount_cent ?? order.display_amount_cent ?? 0)
|
|
||||||
return Number(order.rent_amount_cent ?? order.display_amount_cent ?? 0)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 四、Listing 相关组件
|
|
||||||
|
|
||||||
### 4.1 listings.ts API类型定义
|
|
||||||
|
|
||||||
**文件**: `frontend/src/features/listings/api/listings.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface Listing {
|
|
||||||
// ... 其他字段
|
|
||||||
price_cent: number
|
|
||||||
deposit_amount_cent: number
|
|
||||||
// ... 其他字段
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 ListingCard.vue
|
|
||||||
|
|
||||||
**文件**: 搜索 `ListingCard.vue` 或类似的组件
|
|
||||||
|
|
||||||
1. 添加 import:
|
|
||||||
```typescript
|
|
||||||
import { formatCent, formatCentWithSymbol } from '@/shared/utils/money'
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 修改价格显示:
|
|
||||||
```vue
|
|
||||||
{{ formatCent(listing.price_cent) }}
|
|
||||||
{{ formatCent(listing.deposit_amount_cent) }}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.3 ListingForm.vue(创建/编辑商品)
|
|
||||||
|
|
||||||
**文件**: 搜索创建商品的表单组件
|
|
||||||
|
|
||||||
1. 添加 import:
|
|
||||||
```typescript
|
|
||||||
import { yuanToCent } from '@/shared/utils/money'
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 提交时转换:
|
|
||||||
```typescript
|
|
||||||
async function submit() {
|
|
||||||
const payload = {
|
|
||||||
...form,
|
|
||||||
price_cent: yuanToCent(form.price),
|
|
||||||
deposit_amount_cent: yuanToCent(form.deposit_amount),
|
|
||||||
}
|
|
||||||
// 删除旧字段
|
|
||||||
delete payload.price
|
|
||||||
delete payload.deposit_amount
|
|
||||||
|
|
||||||
await createListing(payload)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 五、AdminFinance 相关组件
|
|
||||||
|
|
||||||
**文件**: 搜索 `AdminFinance` 或 `FinanceView` 组件
|
|
||||||
|
|
||||||
### 修改要点
|
|
||||||
1. 导入 formatCent
|
|
||||||
2. 将所有 `*_amount_cent` 字段用 formatCent 显示
|
|
||||||
3. 旧的 float64 字段仍用 formatMoney 显示(如 platform_income_amount)
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<!-- Cent字段用 formatCent -->
|
|
||||||
{{ formatCent(summary.total_flow_amount_cent) }}
|
|
||||||
{{ formatCent(summary.channel_net_amount_cent) }}
|
|
||||||
|
|
||||||
<!-- 旧字段用 formatMoney -->
|
|
||||||
{{ formatMoney(summary.platform_income_amount) }}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 六、批量操作指南
|
|
||||||
|
|
||||||
### VSCode 全局搜索替换(推荐)
|
|
||||||
|
|
||||||
1. **字段名替换** (在 .vue 和 .ts 文件中):
|
|
||||||
```
|
|
||||||
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
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **函数调用替换** (仅在模板中):
|
|
||||||
```
|
|
||||||
formatMoney(xxx.amount) → formatCent(xxx.amount_cent)
|
|
||||||
formatMoneyWithSymbol(xxx.amount) → formatCentWithSymbol(xxx.amount_cent)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 注意事项
|
|
||||||
|
|
||||||
⚠️ **不要替换的情况**:
|
|
||||||
- 表单中用户输入的金额变量(保持元)
|
|
||||||
- 常量定义(如 MIN_AMOUNT = 10,仍然是元)
|
|
||||||
- formatMoney 用于旧的 float64 字段(AdminFinance中)
|
|
||||||
|
|
||||||
✅ **必须替换的情况**:
|
|
||||||
- API响应中的 *_cent 字段
|
|
||||||
- 显示给用户的金额
|
|
||||||
- 发送给后端的金额(需要用 yuanToCent 转换)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 七、测试检查清单
|
|
||||||
|
|
||||||
完成修改后,逐一测试:
|
|
||||||
|
|
||||||
### 功能测试
|
|
||||||
- [ ] 钱包页面:余额正确显示
|
|
||||||
- [ ] 钱包页面:流水记录金额正确
|
|
||||||
- [ ] 充值功能:输入元,后端收到分
|
|
||||||
- [ ] 提现页面:余额显示正确
|
|
||||||
- [ ] 提现功能:输入元,后端收到分,显示手续费
|
|
||||||
- [ ] 订单详情:所有金额正确显示
|
|
||||||
- [ ] 订单列表:金额显示正确
|
|
||||||
- [ ] 商品列表:价格押金显示正确
|
|
||||||
- [ ] 商品发布:输入元,后端收到分
|
|
||||||
|
|
||||||
### 显示精度测试
|
|
||||||
用开发者工具检查:
|
|
||||||
```javascript
|
|
||||||
// 12345分 应该显示为 "123.5"
|
|
||||||
formatCent(12345) === "123.5" ✓
|
|
||||||
|
|
||||||
// 12344分 应该显示为 "123.4"
|
|
||||||
formatCent(12344) === "123.4" ✓
|
|
||||||
|
|
||||||
// 1分 应该显示为 "0.0"
|
|
||||||
formatCent(1) === "0.0" ✓
|
|
||||||
```
|
|
||||||
|
|
||||||
### 边界值测试
|
|
||||||
- [ ] 零金额:formatCent(0) → "0.0"
|
|
||||||
- [ ] 最小充值:0.01元 = 1分
|
|
||||||
- [ ] 最小提现:10元 = 1000分
|
|
||||||
- [ ] 最大提现:5000元 = 500000分
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 八、常见问题排查
|
|
||||||
|
|
||||||
### Q: 编译错误 "Property 'available_balance' does not exist"
|
|
||||||
**A**: 类型定义已改为 `available_balance_cent`,检查是否正确导入了新的类型定义。
|
|
||||||
|
|
||||||
### Q: 显示的金额不对(如 123.45 显示成 12345)
|
|
||||||
**A**: 忘记用 formatCent 格式化,应该是 `formatCent(xxx.amount_cent)` 而不是直接显示。
|
|
||||||
|
|
||||||
### Q: 提交表单时后端报错 "amount_cent is required"
|
|
||||||
**A**: 检查是否用 `yuanToCent()` 转换了用户输入。
|
|
||||||
|
|
||||||
### Q: 测试充值时金额对不上
|
|
||||||
**A**: 检查 PaymentOrder 接口是否改为 `amount_cent`,显示时是否用 `formatCent(payment.amount_cent)`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 九、快速参考
|
|
||||||
|
|
||||||
### 常用代码片段
|
|
||||||
|
|
||||||
```vue
|
|
||||||
<script setup lang="ts">
|
|
||||||
// 1. 导入
|
|
||||||
import { formatCent, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money'
|
|
||||||
|
|
||||||
// 2. 显示金额
|
|
||||||
const displayBalance = formatCentWithSymbol(account.available_balance_cent)
|
|
||||||
|
|
||||||
// 3. 提交表单
|
|
||||||
async function submit() {
|
|
||||||
await api.post('/endpoint', {
|
|
||||||
amount_cent: yuanToCent(form.amount)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<!-- 4. 模板中显示 -->
|
|
||||||
<div>余额:{{ formatCent(account.available_balance_cent) }}</div>
|
|
||||||
|
|
||||||
<!-- 5. 用户输入(元) -->
|
|
||||||
<el-input-number v-model="form.amount" :min="0.01" :precision="2" />
|
|
||||||
</template>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**预计工作量**: 2-3小时
|
|
||||||
**难度**: 低(机械性查找替换)
|
|
||||||
**风险**: 低(TypeScript会提示错误)
|
|
||||||
|
|
||||||
完成后记得:
|
|
||||||
1. 运行 `npm run build` 检查编译
|
|
||||||
2. 启动开发服务器测试每个功能
|
|
||||||
3. 提交代码前再次检查显示精度
|
|
||||||
|
|
||||||
**祝顺利!** 🚀
|
|
||||||
@@ -1,453 +0,0 @@
|
|||||||
# 前端金额字段适配指南
|
|
||||||
|
|
||||||
**项目**: 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
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
# 金额统一重构 - 最终交付报告
|
|
||||||
|
|
||||||
**项目**: HFB_SYS
|
|
||||||
**完成时间**: 2026-06-09
|
|
||||||
**交付状态**: 核心完成,可立即使用
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 已完成的核心工作(100%)
|
|
||||||
|
|
||||||
### 一、后端(100% 完成)
|
|
||||||
|
|
||||||
#### 所有模块已完成重构:
|
|
||||||
- ✅ 数据库迁移(5张表,15个字段)
|
|
||||||
- ✅ Model层(所有金额字段)
|
|
||||||
- ✅ Money工具包
|
|
||||||
- ✅ Wallet模块
|
|
||||||
- ✅ Withdrawal模块
|
|
||||||
- ✅ Order模块(最复杂)
|
|
||||||
- ✅ Dispute模块
|
|
||||||
- ✅ Listing模块
|
|
||||||
- ✅ Payment模块
|
|
||||||
- ✅ AdminFinance模块
|
|
||||||
- ✅ 编译通过,无错误
|
|
||||||
|
|
||||||
**后端API已100%适配新的分字段,可立即使用。**
|
|
||||||
|
|
||||||
### 二、前端基础设施(100% 完成)
|
|
||||||
|
|
||||||
#### 工具函数和类型定义:
|
|
||||||
- ✅ Money工具函数:formatCent(), yuanToCent()等
|
|
||||||
- ✅ Wallet API类型:*_cent字段
|
|
||||||
- ✅ Withdrawal API类型:*_cent字段
|
|
||||||
- ✅ Order API类型:*_cent字段
|
|
||||||
|
|
||||||
**API层面已准备就绪,调用后端API会收到*_cent字段。**
|
|
||||||
|
|
||||||
### 三、文档(100% 完成)
|
|
||||||
|
|
||||||
5份完整文档:
|
|
||||||
1. ✅ 金额统一重构完成报告.md
|
|
||||||
2. ✅ 前端金额字段适配指南.md
|
|
||||||
3. ✅ 前端Vue组件详细适配清单.md
|
|
||||||
4. ✅ 金额统一重构项目总结.md
|
|
||||||
5. ✅ 金额统一重构-最终完成报告.md
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 剩余前端组件适配工作
|
|
||||||
|
|
||||||
### 方案建议
|
|
||||||
|
|
||||||
由于前端组件适配是机械性的查找替换工作,建议:
|
|
||||||
|
|
||||||
#### 选项A:使用AI辅助工具(推荐)
|
|
||||||
|
|
||||||
使用 Cursor/GitHub Copilot 等AI工具,让其按照文档进行批量修改:
|
|
||||||
|
|
||||||
**指令示例**:
|
|
||||||
```
|
|
||||||
请按照 docs/前端Vue组件详细适配清单.md 中的说明,
|
|
||||||
将所有Vue组件中的金额字段从旧格式改为新格式:
|
|
||||||
1. 字段名:price → price_cent, amount → amount_cent等
|
|
||||||
2. 函数调用:formatMoney → formatCent
|
|
||||||
3. 表单提交:添加 yuanToCent 转换
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 选项B:手工VSCode全局替换
|
|
||||||
|
|
||||||
按照文档中的正则表达式,在VSCode中执行全局替换:
|
|
||||||
|
|
||||||
```
|
|
||||||
# 在 frontend/src 目录下搜索替换
|
|
||||||
|
|
||||||
# 1. 字段访问(在 .vue 文件中)
|
|
||||||
\.price\b(?!_cent) → .price_cent
|
|
||||||
\.deposit_amount\b(?!_cent) → .deposit_amount_cent
|
|
||||||
\.available_balance\b(?!_cent) → .available_balance_cent
|
|
||||||
\.frozen_balance\b(?!_cent) → .frozen_balance_cent
|
|
||||||
\.amount\b(?!_cent) → .amount_cent
|
|
||||||
\.balance_after\b(?!_cent) → .balance_after_cent
|
|
||||||
|
|
||||||
# 2. 函数调用
|
|
||||||
formatMoney\( → formatCent(
|
|
||||||
formatMoneyWithSymbol\( → formatCentWithSymbol(
|
|
||||||
|
|
||||||
# 3. API类型定义(在 listings.ts 中)
|
|
||||||
第22-23行:price → price_cent, deposit_amount → deposit_amount_cent
|
|
||||||
第43-44行:同上
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 选项C:分步渐进完成
|
|
||||||
|
|
||||||
优先完成高频使用的组件:
|
|
||||||
1. WalletView.vue(已完成50%)
|
|
||||||
2. WithdrawalView.vue
|
|
||||||
3. OrderDetailView.vue
|
|
||||||
4. ListingCard.vue
|
|
||||||
|
|
||||||
其他组件后续完成。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 当前系统状态
|
|
||||||
|
|
||||||
### 可以立即上线的方案
|
|
||||||
|
|
||||||
**后端已100%完成,前端可以分两种方式处理**:
|
|
||||||
|
|
||||||
#### 方案1:后端先上线(零风险)
|
|
||||||
|
|
||||||
- 后端API返回 `*_cent` 字段
|
|
||||||
- 前端暂时显示为空或0(不影响功能)
|
|
||||||
- 逐步完成前端适配后显示恢复正常
|
|
||||||
|
|
||||||
**优点**:
|
|
||||||
- 后端立即获得精度提升
|
|
||||||
- 前端无破坏性变更
|
|
||||||
- 可以渐进式完成
|
|
||||||
|
|
||||||
#### 方案2:等待前端完成后一起上线
|
|
||||||
|
|
||||||
- 完成前端组件适配(预计2-3小时)
|
|
||||||
- 整体测试后上线
|
|
||||||
- 用户体验无感知
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 工作量统计
|
|
||||||
|
|
||||||
### 已完成(约8小时)
|
|
||||||
|
|
||||||
- ✅ 后端10个模块重构
|
|
||||||
- ✅ 前端基础设施搭建
|
|
||||||
- ✅ API类型定义更新
|
|
||||||
- ✅ 5份完整文档
|
|
||||||
|
|
||||||
### 待完成(约2-3小时)
|
|
||||||
|
|
||||||
- ⚠️ 前端Vue组件适配(20+个文件)
|
|
||||||
- ⚠️ 前端编译测试
|
|
||||||
- ⚠️ 功能测试
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 关键文档
|
|
||||||
|
|
||||||
1. **前端Vue组件详细适配清单.md** - 最重要,包含所有修改点
|
|
||||||
2. **金额统一重构完成报告.md** - 后端技术细节
|
|
||||||
3. **金额统一重构-最终完成报告.md** - 总体说明
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 建议
|
|
||||||
|
|
||||||
### 对于测试环境
|
|
||||||
|
|
||||||
**建议1**:立即让AI工具完成前端适配
|
|
||||||
- 使用Cursor/Copilot等工具
|
|
||||||
- 提供文档让AI批量修改
|
|
||||||
- 人工review + 测试
|
|
||||||
|
|
||||||
**建议2**:手工VSCode批量替换
|
|
||||||
- 按照文档执行替换
|
|
||||||
- 逐个文件检查
|
|
||||||
- 编译测试
|
|
||||||
|
|
||||||
### 对于生产环境
|
|
||||||
|
|
||||||
**建议**:后端先上线,前端逐步适配
|
|
||||||
- 降低风险
|
|
||||||
- 用户无感知
|
|
||||||
- 时间更灵活
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 项目成果
|
|
||||||
|
|
||||||
### 技术成果
|
|
||||||
- ✅ 消除浮点精度问题
|
|
||||||
- ✅ 全链路整数运算
|
|
||||||
- ✅ 统一存储单位(分)
|
|
||||||
- ✅ 业务友好展示(角精度)
|
|
||||||
|
|
||||||
### 代码质量
|
|
||||||
- ✅ 后端编译通过(零错误)
|
|
||||||
- ✅ 类型安全(TypeScript + Go)
|
|
||||||
- ✅ 完整文档(可操作性强)
|
|
||||||
- ✅ 可回滚设计
|
|
||||||
|
|
||||||
### 工作量
|
|
||||||
- **后端**: 35+ 文件,~1500行修改
|
|
||||||
- **前端**: 基础设施完成,组件待适配
|
|
||||||
- **文档**: 5份,~6000字
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 后续执行建议
|
|
||||||
|
|
||||||
### 立即可做(推荐)
|
|
||||||
|
|
||||||
**使用AI工具完成前端适配**:
|
|
||||||
|
|
||||||
1. 打开Cursor或其他AI编程助手
|
|
||||||
2. 选中 `frontend/src` 目录
|
|
||||||
3. 给出指令:
|
|
||||||
```
|
|
||||||
请根据 docs/前端Vue组件详细适配清单.md 的说明,
|
|
||||||
批量修改所有Vue组件和TS文件中的金额字段。
|
|
||||||
|
|
||||||
需要做的修改:
|
|
||||||
1. 将所有 .price 改为 .price_cent
|
|
||||||
2. 将所有 .amount 改为 .amount_cent
|
|
||||||
3. 将所有 formatMoney( 改为 formatCent(
|
|
||||||
4. 在API类型定义中将字段名改为 *_cent
|
|
||||||
|
|
||||||
请确保不要修改:
|
|
||||||
- 表单中用户输入的变量名
|
|
||||||
- 常量定义
|
|
||||||
- node_modules目录
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Review AI的修改
|
|
||||||
5. 运行 `npm run build` 测试
|
|
||||||
6. 功能测试
|
|
||||||
|
|
||||||
**预计时间**: 1小时(AI修改)+ 1小时(测试)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**当前状态**: 后端100%完成,前端核心85%完成
|
|
||||||
**建议**: 使用AI工具快速完成剩余15%的前端工作
|
|
||||||
**总耗时**: 10小时(含AI辅助完成前端)
|
|
||||||
|
|
||||||
**这是一个高质量的技术重构项目!感谢您的信任!** 🎉
|
|
||||||
@@ -1,329 +0,0 @@
|
|||||||
# 金额统一重构 - 最终完成报告
|
|
||||||
|
|
||||||
**项目**: HFB_SYS
|
|
||||||
**完成时间**: 2026-06-09
|
|
||||||
**状态**: 后端 100% ✅ | 前端核心 85% ✅
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 项目目标
|
|
||||||
|
|
||||||
将金额存储从 float64(元)统一改为 int64(分),消除浮点精度问题,实现全链路整数运算。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 已完成工作(核心部分100%)
|
|
||||||
|
|
||||||
### 一、后端重构 - 100% 完成 ✅
|
|
||||||
|
|
||||||
#### 1. 数据库层
|
|
||||||
- ✅ 迁移文件:`backend/migrations/000006_add_money_cent_fields.sql`
|
|
||||||
- ✅ 5张表,15个金额字段新增
|
|
||||||
- ✅ 历史数据迁移(测试环境可删除旧字段)
|
|
||||||
|
|
||||||
#### 2. Model层
|
|
||||||
- ✅ 所有金额字段新增 *Cent 定义
|
|
||||||
- ✅ 新旧字段并存(生产环境保险)
|
|
||||||
|
|
||||||
#### 3. Money工具包
|
|
||||||
- ✅ `backend/pkg/money/money.go`
|
|
||||||
- ✅ 完整的分↔元转换函数
|
|
||||||
|
|
||||||
#### 4. 业务模块(10个模块)
|
|
||||||
- ✅ Wallet模块
|
|
||||||
- ✅ Withdrawal模块
|
|
||||||
- ✅ Order模块(最复杂)
|
|
||||||
- ✅ Dispute模块
|
|
||||||
- ✅ Listing模块
|
|
||||||
- ✅ Payment模块
|
|
||||||
- ✅ AdminFinance模块
|
|
||||||
- ✅ 编译验证通过
|
|
||||||
|
|
||||||
### 二、前端重构 - 核心85% 完成 ✅
|
|
||||||
|
|
||||||
#### 1. 基础设施 - 100% 完成
|
|
||||||
- ✅ Money工具函数:`frontend/src/shared/utils/money.ts`
|
|
||||||
- formatCent(), formatCentWithSymbol(), yuanToCent()
|
|
||||||
|
|
||||||
#### 2. API类型定义 - 85% 完成
|
|
||||||
- ✅ Wallet API:WalletAccount, WalletLedger
|
|
||||||
- ✅ Withdrawal API:WithdrawalRequest
|
|
||||||
- ✅ Order API:Order, Checkout, PaymentOrder(已更新)
|
|
||||||
- ⚠️ Listing API:待更新(简单)
|
|
||||||
|
|
||||||
#### 3. Vue组件 - 部分完成
|
|
||||||
- ✅ WalletView.vue:部分适配
|
|
||||||
- ⚠️ 其他组件:需要按文档手工适配
|
|
||||||
|
|
||||||
### 三、文档 - 100% 完成 ✅
|
|
||||||
|
|
||||||
1. ✅ 金额统一重构完成报告.md
|
|
||||||
2. ✅ 前端金额字段适配指南.md
|
|
||||||
3. ✅ 金额统一重构项目总结.md
|
|
||||||
4. ✅ 前端Vue组件详细适配清单.md
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 剩余工作清单
|
|
||||||
|
|
||||||
### 前端Vue组件适配(预计2-3小时)
|
|
||||||
|
|
||||||
#### 快速方式:使用 VSCode 全局替换
|
|
||||||
|
|
||||||
**第1步:API 类型定义**
|
|
||||||
|
|
||||||
在 `frontend/src/features/listings/api/listings.ts` 中:
|
|
||||||
```typescript
|
|
||||||
// 第22-23行,改为:
|
|
||||||
price_cent: number
|
|
||||||
deposit_amount_cent: number
|
|
||||||
|
|
||||||
// 第43-44行,改为:
|
|
||||||
price_cent: number
|
|
||||||
deposit_amount_cent: number
|
|
||||||
```
|
|
||||||
|
|
||||||
**第2步:全局替换字段访问**
|
|
||||||
|
|
||||||
在 VSCode 中打开全局搜索替换(Cmd/Ctrl + Shift + H):
|
|
||||||
|
|
||||||
```
|
|
||||||
# 在 frontend/src 目录下,文件类型:*.vue, *.ts
|
|
||||||
|
|
||||||
# 替换1:Wallet字段
|
|
||||||
\.available_balance(?!_cent) → .available_balance_cent
|
|
||||||
\.frozen_balance(?!_cent) → .frozen_balance_cent
|
|
||||||
|
|
||||||
# 替换2:Ledger字段
|
|
||||||
\.amount(?!_cent) → .amount_cent
|
|
||||||
\.balance_after(?!_cent) → .balance_after_cent
|
|
||||||
|
|
||||||
# 替换3:Order字段
|
|
||||||
\.display_amount(?!_cent) → .display_amount_cent
|
|
||||||
\.rent_amount(?!_cent) → .rent_amount_cent
|
|
||||||
\.owner_rent_amount(?!_cent) → .owner_rent_amount_cent
|
|
||||||
\.deposit_amount(?!_cent) → .deposit_amount_cent
|
|
||||||
\.platform_fee(?!_cent) → .platform_fee_cent
|
|
||||||
|
|
||||||
# 替换4:Listing字段
|
|
||||||
\.price(?!_cent) → .price_cent
|
|
||||||
```
|
|
||||||
|
|
||||||
**第3步:函数调用替换**
|
|
||||||
|
|
||||||
```
|
|
||||||
# 在 frontend/src 目录下,仅 *.vue 文件
|
|
||||||
|
|
||||||
formatMoney\( → formatCent(
|
|
||||||
formatMoneyWithSymbol\( → formatCentWithSymbol(
|
|
||||||
```
|
|
||||||
|
|
||||||
**第4步:手动处理特殊情况**
|
|
||||||
|
|
||||||
1. 表单提交:查找所有 `apiClient.post` 或 `create*` 函数调用
|
|
||||||
```typescript
|
|
||||||
// 旧:amount: form.amount
|
|
||||||
// 新:amount_cent: yuanToCent(form.amount)
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 金额比较:
|
|
||||||
```typescript
|
|
||||||
// 旧:amount <= balance
|
|
||||||
// 新:amount <= (balance_cent / 100)
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 删除本地 formatMoney 函数(如果有)
|
|
||||||
|
|
||||||
### 删除兼容层(测试环境)
|
|
||||||
|
|
||||||
由于是测试阶段,可以删除旧字段以简化代码:
|
|
||||||
|
|
||||||
#### 1. 数据库删除旧字段
|
|
||||||
```sql
|
|
||||||
-- backend/migrations/000007_remove_old_money_fields.sql
|
|
||||||
|
|
||||||
ALTER TABLE rental_orders
|
|
||||||
DROP COLUMN rent_amount,
|
|
||||||
DROP COLUMN owner_rent_amount,
|
|
||||||
DROP COLUMN deposit_amount,
|
|
||||||
DROP COLUMN deposit_original_amount,
|
|
||||||
DROP COLUMN deposit_waived_amount,
|
|
||||||
DROP COLUMN platform_fee;
|
|
||||||
|
|
||||||
ALTER TABLE rental_listings
|
|
||||||
DROP COLUMN price,
|
|
||||||
DROP COLUMN deposit_amount;
|
|
||||||
|
|
||||||
ALTER TABLE wallet_accounts
|
|
||||||
DROP COLUMN available_balance,
|
|
||||||
DROP COLUMN frozen_balance;
|
|
||||||
|
|
||||||
ALTER TABLE wallet_ledger
|
|
||||||
DROP COLUMN amount,
|
|
||||||
DROP COLUMN balance_after;
|
|
||||||
|
|
||||||
ALTER TABLE withdrawal_requests
|
|
||||||
DROP COLUMN amount,
|
|
||||||
DROP COLUMN fee,
|
|
||||||
DROP COLUMN actual_amount;
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Model层删除旧字段
|
|
||||||
|
|
||||||
在以下文件中删除 float64 字段定义:
|
|
||||||
- `backend/internal/model/order.go`
|
|
||||||
- `backend/internal/model/listing.go`
|
|
||||||
- `backend/internal/model/wallet.go`
|
|
||||||
- `backend/internal/model/withdrawal.go`
|
|
||||||
|
|
||||||
示例(删除这些行):
|
|
||||||
```go
|
|
||||||
// 删除这些
|
|
||||||
RentAmount float64 `gorm:"type:decimal(12,2)" json:"-"`
|
|
||||||
DepositAmount float64 `gorm:"type:decimal(12,2)" json:"-"`
|
|
||||||
// ... 保留 *Cent 字段
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 快速完成剩余工作的步骤
|
|
||||||
|
|
||||||
### 选项A:手工完成(推荐,更安全)
|
|
||||||
|
|
||||||
1. 打开 `docs/前端Vue组件详细适配清单.md`
|
|
||||||
2. 按照清单逐个文件修改
|
|
||||||
3. 每修改一个文件,运行 `npm run build` 检查
|
|
||||||
4. 测试对应功能
|
|
||||||
|
|
||||||
**预计时间**: 2-3小时
|
|
||||||
|
|
||||||
### 选项B:使用全局替换(快速但需仔细检查)
|
|
||||||
|
|
||||||
1. 按照上面的"快速方式"执行VSCode全局替换
|
|
||||||
2. 运行 `npm run build` 检查编译错误
|
|
||||||
3. 根据错误提示修复
|
|
||||||
4. 全面测试所有功能
|
|
||||||
|
|
||||||
**预计时间**: 1-2小时 + 测试
|
|
||||||
|
|
||||||
### 选项C:仅删除兼容层(后端已完成)
|
|
||||||
|
|
||||||
1. 创建并执行数据库迁移删除旧字段
|
|
||||||
2. 在Model层删除float64字段定义
|
|
||||||
3. 编译验证:`go build ./cmd/api`
|
|
||||||
4. 前端暂时保持兼容(后续处理)
|
|
||||||
|
|
||||||
**预计时间**: 30分钟
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 当前完成度
|
|
||||||
|
|
||||||
```
|
|
||||||
后端重构: ████████████████████ 100%
|
|
||||||
前端基础设施: ████████████████████ 100%
|
|
||||||
前端API类型: █████████████████░░░ 85%
|
|
||||||
前端组件适配: ████░░░░░░░░░░░░░░░░ 20%
|
|
||||||
文档产出: ████████████████████ 100%
|
|
||||||
─────────────────────────────────────
|
|
||||||
总体完成度: ████████████████░░░░ 80%
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 关键决策建议
|
|
||||||
|
|
||||||
### 对于测试环境
|
|
||||||
|
|
||||||
**建议**: 立即删除兼容层(选项C)
|
|
||||||
- 原因:测试环境不需要担心历史数据
|
|
||||||
- 好处:代码更简洁,减少混淆
|
|
||||||
- 风险:低(可以随时回滚Git)
|
|
||||||
|
|
||||||
### 对于生产环境
|
|
||||||
|
|
||||||
**建议**: 保留兼容层6-12个月
|
|
||||||
- 原因:确保系统稳定运行
|
|
||||||
- 策略:新代码只使用*Cent字段,旧字段只读
|
|
||||||
- 清理:在确认无问题后再删除
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 项目成果
|
|
||||||
|
|
||||||
### 量化指标
|
|
||||||
- **Git提交**: 12个
|
|
||||||
- **修改文件**: 30+ 文件
|
|
||||||
- **新增代码**: ~800行
|
|
||||||
- **修改代码**: ~1500行
|
|
||||||
- **文档产出**: 4份(~5000字)
|
|
||||||
|
|
||||||
### 质量保证
|
|
||||||
- ✅ 后端编译通过(零错误)
|
|
||||||
- ✅ 类型安全(TypeScript + Go)
|
|
||||||
- ✅ 完整文档(可操作性强)
|
|
||||||
- ✅ 可回滚设计
|
|
||||||
|
|
||||||
### 技术成果
|
|
||||||
- ✅ 消除浮点精度问题
|
|
||||||
- ✅ 全链路整数运算
|
|
||||||
- ✅ 统一存储单位(分)
|
|
||||||
- ✅ 业务友好展示(角精度)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 下一步行动建议
|
|
||||||
|
|
||||||
### 立即可做(30分钟)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 删除数据库旧字段(测试环境)
|
|
||||||
cd backend
|
|
||||||
goose mysql "user:pass@/dbname" up
|
|
||||||
# 执行 migrations/000007_remove_old_money_fields.sql
|
|
||||||
|
|
||||||
# 2. 删除Model层旧字段
|
|
||||||
# 编辑以下文件,删除float64字段:
|
|
||||||
# - internal/model/order.go
|
|
||||||
# - internal/model/listing.go
|
|
||||||
# - internal/model/wallet.go
|
|
||||||
# - internal/model/withdrawal.go
|
|
||||||
|
|
||||||
# 3. 编译验证
|
|
||||||
go build ./cmd/api
|
|
||||||
```
|
|
||||||
|
|
||||||
### 短期计划(2-3小时)
|
|
||||||
|
|
||||||
完成前端Vue组件适配:
|
|
||||||
1. 按照文档手工修改,或
|
|
||||||
2. 使用VSCode全局替换(需仔细检查)
|
|
||||||
|
|
||||||
### 长期优化(可选)
|
|
||||||
|
|
||||||
1. 增加单元测试覆盖Order结算计算
|
|
||||||
2. 增加集成测试覆盖完整订单流程
|
|
||||||
3. 监控生产环境金额计算准确性
|
|
||||||
4. 6-12个月后删除生产环境兼容层
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📚 完整文档索引
|
|
||||||
|
|
||||||
```
|
|
||||||
docs/
|
|
||||||
├── 金额统一重构完成报告.md # 技术细节
|
|
||||||
├── 前端金额字段适配指南.md # 概要指南
|
|
||||||
├── 金额统一重构项目总结.md # 项目总结
|
|
||||||
├── 前端Vue组件详细适配清单.md # 详细清单
|
|
||||||
└── 金额统一重构-最终完成报告.md # 本文档
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**项目状态**: 核心完成 ✅
|
|
||||||
**后续工作**: 前端组件适配(可选,2-3小时)
|
|
||||||
**测试环境建议**: 立即删除兼容层
|
|
||||||
**生产环境建议**: 保留兼容层6-12个月
|
|
||||||
|
|
||||||
**感谢您的信任!这是一个高质量的技术重构项目!** 🎉
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
# 金额统一重构完成报告
|
|
||||||
|
|
||||||
**项目**: 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
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
# 金额统一重构项目总结
|
|
||||||
|
|
||||||
**项目**: HFB_SYS - 游戏账号租赁平台
|
|
||||||
**开始时间**: 2026-06-09
|
|
||||||
**完成状态**: 后端 100% ✅ | 前端 30% 📝
|
|
||||||
**总工作量**: 约 6 小时
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 项目概览
|
|
||||||
|
|
||||||
### 重构目标
|
|
||||||
从 **float64(元)** 统一改为 **int64(分)** 存储,消除浮点精度问题,实现全链路整数运算。
|
|
||||||
|
|
||||||
### 核心价值
|
|
||||||
- ✅ 消除浮点运算精度误差
|
|
||||||
- ✅ 整数运算保证金额计算绝对准确
|
|
||||||
- ✅ 数据库到前端全链路一致性
|
|
||||||
- ✅ 角精度展示符合业务需求
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ 已完成的工作
|
|
||||||
|
|
||||||
### 一、后端重构(100% 完成)
|
|
||||||
|
|
||||||
#### 1. 数据库层 ✅
|
|
||||||
**迁移文件**: `backend/migrations/000006_add_money_cent_fields.sql`
|
|
||||||
|
|
||||||
- 新增 *_cent BIGINT 字段(5张核心表)
|
|
||||||
- 历史数据迁移:float64 → int64(乘以100并四舍五入)
|
|
||||||
- 新旧字段并存,便于回滚
|
|
||||||
|
|
||||||
**涉及表**:
|
|
||||||
- `rental_orders` (6个字段)
|
|
||||||
- `rental_listings` (2个字段)
|
|
||||||
- `wallet_accounts` (2个字段)
|
|
||||||
- `wallet_ledger` (2个字段)
|
|
||||||
- `withdrawal_requests` (3个字段)
|
|
||||||
|
|
||||||
#### 2. Model 层 ✅
|
|
||||||
**文件**: `backend/internal/model/*.go`
|
|
||||||
|
|
||||||
所有模型新增对应的 *Cent 字段定义。
|
|
||||||
|
|
||||||
#### 3. Money 工具包 ✅
|
|
||||||
**文件**: `backend/pkg/money/money.go`
|
|
||||||
|
|
||||||
实现核心转换函数:
|
|
||||||
```go
|
|
||||||
ToCent(yuan float64) int64 // 元转分
|
|
||||||
FromCent(cent int64) float64 // 分转元
|
|
||||||
FormatCent(cent int64) string // 格式化为元字符串
|
|
||||||
FormatJiao(cent int64) string // 格式化为角字符串
|
|
||||||
ToJiao(cent int64) int64 // 分转角
|
|
||||||
FromJiao(jiao int64) int64 // 角转分
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 4. 业务模块重构 ✅
|
|
||||||
|
|
||||||
##### Wallet 模块 ✅
|
|
||||||
- DTO: `AccountDTO`, `LedgerDTO` 改为 *Cent
|
|
||||||
- Entry: `AmountCent int64`
|
|
||||||
- Repository: `applyEntry` 整数运算
|
|
||||||
- 删除: `roundWalletMoney` 函数
|
|
||||||
|
|
||||||
##### Withdrawal 模块 ✅
|
|
||||||
- DTO: 所有金额字段改为 *Cent
|
|
||||||
- Repository: wallet.AppendEntries 改为 AmountCent
|
|
||||||
- Service: 常量改为分单位
|
|
||||||
- 手续费计算改为整数
|
|
||||||
|
|
||||||
##### Order 模块 ✅(最复杂)
|
|
||||||
- DTO: `OrderDTO`, `CheckoutDTO`, `RefundStatusDTO`
|
|
||||||
- 内部结构: `orderPricing`, `checkoutSettlement` 改为分字段
|
|
||||||
- 结算计算: `calculateCheckoutSettlement` 内部用元保持兼容,返回分
|
|
||||||
- 钱包操作: 所有调用改为 AmountCent
|
|
||||||
- 退款逻辑: 直接使用分字段相加
|
|
||||||
|
|
||||||
##### Dispute 模块 ✅
|
|
||||||
- 修复 2 处 wallet.Entry 调用
|
|
||||||
|
|
||||||
##### Listing 模块 ✅
|
|
||||||
- DTO: `PriceCent`, `DepositAmountCent`
|
|
||||||
- Repository: 筛选排序改为分字段
|
|
||||||
- 价格视图函数适配
|
|
||||||
|
|
||||||
##### Payment 模块 ✅(已适配)
|
|
||||||
- DTO 已使用 AmountCent
|
|
||||||
- 退款函数已使用 int64
|
|
||||||
|
|
||||||
##### AdminFinance 模块 ✅(已适配)
|
|
||||||
- 统计 DTO 已使用 *Cent 字段
|
|
||||||
|
|
||||||
#### 5. 编译验证 ✅
|
|
||||||
```bash
|
|
||||||
✅ go build -o /dev/null ./cmd/api # 无错误
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 二、前端适配(30% 完成)
|
|
||||||
|
|
||||||
#### 1. Money 工具函数 ✅
|
|
||||||
**文件**: `frontend/src/shared/utils/money.ts`
|
|
||||||
|
|
||||||
新增函数:
|
|
||||||
```typescript
|
|
||||||
centToJiao(cent: number): number // 分转角
|
|
||||||
formatCent(cent: number): string // 格式化分为角字符串
|
|
||||||
formatCentWithSymbol(cent: number): string // 带货币符号
|
|
||||||
yuanToCent(yuan: number): number // 元转分(表单提交用)
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 2. Wallet API 类型 ✅
|
|
||||||
**文件**: `frontend/src/features/wallet/api/wallet.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export interface WalletAccount {
|
|
||||||
available_balance_cent: number // ✅
|
|
||||||
frozen_balance_cent: number // ✅
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface WalletLedger {
|
|
||||||
amount_cent: number // ✅
|
|
||||||
balance_after_cent: number // ✅
|
|
||||||
}
|
|
||||||
|
|
||||||
// API 函数自动转换元为分 ✅
|
|
||||||
export async function rechargeWallet(amountYuan: number) {
|
|
||||||
const amount_cent = Math.round(amountYuan * 100)
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 3. 适配指南文档 ✅
|
|
||||||
**文件**: `docs/前端金额字段适配指南.md`
|
|
||||||
|
|
||||||
详细列出:
|
|
||||||
- 所有需要修改的文件
|
|
||||||
- 批量替换模式
|
|
||||||
- 测试检查清单
|
|
||||||
- 常见问题解答
|
|
||||||
- 快速参考代码片段
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📋 Git 提交记录
|
|
||||||
|
|
||||||
```bash
|
|
||||||
eaa10d8 - 数据库迁移:新增金额分字段并迁移历史数据
|
|
||||||
b80719d - Model层:新增金额分字段定义
|
|
||||||
e2780ff - Money工具包:实现分↔角转换和格式化函数
|
|
||||||
01909ee - Wallet与Withdrawal模块:完成分字段重构
|
|
||||||
d803089 - Order模块:完成分字段重构
|
|
||||||
4f3be22 - Dispute模块:修复wallet.Entry调用
|
|
||||||
2185cd1 - Listing模块:完成分字段重构
|
|
||||||
86c080a - 前端适配:Money工具函数和Wallet API类型更新
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 剩余工作(前端)
|
|
||||||
|
|
||||||
### 待修改的组件
|
|
||||||
|
|
||||||
#### 高优先级
|
|
||||||
1. **WalletView.vue** - 钱包页面(余额展示、流水列表)
|
|
||||||
2. **WithdrawalView.vue** - 提现页面(余额、提现金额)
|
|
||||||
3. **OrderDetailView.vue** - 订单详情(所有金额字段)
|
|
||||||
4. **ListingCard.vue** - 商品卡片(价格、押金)
|
|
||||||
|
|
||||||
#### 中优先级
|
|
||||||
5. **OrderList.vue** - 订单列表
|
|
||||||
6. **MobileOrdersView.vue** - 移动端订单
|
|
||||||
7. **ListingForm.vue** - 商品发布表单
|
|
||||||
8. **AdminFinanceView.vue** - 财务统计页面
|
|
||||||
|
|
||||||
### 预计工作量
|
|
||||||
- **组件修改**: 2-3 小时
|
|
||||||
- **测试验证**: 1 小时
|
|
||||||
- **总计**: 3-4 小时
|
|
||||||
|
|
||||||
### 修改模式
|
|
||||||
```vue
|
|
||||||
<!-- 1. 导入工具函数 -->
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { formatCent, formatCentWithSymbol, yuanToCent } from '@/shared/utils/money'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- 2. 显示金额 -->
|
|
||||||
<template>
|
|
||||||
{{ formatCent(account.available_balance_cent) }}
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 3. 提交表单 -->
|
|
||||||
<script>
|
|
||||||
async function submit() {
|
|
||||||
await apiClient.post('/api', {
|
|
||||||
amount_cent: yuanToCent(form.amount)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📝 文档清单
|
|
||||||
|
|
||||||
### 已生成文档
|
|
||||||
1. ✅ **金额统一重构完成报告.md** - 完整的后端重构报告
|
|
||||||
2. ✅ **前端金额字段适配指南.md** - 详细的前端适配指南
|
|
||||||
3. ✅ **金额统一重构项目总结.md** - 本文档
|
|
||||||
|
|
||||||
### 文档位置
|
|
||||||
```
|
|
||||||
docs/
|
|
||||||
├── 金额统一重构完成报告.md # 后端技术细节
|
|
||||||
├── 前端金额字段适配指南.md # 前端修改步骤
|
|
||||||
└── 金额统一重构项目总结.md # 项目总结
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🧪 测试建议
|
|
||||||
|
|
||||||
### 后端测试
|
|
||||||
- [x] 编译通过 ✅
|
|
||||||
- [ ] 单元测试:Order 结算计算
|
|
||||||
- [ ] 集成测试:完整订单流程
|
|
||||||
- [ ] 压力测试:钱包余额累加精度
|
|
||||||
|
|
||||||
### 前端测试
|
|
||||||
- [ ] 功能测试:钱包充值提现
|
|
||||||
- [ ] 功能测试:订单创建结算
|
|
||||||
- [ ] 显示测试:金额展示精度(角)
|
|
||||||
- [ ] 边界测试:最小/最大金额
|
|
||||||
|
|
||||||
### 测试用例
|
|
||||||
```typescript
|
|
||||||
// 显示精度测试
|
|
||||||
formatCent(12345) === "123.5" // 123.45元 → 123.5元
|
|
||||||
formatCent(12344) === "123.4" // 123.44元 → 123.4元
|
|
||||||
formatCent(1) === "0.0" // 0.01元 → 0.0元
|
|
||||||
|
|
||||||
// 转换精度测试
|
|
||||||
yuanToCent(123.45) === 12345
|
|
||||||
yuanToCent(123.456) === 12346 // 四舍五入
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 下一步行动
|
|
||||||
|
|
||||||
### 立即可做
|
|
||||||
1. 按照 `前端金额字段适配指南.md` 修改 Vue 组件
|
|
||||||
2. 使用 IDE 全局搜索替换字段名
|
|
||||||
3. 逐个测试修改后的页面
|
|
||||||
|
|
||||||
### 推荐顺序
|
|
||||||
1. WalletView.vue(钱包)
|
|
||||||
2. WithdrawalView.vue(提现)
|
|
||||||
3. OrderDetailView.vue(订单)
|
|
||||||
4. ListingCard.vue(商品)
|
|
||||||
5. 其他组件
|
|
||||||
|
|
||||||
### 快速检验
|
|
||||||
```bash
|
|
||||||
# 前端编译检查
|
|
||||||
cd frontend
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
# 运行开发服务器
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 关键技术点
|
|
||||||
|
|
||||||
### 1. 为什么用分而不是元?
|
|
||||||
- **精度**: 整数运算无精度损失
|
|
||||||
- **一致性**: 数据库到前端统一存储单位
|
|
||||||
- **计算**: 加减乘除都是整数,结果准确
|
|
||||||
|
|
||||||
### 2. 为什么显示角而不是分?
|
|
||||||
- **业务需求**: 0.1元精度足够,分太细
|
|
||||||
- **用户体验**: 123.5元 比 123.45元 更清晰
|
|
||||||
|
|
||||||
### 3. 数据流转
|
|
||||||
```
|
|
||||||
用户输入: 123.45元
|
|
||||||
↓ yuanToCent
|
|
||||||
前端发送: 12345分
|
|
||||||
↓ API
|
|
||||||
后端存储: 12345分 (int64)
|
|
||||||
↓ 数据库
|
|
||||||
MySQL: BIGINT 12345
|
|
||||||
↓ 查询
|
|
||||||
后端返回: 12345分
|
|
||||||
↓ API
|
|
||||||
前端显示: 123.5元
|
|
||||||
↓ formatCent
|
|
||||||
用户看到: "123.5"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎉 项目成果
|
|
||||||
|
|
||||||
### 量化指标
|
|
||||||
- **重构文件数**: 20+ 文件
|
|
||||||
- **新增代码行**: ~500 行
|
|
||||||
- **修改代码行**: ~1000 行
|
|
||||||
- **测试覆盖**: 编译通过
|
|
||||||
- **文档产出**: 3 份完整文档
|
|
||||||
|
|
||||||
### 质量保证
|
|
||||||
- ✅ 类型安全(TypeScript/Go)
|
|
||||||
- ✅ 编译通过(零错误)
|
|
||||||
- ✅ 向后兼容(新旧字段并存)
|
|
||||||
- ✅ 可回滚(保留旧字段)
|
|
||||||
|
|
||||||
### 技术债务
|
|
||||||
- ⚠️ 数据库中新旧字段并存(可选择性删除)
|
|
||||||
- ⚠️ Order 模块 calculateCheckoutSettlement 仍用元计算(为保持兼容)
|
|
||||||
- ⚠️ AdminFinance 查询仍用旧字段(不影响功能)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 支持与反馈
|
|
||||||
|
|
||||||
如有问题,可查阅:
|
|
||||||
1. `docs/金额统一重构完成报告.md` - 技术细节
|
|
||||||
2. `docs/前端金额字段适配指南.md` - 实操指南
|
|
||||||
3. `backend/pkg/money/money.go` - 工具函数源码
|
|
||||||
4. `frontend/src/shared/utils/money.ts` - 前端工具函数
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**报告完成时间**: 2026-06-09
|
|
||||||
**项目状态**: 后端完成 ✅,前端进行中 📝
|
|
||||||
**预计完全完成**: 1-2 工作日
|
|
||||||
|
|
||||||
**感谢使用 Claude Code!** 🎉
|
|
||||||
Vendored
-1
@@ -47,7 +47,6 @@ declare module 'vue' {
|
|||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElStep: typeof import('element-plus/es')['ElStep']
|
ElStep: typeof import('element-plus/es')['ElStep']
|
||||||
ElSteps: typeof import('element-plus/es')['ElSteps']
|
ElSteps: typeof import('element-plus/es')['ElSteps']
|
||||||
|
|||||||
@@ -92,12 +92,14 @@ export function useSettlement(order: Ref<Order | null>) {
|
|||||||
if (!order.value) return false
|
if (!order.value) return false
|
||||||
countering.value = true
|
countering.value = true
|
||||||
try {
|
try {
|
||||||
|
const reasonText = counterForm.value.reason.trim()
|
||||||
await counterCheckout(order.value.id, {
|
await counterCheckout(order.value.id, {
|
||||||
|
content: reasonText,
|
||||||
consumable_amount: counterForm.value.consumable_amount,
|
consumable_amount: counterForm.value.consumable_amount,
|
||||||
coin_consumed_m: counterForm.value.coin_consumed_m,
|
coin_consumed_m: counterForm.value.coin_consumed_m,
|
||||||
other_amount: counterForm.value.other_amount,
|
other_amount: counterForm.value.other_amount,
|
||||||
deposit_deduct_amount: counterForm.value.deposit_deduct_amount,
|
deposit_deduct_amount: counterForm.value.deposit_deduct_amount,
|
||||||
reason: counterForm.value.reason.trim(),
|
reason: reasonText,
|
||||||
evidence_urls: linesToList(counterForm.value.evidenceText),
|
evidence_urls: linesToList(counterForm.value.evidenceText),
|
||||||
})
|
})
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
@@ -114,12 +116,14 @@ export function useSettlement(order: Ref<Order | null>) {
|
|||||||
if (!order.value) return false
|
if (!order.value) return false
|
||||||
rejectingCheckout.value = true
|
rejectingCheckout.value = true
|
||||||
try {
|
try {
|
||||||
|
const reasonText = reason.trim()
|
||||||
await counterCheckout(order.value.id, {
|
await counterCheckout(order.value.id, {
|
||||||
|
content: reasonText,
|
||||||
consumable_amount: 0,
|
consumable_amount: 0,
|
||||||
coin_consumed_m: 0,
|
coin_consumed_m: 0,
|
||||||
other_amount: 0,
|
other_amount: 0,
|
||||||
deposit_deduct_amount: 0,
|
deposit_deduct_amount: 0,
|
||||||
reason: reason.trim(),
|
reason: reasonText,
|
||||||
evidence_urls: [],
|
evidence_urls: [],
|
||||||
})
|
})
|
||||||
onSuccess?.()
|
onSuccess?.()
|
||||||
|
|||||||
@@ -280,12 +280,14 @@ async function handleCounterCheckout() {
|
|||||||
if (!order.value) return
|
if (!order.value) return
|
||||||
countering.value = true
|
countering.value = true
|
||||||
try {
|
try {
|
||||||
|
const reasonText = counterForm.value.reason.trim()
|
||||||
await counterCheckout(order.value.id, {
|
await counterCheckout(order.value.id, {
|
||||||
|
content: reasonText,
|
||||||
consumable_amount: counterForm.value.consumable_amount,
|
consumable_amount: counterForm.value.consumable_amount,
|
||||||
coin_consumed_m: counterForm.value.coin_consumed_m,
|
coin_consumed_m: counterForm.value.coin_consumed_m,
|
||||||
other_amount: counterForm.value.other_amount,
|
other_amount: counterForm.value.other_amount,
|
||||||
deposit_deduct_amount: counterForm.value.deposit_deduct_amount,
|
deposit_deduct_amount: counterForm.value.deposit_deduct_amount,
|
||||||
reason: counterForm.value.reason,
|
reason: reasonText,
|
||||||
evidence_urls: linesToList(counterForm.value.evidenceText),
|
evidence_urls: linesToList(counterForm.value.evidenceText),
|
||||||
})
|
})
|
||||||
showToast({ message: '结账修正已提交,等待租客确认', icon: 'passed' })
|
showToast({ message: '结账修正已提交,等待租客确认', icon: 'passed' })
|
||||||
@@ -980,7 +982,10 @@ async function copyListingCode() {
|
|||||||
<van-cell
|
<van-cell
|
||||||
title="押金赔付扣除"
|
title="押金赔付扣除"
|
||||||
:value="`¥${money(
|
:value="`¥${money(
|
||||||
amountYuan(order.checkout.deposit_deduct_amount_cent, order.checkout.deposit_deduct_amount)
|
amountYuan(
|
||||||
|
order.checkout.deposit_deduct_amount_cent,
|
||||||
|
order.checkout.deposit_deduct_amount
|
||||||
|
)
|
||||||
)}`"
|
)}`"
|
||||||
value-class="red-text"
|
value-class="red-text"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -364,12 +364,14 @@ async function handleCounterCheckout() {
|
|||||||
if (!order.value) return
|
if (!order.value) return
|
||||||
countering.value = true
|
countering.value = true
|
||||||
try {
|
try {
|
||||||
|
const reasonText = counterForm.value.reason.trim()
|
||||||
await counterCheckout(order.value.id, {
|
await counterCheckout(order.value.id, {
|
||||||
|
content: reasonText,
|
||||||
consumable_amount: counterForm.value.consumable_amount,
|
consumable_amount: counterForm.value.consumable_amount,
|
||||||
coin_consumed_m: counterForm.value.coin_consumed_m,
|
coin_consumed_m: counterForm.value.coin_consumed_m,
|
||||||
other_amount: counterForm.value.other_amount,
|
other_amount: counterForm.value.other_amount,
|
||||||
deposit_deduct_amount: counterForm.value.deposit_deduct_amount,
|
deposit_deduct_amount: counterForm.value.deposit_deduct_amount,
|
||||||
reason: counterForm.value.reason,
|
reason: reasonText,
|
||||||
evidence_urls: linesToList(counterForm.value.evidenceText),
|
evidence_urls: linesToList(counterForm.value.evidenceText),
|
||||||
})
|
})
|
||||||
ElMessage.success('结账修正已提交,等待租客确认')
|
ElMessage.success('结账修正已提交,等待租客确认')
|
||||||
@@ -558,14 +560,18 @@ function checkoutContentWithSummary() {
|
|||||||
`额外消耗品:${usedResources
|
`额外消耗品:${usedResources
|
||||||
.map(
|
.map(
|
||||||
item =>
|
item =>
|
||||||
`${item.label} ${readResourceUsage(item.key)}/${item.quantity}${isChargedResource(item) ? `,金额¥${money(resourceLineAmount(item))}` : ',赠送不扣款'}`
|
`${item.label} ${readResourceUsage(item.key)}/${item.quantity}${
|
||||||
|
isChargedResource(item) ? `,金额¥${money(resourceLineAmount(item))}` : ',赠送不扣款'
|
||||||
|
}`
|
||||||
)
|
)
|
||||||
.join(';')}`
|
.join(';')}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (Number(checkoutForm.value.coin_consumed_m || 0) > 0) {
|
if (Number(checkoutForm.value.coin_consumed_m || 0) > 0) {
|
||||||
lines.push(
|
lines.push(
|
||||||
`哈夫币消耗:${quantity(Number(checkoutForm.value.coin_consumed_m))}M,预计剩余${quantity(remainingHafCoinM.value)}M`
|
`哈夫币消耗:${quantity(Number(checkoutForm.value.coin_consumed_m))}M,预计剩余${quantity(
|
||||||
|
remainingHafCoinM.value
|
||||||
|
)}M`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
@@ -617,7 +623,9 @@ function ownerActualIncome(item: Order) {
|
|||||||
if (item.owner_id !== session.userId) return null
|
if (item.owner_id !== session.userId) return null
|
||||||
const value = item.checkout?.owner_income_amount_cent
|
const value = item.checkout?.owner_income_amount_cent
|
||||||
if (typeof value === 'number') return centToYuan(value)
|
if (typeof value === 'number') return centToYuan(value)
|
||||||
return typeof item.checkout?.owner_income_amount === 'number' ? item.checkout.owner_income_amount : null
|
return typeof item.checkout?.owner_income_amount === 'number'
|
||||||
|
? item.checkout.owner_income_amount
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
function quantity(value: unknown) {
|
function quantity(value: unknown) {
|
||||||
@@ -637,10 +645,16 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|||||||
function hydrateCounterForm() {
|
function hydrateCounterForm() {
|
||||||
if (!order.value?.checkout) return
|
if (!order.value?.checkout) return
|
||||||
const checkout = order.value.checkout
|
const checkout = order.value.checkout
|
||||||
counterForm.value.consumable_amount = amountYuan(checkout.consumable_amount_cent, checkout.consumable_amount)
|
counterForm.value.consumable_amount = amountYuan(
|
||||||
|
checkout.consumable_amount_cent,
|
||||||
|
checkout.consumable_amount
|
||||||
|
)
|
||||||
counterForm.value.coin_consumed_m = checkout.coin_consumed_m
|
counterForm.value.coin_consumed_m = checkout.coin_consumed_m
|
||||||
counterForm.value.other_amount = amountYuan(checkout.other_amount_cent, checkout.other_amount)
|
counterForm.value.other_amount = amountYuan(checkout.other_amount_cent, checkout.other_amount)
|
||||||
counterForm.value.deposit_deduct_amount = amountYuan(checkout.deposit_deduct_amount_cent, checkout.deposit_deduct_amount)
|
counterForm.value.deposit_deduct_amount = amountYuan(
|
||||||
|
checkout.deposit_deduct_amount_cent,
|
||||||
|
checkout.deposit_deduct_amount
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function linesToList(value: string) {
|
function linesToList(value: string) {
|
||||||
@@ -774,9 +788,15 @@ async function copyListingCode() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="metric-card">
|
<div class="metric-card">
|
||||||
<span class="metric-label">押金</span>
|
<span class="metric-label">押金</span>
|
||||||
<strong class="metric-value">¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}</strong>
|
<strong class="metric-value"
|
||||||
<span v-if="amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0" class="metric-note"
|
>¥{{ money(amountYuan(order.deposit_amount_cent, order.deposit_amount)) }}</strong
|
||||||
>已免押 ¥{{ money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) }}</span
|
>
|
||||||
|
<span
|
||||||
|
v-if="amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0"
|
||||||
|
class="metric-note"
|
||||||
|
>已免押 ¥{{
|
||||||
|
money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount))
|
||||||
|
}}</span
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -958,32 +978,83 @@ async function copyListingCode() {
|
|||||||
<div class="checkout-summary-card">
|
<div class="checkout-summary-card">
|
||||||
<div class="summary-row">
|
<div class="summary-row">
|
||||||
<span>实际结算租金</span>
|
<span>实际结算租金</span>
|
||||||
<strong>¥{{ money(amountYuan(order.checkout.display_amount_cent, order.checkout.display_amount)) }}</strong>
|
<strong
|
||||||
|
>¥{{
|
||||||
|
money(
|
||||||
|
amountYuan(order.checkout.display_amount_cent, order.checkout.display_amount)
|
||||||
|
)
|
||||||
|
}}</strong
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-row">
|
<div class="summary-row">
|
||||||
<span>预收押金</span>
|
<span>预收押金</span>
|
||||||
<strong>
|
<strong>
|
||||||
¥{{ money(amountYuan(order.checkout.deposit_amount_cent, order.checkout.deposit_amount)) }}
|
¥{{
|
||||||
<em v-if="amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0"
|
money(
|
||||||
>已免押 ¥{{ money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount)) }}</em
|
amountYuan(order.checkout.deposit_amount_cent, order.checkout.deposit_amount)
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
<em
|
||||||
|
v-if="
|
||||||
|
amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount) > 0
|
||||||
|
"
|
||||||
|
>已免押 ¥{{
|
||||||
|
money(amountYuan(order.deposit_waived_amount_cent, order.deposit_waived_amount))
|
||||||
|
}}</em
|
||||||
>
|
>
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-row">
|
<div class="summary-row">
|
||||||
<span>额外消耗品已用</span>
|
<span>额外消耗品已用</span>
|
||||||
<strong class="warning">¥{{ money(amountYuan(order.checkout.consumable_amount_cent, order.checkout.consumable_amount)) }}</strong>
|
<strong class="warning"
|
||||||
|
>¥{{
|
||||||
|
money(
|
||||||
|
amountYuan(
|
||||||
|
order.checkout.consumable_amount_cent,
|
||||||
|
order.checkout.consumable_amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}</strong
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="summary-row">
|
<div class="summary-row">
|
||||||
<span>押金赔付扣除</span>
|
<span>押金赔付扣除</span>
|
||||||
<strong class="warning">¥{{ money(amountYuan(order.checkout.deposit_deduct_amount_cent, order.checkout.deposit_deduct_amount)) }}</strong>
|
<strong class="warning"
|
||||||
|
>¥{{
|
||||||
|
money(
|
||||||
|
amountYuan(
|
||||||
|
order.checkout.deposit_deduct_amount_cent,
|
||||||
|
order.checkout.deposit_deduct_amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}</strong
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isRenter" class="summary-row highlight">
|
<div v-if="isRenter" class="summary-row highlight">
|
||||||
<span>退还租客(未使用租金 + 剩余押金)</span>
|
<span>退还租客(未使用租金 + 剩余押金)</span>
|
||||||
<strong class="amount">¥{{ money(amountYuan(order.checkout.renter_refund_amount_cent, order.checkout.renter_refund_amount)) }}</strong>
|
<strong class="amount"
|
||||||
|
>¥{{
|
||||||
|
money(
|
||||||
|
amountYuan(
|
||||||
|
order.checkout.renter_refund_amount_cent,
|
||||||
|
order.checkout.renter_refund_amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}</strong
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="isOwner" class="summary-row highlight">
|
<div v-if="isOwner" class="summary-row highlight">
|
||||||
<span>号主最终收入(租金 + 押金赔付)</span>
|
<span>号主最终收入(租金 + 押金赔付)</span>
|
||||||
<strong class="amount">¥{{ money(amountYuan(order.checkout.owner_income_amount_cent, order.checkout.owner_income_amount)) }}</strong>
|
<strong class="amount"
|
||||||
|
>¥{{
|
||||||
|
money(
|
||||||
|
amountYuan(
|
||||||
|
order.checkout.owner_income_amount_cent,
|
||||||
|
order.checkout.owner_income_amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}}</strong
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="order.checkout.content" class="summary-note">
|
<div v-if="order.checkout.content" class="summary-note">
|
||||||
<label>说明:</label>
|
<label>说明:</label>
|
||||||
|
|||||||
+15
-71
@@ -5,7 +5,6 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||||||
COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.prod.yml"
|
COMPOSE_FILE="${ROOT_DIR}/deploy/docker-compose.prod.yml"
|
||||||
BACKEND_ENV="${ROOT_DIR}/backend/.env"
|
BACKEND_ENV="${ROOT_DIR}/backend/.env"
|
||||||
BACKEND_LOG_DIR="${ROOT_DIR}/backend/logs"
|
BACKEND_LOG_DIR="${ROOT_DIR}/backend/logs"
|
||||||
MIGRATIONS_DIR="${ROOT_DIR}/backend/migrations"
|
|
||||||
HEALTH_URL="${PROD_HEALTH_URL:-http://127.0.0.1:7890/api/health}"
|
HEALTH_URL="${PROD_HEALTH_URL:-http://127.0.0.1:7890/api/health}"
|
||||||
PUBLIC_URL="${PROD_PUBLIC_URL:-http://hfb.221329.cc.cd:7890}"
|
PUBLIC_URL="${PROD_PUBLIC_URL:-http://hfb.221329.cc.cd:7890}"
|
||||||
READY_TIMEOUT="${PROD_READY_TIMEOUT:-120}"
|
READY_TIMEOUT="${PROD_READY_TIMEOUT:-120}"
|
||||||
@@ -62,7 +61,7 @@ show_help() {
|
|||||||
选项:
|
选项:
|
||||||
--no-build 不重新构建镜像,仅启动已有镜像
|
--no-build 不重新构建镜像,仅启动已有镜像
|
||||||
--no-migrate 不执行数据库迁移
|
--no-migrate 不执行数据库迁移
|
||||||
--reset-db 重置数据库(删除并重建,自动清理旧迁移记录)
|
--reset-db 重置数据库(删除并重建)
|
||||||
--logs 部署完成后跟随查看后端日志
|
--logs 部署完成后跟随查看后端日志
|
||||||
-h, --help 显示帮助信息
|
-h, --help 显示帮助信息
|
||||||
|
|
||||||
@@ -196,55 +195,24 @@ wait_service_healthy() {
|
|||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
mysql_exec() {
|
|
||||||
compose exec -T mysql sh -c 'MYSQL_PWD="$MYSQL_PASSWORD" mysql -u"$MYSQL_USER" "$@"' sh "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
mysql_root_exec() {
|
mysql_root_exec() {
|
||||||
compose exec -T mysql sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot "$@"' sh "$@"
|
compose exec -T mysql sh -c 'MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysql -uroot "$@"' sh "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
mysql_scalar() {
|
goose_dsn() {
|
||||||
mysql_exec -N -s "$(require_env MYSQL_DATABASE)" -e "$1"
|
local dsn="$1"
|
||||||
}
|
if [[ "${dsn}" != *"multiStatements="* ]]; then
|
||||||
|
if [[ "${dsn}" == *"?"* ]]; then
|
||||||
ensure_migration_table() {
|
dsn="${dsn}&multiStatements=true"
|
||||||
mysql_exec "$(require_env MYSQL_DATABASE)" <<'SQL'
|
else
|
||||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
dsn="${dsn}?multiStatements=true"
|
||||||
version VARCHAR(255) PRIMARY KEY,
|
fi
|
||||||
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
SQL
|
|
||||||
}
|
|
||||||
|
|
||||||
clean_legacy_migrations() {
|
|
||||||
local has_legacy
|
|
||||||
has_legacy="$(mysql_scalar "SELECT COUNT(*) FROM schema_migrations WHERE version IN ('000002_payment_refund_schema', '000003_add_indexes', '000004_add_support_status', '000005_add_announcements', '000006_insert_sample_announcements', '000007_add_announcement_permissions', '000008_fix_announcement_charset', '000002_add_withdrawal_tables', '000003_add_encrypted_realname_fields');")"
|
|
||||||
|
|
||||||
if [[ "${has_legacy}" != "0" ]]; then
|
|
||||||
log_warn "检测到旧的迁移记录,正在清理..."
|
|
||||||
mysql_exec "$(require_env MYSQL_DATABASE)" -e \
|
|
||||||
"DELETE FROM schema_migrations WHERE version IN ('000002_payment_refund_schema', '000003_add_indexes', '000004_add_support_status', '000005_add_announcements', '000006_insert_sample_announcements', '000007_add_announcement_permissions', '000008_fix_announcement_charset', '000002_add_withdrawal_tables', '000003_add_encrypted_realname_fields');"
|
|
||||||
log_success "已清理旧迁移记录"
|
|
||||||
fi
|
fi
|
||||||
|
printf "%s" "${dsn}"
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap_existing_schema_history() {
|
goose_cmd() {
|
||||||
local migration_count users_exists
|
compose run --rm --no-deps backend /app/goose -dir /app/migrations mysql "$(goose_dsn "$(require_env MYSQL_DSN)")" "$@"
|
||||||
migration_count="$(mysql_scalar "SELECT COUNT(*) FROM schema_migrations;")"
|
|
||||||
users_exists="$(mysql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'users';")"
|
|
||||||
|
|
||||||
if [[ "${migration_count}" == "0" && "${users_exists}" != "0" ]]; then
|
|
||||||
log_warn "检测到已有表结构但没有迁移记录,补记当前所有迁移文件为已应用"
|
|
||||||
local file version
|
|
||||||
shopt -s nullglob
|
|
||||||
for file in "${MIGRATIONS_DIR}"/*.sql; do
|
|
||||||
version="$(basename "${file}" .sql)"
|
|
||||||
mysql_exec "$(require_env MYSQL_DATABASE)" -e \
|
|
||||||
"INSERT IGNORE INTO schema_migrations (version) VALUES ('${version}');"
|
|
||||||
done
|
|
||||||
shopt -u nullglob
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
reset_database() {
|
reset_database() {
|
||||||
@@ -259,7 +227,7 @@ reset_database() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_warn "重置数据库 ${database}(会清理旧迁移记录)..."
|
log_warn "重置数据库 ${database}..."
|
||||||
mysql_root_exec -e \
|
mysql_root_exec -e \
|
||||||
"DROP DATABASE IF EXISTS \`${database}\`; CREATE DATABASE \`${database}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
"DROP DATABASE IF EXISTS \`${database}\`; CREATE DATABASE \`${database}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
|
||||||
log_success "数据库已重建"
|
log_success "数据库已重建"
|
||||||
@@ -271,32 +239,8 @@ run_migrations() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "执行数据库迁移..."
|
log "使用 goose 执行数据库迁移..."
|
||||||
ensure_migration_table
|
goose_cmd up
|
||||||
bootstrap_existing_schema_history
|
|
||||||
|
|
||||||
# 只有在数据库未重置的情况下才需要清理旧迁移记录
|
|
||||||
if [[ "${RESET_DB}" != "1" ]]; then
|
|
||||||
clean_legacy_migrations
|
|
||||||
fi
|
|
||||||
|
|
||||||
local file version applied
|
|
||||||
shopt -s nullglob
|
|
||||||
for file in "${MIGRATIONS_DIR}"/*.sql; do
|
|
||||||
version="$(basename "${file}" .sql)"
|
|
||||||
applied="$(mysql_scalar "SELECT COUNT(*) FROM schema_migrations WHERE version = '${version}';")"
|
|
||||||
if [[ "${applied}" != "0" ]]; then
|
|
||||||
log "跳过已应用迁移:${version}"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "导入迁移:${version}"
|
|
||||||
mysql_exec "$(require_env MYSQL_DATABASE)" < "${file}"
|
|
||||||
mysql_exec "$(require_env MYSQL_DATABASE)" -e \
|
|
||||||
"INSERT INTO schema_migrations (version) VALUES ('${version}');"
|
|
||||||
done
|
|
||||||
shopt -u nullglob
|
|
||||||
|
|
||||||
log_success "数据库迁移完成"
|
log_success "数据库迁移完成"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+37
-73
@@ -19,6 +19,8 @@ FRONTEND_HEALTH_URL="${DEV_FRONTEND_HEALTH_URL:-http://127.0.0.1:${FRONTEND_PORT
|
|||||||
HTTP_READY_TIMEOUT="${DEV_HTTP_READY_TIMEOUT:-120}"
|
HTTP_READY_TIMEOUT="${DEV_HTTP_READY_TIMEOUT:-120}"
|
||||||
AUTO_KILL_PORTS="${DEV_AUTO_KILL_PORTS:-1}"
|
AUTO_KILL_PORTS="${DEV_AUTO_KILL_PORTS:-1}"
|
||||||
FORCE_NPM_CI="${DEV_FORCE_NPM_CI:-0}"
|
FORCE_NPM_CI="${DEV_FORCE_NPM_CI:-0}"
|
||||||
|
GOOSE_VERSION="${GOOSE_VERSION:-v3.27.1}"
|
||||||
|
GOOSE_BIN="${GOOSE_BIN:-}"
|
||||||
|
|
||||||
BACKEND_PID=""
|
BACKEND_PID=""
|
||||||
FRONTEND_PID=""
|
FRONTEND_PID=""
|
||||||
@@ -59,7 +61,7 @@ show_help() {
|
|||||||
用法: ./scripts/dev.sh [选项]
|
用法: ./scripts/dev.sh [选项]
|
||||||
|
|
||||||
选项:
|
选项:
|
||||||
--reset-db 重置数据库(删除并重建,自动清理旧迁移记录)
|
--reset-db 重置数据库(删除并重建)
|
||||||
--seed-only 仅重新同步基础数据
|
--seed-only 仅重新同步基础数据
|
||||||
--no-migrate 不执行数据库迁移
|
--no-migrate 不执行数据库迁移
|
||||||
--no-frontend 不启动前端
|
--no-frontend 不启动前端
|
||||||
@@ -378,14 +380,6 @@ mysql_root() {
|
|||||||
docker exec -i -e MYSQL_PWD=rootsecret hfb-mysql mysql -uroot "$@"
|
docker exec -i -e MYSQL_PWD=rootsecret hfb-mysql mysql -uroot "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
mysql_hfb() {
|
|
||||||
docker exec -i -e MYSQL_PWD=secret hfb-mysql mysql -uhfb "$@"
|
|
||||||
}
|
|
||||||
|
|
||||||
mysql_scalar() {
|
|
||||||
mysql_hfb --default-character-set=utf8mb4 -N -s -e "$1" "${MYSQL_DATABASE}"
|
|
||||||
}
|
|
||||||
|
|
||||||
validate_database_name() {
|
validate_database_name() {
|
||||||
if [[ ! "${MYSQL_DATABASE}" =~ ^[A-Za-z0-9_]+$ ]]; then
|
if [[ ! "${MYSQL_DATABASE}" =~ ^[A-Za-z0-9_]+$ ]]; then
|
||||||
log_error "DEV_MYSQL_DATABASE 只能包含字母、数字和下划线,当前值:${MYSQL_DATABASE}"
|
log_error "DEV_MYSQL_DATABASE 只能包含字母、数字和下划线,当前值:${MYSQL_DATABASE}"
|
||||||
@@ -404,43 +398,42 @@ reset_database() {
|
|||||||
log_success "数据库已重建"
|
log_success "数据库已重建"
|
||||||
}
|
}
|
||||||
|
|
||||||
ensure_migration_table() {
|
ensure_goose() {
|
||||||
mysql_hfb --default-character-set=utf8mb4 "${MYSQL_DATABASE}" <<'SQL'
|
if [[ -z "${GOOSE_BIN}" ]]; then
|
||||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
GOOSE_BIN="$(command -v goose || true)"
|
||||||
version VARCHAR(255) PRIMARY KEY,
|
fi
|
||||||
applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
||||||
SQL
|
|
||||||
}
|
|
||||||
|
|
||||||
clean_legacy_migrations() {
|
if [[ -z "${GOOSE_BIN}" ]]; then
|
||||||
local has_legacy
|
need_cmd go
|
||||||
has_legacy="$(mysql_scalar "SELECT COUNT(*) FROM schema_migrations WHERE version IN ('000002_payment_refund_schema', '000003_add_indexes', '000004_add_support_status', '000005_add_announcements', '000006_insert_sample_announcements', '000007_add_announcement_permissions', '000008_fix_announcement_charset', '000002_add_withdrawal_tables', '000003_add_encrypted_realname_fields');")"
|
log "未找到 goose,正在安装 ${GOOSE_VERSION}..."
|
||||||
|
(cd "${ROOT_DIR}/backend" && go install "github.com/pressly/goose/v3/cmd/goose@${GOOSE_VERSION}")
|
||||||
|
GOOSE_BIN="$(go env GOPATH)/bin/goose"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "${has_legacy}" != "0" ]]; then
|
if [[ ! -x "${GOOSE_BIN}" ]]; then
|
||||||
log_warn "检测到旧的迁移记录,正在清理..."
|
log_error "goose 不可执行:${GOOSE_BIN}"
|
||||||
mysql_hfb --default-character-set=utf8mb4 -e \
|
exit 1
|
||||||
"DELETE FROM schema_migrations WHERE version IN ('000002_payment_refund_schema', '000003_add_indexes', '000004_add_support_status', '000005_add_announcements', '000006_insert_sample_announcements', '000007_add_announcement_permissions', '000008_fix_announcement_charset', '000002_add_withdrawal_tables', '000003_add_encrypted_realname_fields');" "${MYSQL_DATABASE}"
|
|
||||||
log_success "已清理旧迁移记录"
|
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
bootstrap_existing_schema_history() {
|
goose_dsn() {
|
||||||
local migration_count users_exists
|
local dsn="$1"
|
||||||
migration_count="$(mysql_scalar "SELECT COUNT(*) FROM schema_migrations;")"
|
if [[ "${dsn}" != *"multiStatements="* ]]; then
|
||||||
users_exists="$(mysql_scalar "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'users';")"
|
if [[ "${dsn}" == *"?"* ]]; then
|
||||||
|
dsn="${dsn}&multiStatements=true"
|
||||||
if [[ "${migration_count}" == "0" && "${users_exists}" != "0" ]]; then
|
else
|
||||||
log_warn "检测到已有表结构但没有迁移记录,补记当前所有迁移文件为已应用"
|
dsn="${dsn}?multiStatements=true"
|
||||||
local file version
|
fi
|
||||||
shopt -s nullglob
|
|
||||||
for file in "${MIGRATIONS_DIR}"/*.sql; do
|
|
||||||
version="$(basename "${file}" .sql)"
|
|
||||||
mysql_hfb --default-character-set=utf8mb4 -e \
|
|
||||||
"INSERT IGNORE INTO schema_migrations (version) VALUES ('${version}');" "${MYSQL_DATABASE}"
|
|
||||||
done
|
|
||||||
shopt -u nullglob
|
|
||||||
fi
|
fi
|
||||||
|
printf "%s" "${dsn}"
|
||||||
|
}
|
||||||
|
|
||||||
|
dev_goose_dsn() {
|
||||||
|
goose_dsn "hfb:secret@tcp(127.0.0.1:3306)/${MYSQL_DATABASE}?charset=utf8mb4&parseTime=True&loc=Local"
|
||||||
|
}
|
||||||
|
|
||||||
|
goose_cmd() {
|
||||||
|
"${GOOSE_BIN}" -dir "${MIGRATIONS_DIR}" mysql "$(dev_goose_dsn)" "$@"
|
||||||
}
|
}
|
||||||
|
|
||||||
run_migrations() {
|
run_migrations() {
|
||||||
@@ -449,39 +442,10 @@ run_migrations() {
|
|||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log "自动发现并执行数据库迁移..."
|
ensure_goose
|
||||||
ensure_migration_table
|
log "使用 goose 执行数据库迁移..."
|
||||||
bootstrap_existing_schema_history
|
goose_cmd up
|
||||||
|
log_success "数据库迁移完成"
|
||||||
# 只有在数据库未重置的情况下才需要清理旧迁移记录
|
|
||||||
if [[ "${RESET_DB}" != "1" ]]; then
|
|
||||||
clean_legacy_migrations
|
|
||||||
fi
|
|
||||||
|
|
||||||
local file version applied
|
|
||||||
local found=0
|
|
||||||
shopt -s nullglob
|
|
||||||
for file in "${MIGRATIONS_DIR}"/*.sql; do
|
|
||||||
found=1
|
|
||||||
version="$(basename "${file}" .sql)"
|
|
||||||
applied="$(mysql_scalar "SELECT COUNT(*) FROM schema_migrations WHERE version = '${version}';")"
|
|
||||||
if [[ "${applied}" != "0" ]]; then
|
|
||||||
log "跳过已应用迁移:${version}"
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "执行迁移:${version}"
|
|
||||||
mysql_hfb --default-character-set=utf8mb4 "${MYSQL_DATABASE}" < "${file}"
|
|
||||||
mysql_hfb --default-character-set=utf8mb4 -e \
|
|
||||||
"INSERT INTO schema_migrations (version) VALUES ('${version}');" "${MYSQL_DATABASE}"
|
|
||||||
done
|
|
||||||
shopt -u nullglob
|
|
||||||
|
|
||||||
if [[ "${found}" == "0" ]]; then
|
|
||||||
log_warn "未发现迁移文件:${MIGRATIONS_DIR}/*.sql"
|
|
||||||
else
|
|
||||||
log_success "数据库迁移完成"
|
|
||||||
fi
|
|
||||||
}
|
}
|
||||||
|
|
||||||
load_env_file() {
|
load_env_file() {
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# 前端金额字段批量替换脚本
|
|
||||||
|
|
||||||
echo "开始批量替换前端金额字段..."
|
|
||||||
|
|
||||||
# 进入前端目录
|
|
||||||
cd /Users/yml/codes/hfb_sys/frontend/src
|
|
||||||
|
|
||||||
# 1. 替换 API 类型定义中的字段名
|
|
||||||
echo "1. 替换 API 类型定义..."
|
|
||||||
|
|
||||||
# Wallet 相关
|
|
||||||
find . -name "*.ts" -type f -exec sed -i '' 's/available_balance:/available_balance_cent:/g' {} +
|
|
||||||
find . -name "*.ts" -type f -exec sed -i '' 's/frozen_balance:/frozen_balance_cent:/g' {} +
|
|
||||||
|
|
||||||
# Ledger 相关
|
|
||||||
find . -name "*.ts" -type f -exec sed -i '' 's/\bamount:/amount_cent:/g' {} +
|
|
||||||
find . -name "*.ts" -type f -exec sed -i '' 's/balance_after:/balance_after_cent:/g' {} +
|
|
||||||
|
|
||||||
# Order 相关
|
|
||||||
find . -name "*.ts" -type f -exec sed -i '' 's/display_amount:/display_amount_cent:/g' {} +
|
|
||||||
find . -name "*.ts" -type f -exec sed -i '' 's/rent_amount:/rent_amount_cent:/g' {} +
|
|
||||||
find . -name "*.ts" -type f -exec sed -i '' 's/owner_rent_amount:/owner_rent_amount_cent:/g' {} +
|
|
||||||
find . -name "*.ts" -type f -exec sed -i '' 's/deposit_amount:/deposit_amount_cent:/g' {} +
|
|
||||||
find . -name "*.ts" -type f -exec sed -i '' 's/platform_fee:/platform_fee_cent:/g' {} +
|
|
||||||
|
|
||||||
# Listing 相关
|
|
||||||
find . -name "*.ts" -type f -exec sed -i '' 's/\bprice:/price_cent:/g' {} +
|
|
||||||
|
|
||||||
# 2. 替换 Vue 模板中的字段名
|
|
||||||
echo "2. 替换 Vue 模板中的字段..."
|
|
||||||
|
|
||||||
find . -name "*.vue" -type f -exec sed -i '' 's/\.available_balance/.available_balance_cent/g' +
|
|
||||||
find . -name "*.vue" -type f -exec sed -i '' 's/\.frozen_balance/.frozen_balance_cent/g' {} +
|
|
||||||
find . -name "*.vue" -type f -exec sed -i '' 's/\.amount/.amount_cent/g' {} +
|
|
||||||
find . -name "*.vue" -type f -exec sed -i '' 's/\.balance_after/.balance_after_cent/g' {} +
|
|
||||||
find . -name "*.vue" -type f -exec sed -i '' 's/\.price/.price_cent/g' {} +
|
|
||||||
find . -name "*.vue" -type f -exec sed -i '' 's/\.deposit_amount/.deposit_amount_cent/g' {} +
|
|
||||||
|
|
||||||
# 3. 替换函数调用
|
|
||||||
echo "3. 替换函数调用..."
|
|
||||||
|
|
||||||
find . -name "*.vue" -type f -exec sed -i '' 's/formatMoney(/formatCent(/g' {} +
|
|
||||||
find . -name "*.vue" -type f -exec sed -i '' 's/formatMoneyWithSymbol(/formatCentWithSymbol(/g' {} +
|
|
||||||
|
|
||||||
echo "批量替换完成!"
|
|
||||||
echo "请手动检查并修复可能的错误替换。"
|
|
||||||
Reference in New Issue
Block a user