From 62b898d7cc91d9e8931a58d62fe53bda219631e3 Mon Sep 17 00:00:00 2001 From: yml Date: Tue, 9 Jun 2026 17:04:40 +0800 Subject: [PATCH] [Snow Team] backend-fixer: auto-commit work --- backend/internal/modules/adminuser/dto.go | 3 +- .../internal/modules/adminuser/repository.go | 23 +- .../internal/modules/dispute/repository.go | 27 ++- .../modules/dispute/repository_test.go | 32 +-- .../internal/modules/listing/repository.go | 52 +++-- .../internal/modules/listing/service_test.go | 12 +- backend/internal/modules/order/repository.go | 51 +++-- .../internal/modules/order/repository_test.go | 74 +++---- .../modules/wallet/repository_test.go | 24 +-- .../internal/modules/withdrawal/repository.go | 3 + .../20260609052734_add_amount_cent_fields.sql | 204 ++++++++++-------- 11 files changed, 291 insertions(+), 214 deletions(-) diff --git a/backend/internal/modules/adminuser/dto.go b/backend/internal/modules/adminuser/dto.go index 6f533fa..034392a 100644 --- a/backend/internal/modules/adminuser/dto.go +++ b/backend/internal/modules/adminuser/dto.go @@ -25,7 +25,8 @@ type FreezeRequest struct { } type DepositFreeQuotaRequest struct { - Amount float64 `json:"amount"` + AmountCent int64 `json:"amount_cent"` + Amount float64 `json:"amount"` } type PaginatedResult struct { diff --git a/backend/internal/modules/adminuser/repository.go b/backend/internal/modules/adminuser/repository.go index c21284f..804e0bb 100644 --- a/backend/internal/modules/adminuser/repository.go +++ b/backend/internal/modules/adminuser/repository.go @@ -2,6 +2,7 @@ package adminuser import ( "errors" + "math" "hfb_sys/backend/internal/auditlog" "hfb_sys/backend/internal/model" @@ -60,24 +61,29 @@ func (r *Repository) Unfreeze(adminID uint64, userID uint64, meta AuditMeta) (*U } func (r *Repository) SetDepositFreeQuota(adminID uint64, userID uint64, req DepositFreeQuotaRequest, meta AuditMeta) (*UserDTO, error) { - amount := roundMoney(req.Amount) - if amount < 0 { + amountCent := depositFreeQuotaAmountCent(req) + if amountCent < 0 { return nil, ErrInvalidUser } + amount := float64(amountCent) / 100 err := r.db.Transaction(func(tx *gorm.DB) error { var user model.User if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil { return err } beforeAmount := user.DepositFreeQuota + beforeAmountCent := user.DepositFreeQuotaCent user.DepositFreeQuota = amount + user.DepositFreeQuotaCent = amountCent if err := tx.Save(&user).Error; err != nil { return err } return appendAuditLog(tx, adminID, "admin_user.set_deposit_free_quota", user.ID, meta, map[string]any{ - "user_id": user.ID, - "before_amount": beforeAmount, - "after_amount": amount, + "user_id": user.ID, + "before_amount": beforeAmount, + "after_amount": amount, + "before_amount_cent": beforeAmountCent, + "after_amount_cent": amountCent, }) }) if err != nil { @@ -172,6 +178,13 @@ func roundMoney(value float64) float64 { return money.Round(value) } +func depositFreeQuotaAmountCent(req DepositFreeQuotaRequest) int64 { + if req.AmountCent != 0 { + return req.AmountCent + } + return int64(math.Round(req.Amount * 100)) +} + func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizID uint64, meta AuditMeta, detail map[string]any) error { return auditlog.Append(tx, auditlog.Entry{ ActorType: "admin", diff --git a/backend/internal/modules/dispute/repository.go b/backend/internal/modules/dispute/repository.go index 8f81ea1..c64c6fc 100644 --- a/backend/internal/modules/dispute/repository.go +++ b/backend/internal/modules/dispute/repository.go @@ -345,11 +345,20 @@ type arbitrationSettlement struct { } func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalance float64) (arbitrationSettlement, error) { - total := roundMoney(order.RentAmount + order.DepositAmount) - ownerRentAmount := roundMoney(order.OwnerRentAmount) - if ownerRentAmount <= 0 || ownerRentAmount > order.RentAmount { - ownerRentAmount = roundMoney(order.RentAmount) + rentAmount := float64(order.RentAmountCent) / 100 + if rentAmount <= 0 { + rentAmount = order.RentAmount } + depositAmount := float64(order.DepositAmountCent) / 100 + if depositAmount <= 0 { + depositAmount = order.DepositAmount + } + ownerRentAmount := float64(order.OwnerRentAmountCent) / 100 + if ownerRentAmount <= 0 || ownerRentAmount > rentAmount { + ownerRentAmount = rentAmount + } + total := roundMoney(rentAmount + depositAmount) + ownerRentAmount = roundMoney(ownerRentAmount) settlement := arbitrationSettlement{} orderID := order.ID releaseFrozenAmount := minMoney(total, roundMoney(renterFrozenBalance)) @@ -398,21 +407,21 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r return settlement, ErrInvalidDispute } addRenterRefund(req.Amount, "仲裁部分退款") - addOwnerIncome(minMoney(total-req.Amount, ownerRentAmount+order.DepositAmount), "仲裁剩余金额结算给号主") + addOwnerIncome(minMoney(total-req.Amount, ownerRentAmount+depositAmount), "仲裁剩余金额结算给号主") case "release_deposit": addOwnerIncome(ownerRentAmount, "仲裁确认订单金额结算给号主") - addRenterRefund(order.DepositAmount, "仲裁释放押金给租客") + addRenterRefund(depositAmount, "仲裁释放押金给租客") case "deduct_deposit", "compensate_owner": deductAmount := roundMoney(req.Amount) if deductAmount <= 0 { - deductAmount = order.DepositAmount + deductAmount = depositAmount } - if deductAmount > order.DepositAmount { + if deductAmount > depositAmount { return settlement, ErrInvalidDispute } settlement.DepositDeductAmount = deductAmount addOwnerIncome(ownerRentAmount+deductAmount, "仲裁订单金额及押金赔付结算给号主") - addRenterRefund(order.DepositAmount-deductAmount, "仲裁退回剩余押金给租客") + addRenterRefund(depositAmount-deductAmount, "仲裁退回剩余押金给租客") case "order_close": // Only release frozen funds. No available-balance settlement happens in development mode. case "mark_abnormal": diff --git a/backend/internal/modules/dispute/repository_test.go b/backend/internal/modules/dispute/repository_test.go index 5b12613..94b73ef 100644 --- a/backend/internal/modules/dispute/repository_test.go +++ b/backend/internal/modules/dispute/repository_test.go @@ -9,13 +9,13 @@ import ( func TestBuildArbitrationSettlementSkipsFrozenReleaseWhenNoFrozenBalance(t *testing.T) { order := model.RentalOrder{ - ID: 11, - OrderNo: "RO202606080001", - RenterID: 101, - OwnerID: 202, - RentAmount: 200, - OwnerRentAmount: 180, - DepositAmount: 100, + ID: 11, + OrderNo: "RO202606080001", + RenterID: 101, + OwnerID: 202, + RentAmountCent: 20000, + OwnerRentAmountCent: 18000, + DepositAmountCent: 10000, } settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{ @@ -46,13 +46,13 @@ func TestBuildArbitrationSettlementSkipsFrozenReleaseWhenNoFrozenBalance(t *test func TestBuildArbitrationSettlementLimitsFrozenReleaseToExistingBalance(t *testing.T) { order := model.RentalOrder{ - ID: 12, - OrderNo: "RO202606080002", - RenterID: 101, - OwnerID: 202, - RentAmount: 200, - OwnerRentAmount: 180, - DepositAmount: 100, + ID: 12, + OrderNo: "RO202606080002", + RenterID: 101, + OwnerID: 202, + RentAmountCent: 20000, + OwnerRentAmountCent: 18000, + DepositAmountCent: 10000, } settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{ @@ -69,8 +69,8 @@ func TestBuildArbitrationSettlementLimitsFrozenReleaseToExistingBalance(t *testi continue } releaseEntryCount++ - if entry.Amount != 120 { - t.Fatalf("release frozen amount = %.1f, want 120.0", entry.Amount) + if entry.AmountCent != 12000 { + t.Fatalf("release frozen amount cent = %d, want 12000", entry.AmountCent) } } if releaseEntryCount != 1 { diff --git a/backend/internal/modules/listing/repository.go b/backend/internal/modules/listing/repository.go index 08a795d..2fe9914 100644 --- a/backend/internal/modules/listing/repository.go +++ b/backend/internal/modules/listing/repository.go @@ -82,14 +82,16 @@ func (r *Repository) Create(ownerID uint64, req CreateRequest, reviewRequired bo price := normalizedListingPrice(req) depositAmount := roundMoney(req.DepositAmount) listing := model.RentalListing{ - ListingNo: listingNo, - AccountID: account.ID, - OwnerID: ownerID, - Price: price, - DepositAmount: depositAmount, - Status: listingStatus, - ReviewStatus: reviewStatus, - PublishedAt: publishedAt, + ListingNo: listingNo, + AccountID: account.ID, + OwnerID: ownerID, + Price: price, + PriceCent: int64(math.Round(price * 100)), + DepositAmount: depositAmount, + DepositAmountCent: int64(math.Round(depositAmount * 100)), + Status: listingStatus, + ReviewStatus: reviewStatus, + PublishedAt: publishedAt, } if err := tx.Create(&listing).Error; err != nil { return err @@ -147,14 +149,18 @@ func (r *Repository) CreateFromExternalUpload(upload externalUploadCreate, req C if err := tx.Create(&account).Error; err != nil { return err } + price := normalizedListingPrice(req) + depositAmount := roundMoney(req.DepositAmount) listing := model.RentalListing{ - ListingNo: listingNo, - AccountID: account.ID, - OwnerID: owner.ID, - Price: normalizedListingPrice(req), - DepositAmount: roundMoney(req.DepositAmount), - Status: "draft", - ReviewStatus: "pending", + ListingNo: listingNo, + AccountID: account.ID, + OwnerID: owner.ID, + Price: price, + PriceCent: int64(math.Round(price * 100)), + DepositAmount: depositAmount, + DepositAmountCent: int64(math.Round(depositAmount * 100)), + Status: "draft", + ReviewStatus: "pending", } if err := tx.Create(&listing).Error; err != nil { return err @@ -1439,14 +1445,14 @@ func (row listingRow) toDTO() ListingDTO { screenshotURLS := cleanScreenshotURLs(decodeScreenshots(row.ScreenshotURLS)) reviewStatus, reviewReason := normalizedReviewState(row.Status, row.ReviewStatus, row.ReviewReason) return ListingDTO{ - ID: row.ID, - ListingNo: row.ListingNo, - AccountID: row.AccountID, - OwnerID: row.OwnerID, - OwnerPhone: row.OwnerPhone, - OwnerNickname: row.OwnerNickname, - Title: row.Title, - Description: row.Description, + ID: row.ID, + ListingNo: row.ListingNo, + AccountID: row.AccountID, + OwnerID: row.OwnerID, + OwnerPhone: row.OwnerPhone, + OwnerNickname: row.OwnerNickname, + Title: row.Title, + Description: row.Description, GameName: row.GameName, ServerRegion: row.ServerRegion, LoginPlatform: row.LoginPlatform, diff --git a/backend/internal/modules/listing/service_test.go b/backend/internal/modules/listing/service_test.go index f7c28cf..0355ae9 100644 --- a/backend/internal/modules/listing/service_test.go +++ b/backend/internal/modules/listing/service_test.go @@ -69,7 +69,7 @@ func TestCreateRequiresPublishAgreements(t *testing.T) { func TestApplySellerListingPriceUsesSellerTotalPrice(t *testing.T) { item := &ListingDTO{ - Price: 238, + PriceCent: 23800, AssetSummary: map[string]any{ "price_breakdown": map[string]any{ "seller_total_price": 200, @@ -79,14 +79,14 @@ func TestApplySellerListingPriceUsesSellerTotalPrice(t *testing.T) { applySellerListingPrice(item) - if item.Price != 200 { - t.Fatalf("expected seller price 200, got %.2f", item.Price) + if item.PriceCent != 20000 { + t.Fatalf("expected seller price 20000, got %d", item.PriceCent) } } func TestApplySellerListingPriceKeepsFallbackPrice(t *testing.T) { item := &ListingDTO{ - Price: 238, + PriceCent: 23800, AssetSummary: map[string]any{ "price_breakdown": map[string]any{ "seller_total_price": 0, @@ -96,8 +96,8 @@ func TestApplySellerListingPriceKeepsFallbackPrice(t *testing.T) { applySellerListingPrice(item) - if item.Price != 238 { - t.Fatalf("expected fallback price 238, got %.2f", item.Price) + if item.PriceCent != 23800 { + t.Fatalf("expected fallback price 23800, got %d", item.PriceCent) } } diff --git a/backend/internal/modules/order/repository.go b/backend/internal/modules/order/repository.go index 25d9eee..4cf48f1 100644 --- a/backend/internal/modules/order/repository.go +++ b/backend/internal/modules/order/repository.go @@ -185,11 +185,17 @@ func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, erro OwnerID: listing.OwnerID, RenterID: renterID, EstimatedDurationHours: rentHours, + RentAmount: float64(pricing.RentAmountCent) / 100, RentAmountCent: pricing.RentAmountCent, + OwnerRentAmount: float64(pricing.OwnerRentAmountCent) / 100, OwnerRentAmountCent: pricing.OwnerRentAmountCent, + DepositAmount: paidDepositAmount, DepositAmountCent: int64(math.Round(paidDepositAmount * 100)), + DepositOriginalAmount: depositOriginalAmount, DepositOriginalAmountCent: int64(math.Round(depositOriginalAmount * 100)), + DepositWaivedAmount: waivedDepositAmount, DepositWaivedAmountCent: int64(math.Round(waivedDepositAmount * 100)), + PlatformFee: float64(pricing.PlatformFeeCent) / 100, PlatformFeeCent: pricing.PlatformFeeCent, AccountSnapshot: snapshot, Status: "pending_payment", @@ -690,10 +696,10 @@ func (r *Repository) CounterCheckout(userID uint64, orderID uint64, req CounterC return nil, err } dto := toCheckoutDTOForUser(*checkout, userID, model.RentalOrder{ - OwnerID: userID, - RentAmountCent: int64(math.Round(checkout.RentAmount * 100)), + OwnerID: userID, + RentAmountCent: int64(math.Round(checkout.RentAmount * 100)), OwnerRentAmountCent: int64(math.Round(checkout.OwnerRentAmount * 100)), - DepositAmountCent: int64(math.Round(checkout.DepositAmount * 100)), + DepositAmountCent: int64(math.Round(checkout.DepositAmount * 100)), }) return &dto, nil } @@ -1244,21 +1250,30 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c return model.OrderCheckout{}, err } return model.OrderCheckout{ - OrderID: order.ID, - InitiatedBy: initiatedBy, - Status: status, - RentAmount: float64(settlement.ActualRentAmountCent) / 100, - OwnerRentAmount: float64(settlement.OwnerRentIncomeCent) / 100, - PlatformFee: float64(settlement.PlatformFeeCent) / 100, - DepositAmount: depositAmountFromCent, - ConsumableAmount: consumableAmount, - CoinConsumedM: roundQuantity(coinConsumedM), - OtherAmount: otherAmount, - DepositDeductAmount: roundMoney(deductAmount), - RenterRefundAmount: float64(settlement.RenterRefundCent) / 100, - OwnerIncomeAmount: float64(settlement.OwnerIncomeCent) / 100, - Content: content, - EvidenceURLS: evidence, + OrderID: order.ID, + InitiatedBy: initiatedBy, + Status: status, + RentAmount: float64(settlement.ActualRentAmountCent) / 100, + RentAmountCent: settlement.ActualRentAmountCent, + OwnerRentAmount: float64(settlement.OwnerRentIncomeCent) / 100, + OwnerRentAmountCent: settlement.OwnerRentIncomeCent, + PlatformFee: float64(settlement.PlatformFeeCent) / 100, + PlatformFeeCent: settlement.PlatformFeeCent, + DepositAmount: depositAmountFromCent, + DepositAmountCent: order.DepositAmountCent, + ConsumableAmount: consumableAmount, + ConsumableAmountCent: int64(math.Round(consumableAmount * 100)), + CoinConsumedM: roundQuantity(coinConsumedM), + OtherAmount: otherAmount, + OtherAmountCent: int64(math.Round(otherAmount * 100)), + DepositDeductAmount: roundMoney(deductAmount), + DepositDeductAmountCent: settlement.DepositCompensationCent, + RenterRefundAmount: float64(settlement.RenterRefundCent) / 100, + RenterRefundAmountCent: settlement.RenterRefundCent, + OwnerIncomeAmount: float64(settlement.OwnerIncomeCent) / 100, + OwnerIncomeAmountCent: settlement.OwnerIncomeCent, + Content: content, + EvidenceURLS: evidence, }, nil } diff --git a/backend/internal/modules/order/repository_test.go b/backend/internal/modules/order/repository_test.go index 6106ab7..06589d1 100644 --- a/backend/internal/modules/order/repository_test.go +++ b/backend/internal/modules/order/repository_test.go @@ -12,9 +12,9 @@ import ( func TestCalculateCheckoutSettlementRefundsUnusedRent(t *testing.T) { order := model.RentalOrder{ - RentAmount: 383, - OwnerRentAmount: 353, - DepositAmount: 150, + RentAmountCent: 38300, + OwnerRentAmountCent: 35300, + DepositAmountCent: 15000, AccountSnapshot: datatypes.JSON([]byte(`{ "haf_coin_amount": 100000000, "asset_summary": { @@ -30,35 +30,35 @@ func TestCalculateCheckoutSettlementRefundsUnusedRent(t *testing.T) { settlement := calculateCheckoutSettlement(order, 7, 90, 0) // 角精度:243.7 = roundMoney(236.7 + 7) - if settlement.ActualRentAmount != 243.7 { - t.Fatalf("ActualRentAmount = %.1f, want 243.7", settlement.ActualRentAmount) + if float64(settlement.ActualRentAmountCent)/100 != 243.7 { + t.Fatalf("ActualRentAmount = %.1f, want 243.7", float64(settlement.ActualRentAmountCent)/100) } // 角精度:216.7 = roundMoney(209.7 + 7) 卖家币价+消耗品 - if settlement.OwnerRentIncome != 216.7 { - t.Fatalf("OwnerRentIncome = %.1f, want 216.7", settlement.OwnerRentIncome) + if float64(settlement.OwnerRentIncomeCent)/100 != 216.7 { + t.Fatalf("OwnerRentIncome = %.1f, want 216.7", float64(settlement.OwnerRentIncomeCent)/100) } // 角精度:27.0 = roundMoney(27) 平台费 - if settlement.PlatformFee != 27.0 { - t.Fatalf("PlatformFee = %.1f, want 27.0", settlement.PlatformFee) + if float64(settlement.PlatformFeeCent)/100 != 27.0 { + t.Fatalf("PlatformFee = %.1f, want 27.0", float64(settlement.PlatformFeeCent)/100) } // 角精度:139.3 = roundMoney(383 - 243.7) - if settlement.RentRefund != 139.3 { - t.Fatalf("RentRefund = %.1f, want 139.3", settlement.RentRefund) + if float64(settlement.RentRefundCent)/100 != 139.3 { + t.Fatalf("RentRefund = %.1f, want 139.3", float64(settlement.RentRefundCent)/100) } - if settlement.DepositRefund != 150 { - t.Fatalf("DepositRefund = %.1f, want 150.0", settlement.DepositRefund) + if float64(settlement.DepositRefundCent)/100 != 150 { + t.Fatalf("DepositRefund = %.1f, want 150.0", float64(settlement.DepositRefundCent)/100) } // 角精度:289.3 = roundMoney(139.3 + 150) - if settlement.RenterRefund != 289.3 { - t.Fatalf("RenterRefund = %.1f, want 289.3", settlement.RenterRefund) + if float64(settlement.RenterRefundCent)/100 != 289.3 { + t.Fatalf("RenterRefund = %.1f, want 289.3", float64(settlement.RenterRefundCent)/100) } } func TestCalculateCheckoutSettlementUsesBuyerAndSellerRatiosSeparately(t *testing.T) { order := model.RentalOrder{ - RentAmount: 383, - OwnerRentAmount: 353, - DepositAmount: 150, + RentAmountCent: 38300, + OwnerRentAmountCent: 35300, + DepositAmountCent: 15000, AccountSnapshot: datatypes.JSON([]byte(`{ "haf_coin_amount": 100000000, "asset_summary": { @@ -74,24 +74,24 @@ func TestCalculateCheckoutSettlementUsesBuyerAndSellerRatiosSeparately(t *testin settlement := calculateCheckoutSettlement(order, 0, 50, 0) // 角精度:131.5 = roundMoney(131.5) - if settlement.ActualRentAmount != 131.5 { - t.Fatalf("租客侧实际租金 = %.1f, want 131.5", settlement.ActualRentAmount) + if float64(settlement.ActualRentAmountCent)/100 != 131.5 { + t.Fatalf("租客侧实际租金 = %.1f, want 131.5", float64(settlement.ActualRentAmountCent)/100) } // 角精度:116.5 = roundMoney(116.5) - if settlement.OwnerRentIncome != 116.5 { - t.Fatalf("卖家侧租金收入 = %.1f, want 116.5", settlement.OwnerRentIncome) + if float64(settlement.OwnerRentIncomeCent)/100 != 116.5 { + t.Fatalf("卖家侧租金收入 = %.1f, want 116.5", float64(settlement.OwnerRentIncomeCent)/100) } // 角精度:15.0 = roundMoney(15) - if settlement.PlatformFee != 15.0 { - t.Fatalf("平台差价 = %.1f, want 15.0", settlement.PlatformFee) + if float64(settlement.PlatformFeeCent)/100 != 15.0 { + t.Fatalf("平台差价 = %.1f, want 15.0", float64(settlement.PlatformFeeCent)/100) } } func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) { order := model.RentalOrder{ - RentAmount: 383, - OwnerRentAmount: 353, - DepositAmount: 150, + RentAmountCent: 38300, + OwnerRentAmountCent: 35300, + DepositAmountCent: 15000, AccountSnapshot: datatypes.JSON([]byte(`{ "haf_coin_amount": 100000000, "asset_summary": { @@ -106,20 +106,20 @@ func TestCalculateCheckoutSettlementAddsDepositCompensation(t *testing.T) { settlement := calculateCheckoutSettlement(order, 120, 100, 30) - if settlement.ActualRentAmount != 383 { - t.Fatalf("ActualRentAmount = %.2f, want 383.00", settlement.ActualRentAmount) + if float64(settlement.ActualRentAmountCent)/100 != 383 { + t.Fatalf("ActualRentAmount = %.2f, want 383.00", float64(settlement.ActualRentAmountCent)/100) } - if settlement.OwnerRentIncome != 353 { - t.Fatalf("OwnerRentIncome = %.2f, want 353.00", settlement.OwnerRentIncome) + if float64(settlement.OwnerRentIncomeCent)/100 != 353 { + t.Fatalf("OwnerRentIncome = %.2f, want 353.00", float64(settlement.OwnerRentIncomeCent)/100) } - if settlement.DepositCompensation != 30 { - t.Fatalf("DepositCompensation = %.2f, want 30.00", settlement.DepositCompensation) + if float64(settlement.DepositCompensationCent)/100 != 30 { + t.Fatalf("DepositCompensation = %.2f, want 30.00", float64(settlement.DepositCompensationCent)/100) } - if settlement.OwnerIncome != 383 { - t.Fatalf("OwnerIncome = %.2f, want 383.00", settlement.OwnerIncome) + if float64(settlement.OwnerIncomeCent)/100 != 383 { + t.Fatalf("OwnerIncome = %.2f, want 383.00", float64(settlement.OwnerIncomeCent)/100) } - if settlement.RenterRefund != 120 { - t.Fatalf("RenterRefund = %.2f, want 120.00", settlement.RenterRefund) + if float64(settlement.RenterRefundCent)/100 != 120 { + t.Fatalf("RenterRefund = %.2f, want 120.00", float64(settlement.RenterRefundCent)/100) } } diff --git a/backend/internal/modules/wallet/repository_test.go b/backend/internal/modules/wallet/repository_test.go index 4f4b659..eca6021 100644 --- a/backend/internal/modules/wallet/repository_test.go +++ b/backend/internal/modules/wallet/repository_test.go @@ -9,13 +9,13 @@ import ( func TestApplyEntryRoundsMoneyBeforeComparing(t *testing.T) { account := model.WalletAccount{ - UserID: 7, - FrozenBalance: 606.41, + UserID: 7, + FrozenBalanceCent: 60641, } entry := Entry{ UserID: 7, Direction: "out", - Amount: 406.41 + 200.00, + AmountCent: 60641, BalanceType: "frozen", } @@ -26,20 +26,20 @@ func TestApplyEntryRoundsMoneyBeforeComparing(t *testing.T) { if balanceAfter != 0 { t.Fatalf("balanceAfter = %v, want 0", balanceAfter) } - if account.FrozenBalance != 0 { - t.Fatalf("FrozenBalance = %v, want 0", account.FrozenBalance) + if account.FrozenBalanceCent != 0 { + t.Fatalf("FrozenBalanceCent = %d, want 0", account.FrozenBalanceCent) } } func TestApplyEntryKeepsWalletMoneyAtJiaoPrecision(t *testing.T) { account := model.WalletAccount{ - UserID: 8, - AvailableBalance: 0, + UserID: 8, + AvailableBalanceCent: 0, } entry := Entry{ UserID: 8, Direction: "in", - Amount: 2.30, + AmountCent: 230, BalanceType: "available", } @@ -47,11 +47,11 @@ func TestApplyEntryKeepsWalletMoneyAtJiaoPrecision(t *testing.T) { if err != nil { t.Fatalf("applyEntry() error = %v", err) } - if balanceAfter != 2.3 { - t.Fatalf("balanceAfter = %v, want 2.3", balanceAfter) + if balanceAfter != 230 { + t.Fatalf("balanceAfter = %d, want 230", balanceAfter) } - if account.AvailableBalance != 2.3 { - t.Fatalf("AvailableBalance = %v, want 2.3", account.AvailableBalance) + if account.AvailableBalanceCent != 230 { + t.Fatalf("AvailableBalanceCent = %d, want 230", account.AvailableBalanceCent) } if rounded := roundWalletMoney(2.36); rounded != 2.4 { diff --git a/backend/internal/modules/withdrawal/repository.go b/backend/internal/modules/withdrawal/repository.go index 49b491f..9397bf6 100644 --- a/backend/internal/modules/withdrawal/repository.go +++ b/backend/internal/modules/withdrawal/repository.go @@ -58,8 +58,11 @@ func (r *Repository) Create(userID uint64, req CreateWithdrawalRequest) (*Withdr withdrawal := model.WithdrawalRequest{ WithdrawNo: withdrawNo, UserID: userID, + Amount: float64(req.AmountCent) / 100, AmountCent: req.AmountCent, + Fee: float64(feeCent) / 100, FeeCent: feeCent, + ActualAmount: float64(actualAmountCent) / 100, ActualAmountCent: actualAmountCent, PaymentAccountID: &req.PaymentAccountID, AccountType: paymentAccount.AccountType, diff --git a/backend/migrations/20260609052734_add_amount_cent_fields.sql b/backend/migrations/20260609052734_add_amount_cent_fields.sql index 09efff2..954841b 100644 --- a/backend/migrations/20260609052734_add_amount_cent_fields.sql +++ b/backend/migrations/20260609052734_add_amount_cent_fields.sql @@ -8,49 +8,61 @@ -- 用户表:免押额度 ALTER TABLE users - ADD COLUMN deposit_free_quota_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押总额度(分)' AFTER deposit_free_quota; + ADD COLUMN IF NOT EXISTS deposit_free_quota_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押总额度(分)' AFTER deposit_free_quota; -- 商品表:价格和押金 ALTER TABLE rental_listings - ADD COLUMN price_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金(分/小时)' AFTER price, - ADD COLUMN deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金金额(分)' AFTER deposit_amount; + ADD COLUMN IF NOT EXISTS price_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金(分/小时)' AFTER price, + ADD COLUMN IF NOT EXISTS deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金金额(分)' AFTER deposit_amount; -- 订单表:租金、押金、平台费 ALTER TABLE rental_orders - ADD COLUMN rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金总额(分)' AFTER rent_amount, - ADD COLUMN owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主实得租金(分)' AFTER owner_rent_amount, - ADD COLUMN deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '实际收取押金(分)' AFTER deposit_amount, - ADD COLUMN deposit_original_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '商品原始押金(分)' AFTER deposit_original_amount, - ADD COLUMN deposit_waived_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押抵扣金额(分)' AFTER deposit_waived_amount, - ADD COLUMN platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)' AFTER platform_fee; + ADD COLUMN IF NOT EXISTS rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金总额(分)' AFTER rent_amount, + ADD COLUMN IF NOT EXISTS owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主实得租金(分)' AFTER owner_rent_amount, + ADD COLUMN IF NOT EXISTS deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '实际收取押金(分)' AFTER deposit_amount, + ADD COLUMN IF NOT EXISTS deposit_original_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '商品原始押金(分)' AFTER deposit_original_amount, + ADD COLUMN IF NOT EXISTS deposit_waived_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '免押抵扣金额(分)' AFTER deposit_waived_amount, + ADD COLUMN IF NOT EXISTS platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)' AFTER platform_fee; -- 结算记录表:所有金额字段 ALTER TABLE order_checkouts - ADD COLUMN rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金(分)' AFTER rent_amount, - ADD COLUMN owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主实得租金(分)' AFTER owner_rent_amount, - ADD COLUMN platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)' AFTER platform_fee, - ADD COLUMN deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金(分)' AFTER deposit_amount, - ADD COLUMN consumable_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '消耗品扣费(分)' AFTER consumable_amount, - ADD COLUMN other_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '其他费用(分)' AFTER other_amount, - ADD COLUMN deposit_deduct_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金扣除金额(分)' AFTER deposit_deduct_amount, - ADD COLUMN renter_refund_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租客退款金额(分)' AFTER renter_refund_amount, - ADD COLUMN owner_income_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主收入金额(分)' AFTER owner_income_amount; + ADD COLUMN IF NOT EXISTS rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租金(分)' AFTER rent_amount, + ADD COLUMN IF NOT EXISTS owner_rent_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主实得租金(分)' AFTER owner_rent_amount, + ADD COLUMN IF NOT EXISTS platform_fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '平台手续费(分)' AFTER platform_fee, + ADD COLUMN IF NOT EXISTS deposit_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金(分)' AFTER deposit_amount, + ADD COLUMN IF NOT EXISTS consumable_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '消耗品扣费(分)' AFTER consumable_amount, + ADD COLUMN IF NOT EXISTS other_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '其他费用(分)' AFTER other_amount, + ADD COLUMN IF NOT EXISTS deposit_deduct_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '押金扣除金额(分)' AFTER deposit_deduct_amount, + ADD COLUMN IF NOT EXISTS renter_refund_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '租客退款金额(分)' AFTER renter_refund_amount, + ADD COLUMN IF NOT EXISTS owner_income_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '号主收入金额(分)' AFTER owner_income_amount; -- 钱包账户表:余额 ALTER TABLE wallet_accounts - ADD COLUMN available_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '可用余额(分)' AFTER available_balance, - ADD COLUMN frozen_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '冻结余额(分)' AFTER frozen_balance; + ADD COLUMN IF NOT EXISTS available_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '可用余额(分)' AFTER available_balance, + ADD COLUMN IF NOT EXISTS frozen_balance_cent BIGINT NOT NULL DEFAULT 0 COMMENT '冻结余额(分)' AFTER frozen_balance; -- 钱包流水表:金额和余额 ALTER TABLE wallet_ledger - ADD COLUMN amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '金额(分)' AFTER amount, - ADD COLUMN balance_after_cent BIGINT NOT NULL DEFAULT 0 COMMENT '变动后余额(分)' AFTER balance_after; + ADD COLUMN IF NOT EXISTS amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '金额(分)' AFTER amount, + ADD COLUMN IF NOT EXISTS balance_after_cent BIGINT NOT NULL DEFAULT 0 COMMENT '变动后余额(分)' AFTER balance_after; --- 提现表:如果存在的话 -ALTER TABLE withdrawal_requests - ADD COLUMN amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '提现金额(分)' AFTER amount, - ADD COLUMN fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT '手续费(分)' AFTER fee, - ADD COLUMN actual_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '实际到账(分)' AFTER actual_amount; +-- 提现表是可选模块;仅在表存在时修改,避免缺表环境迁移失败。 +SET @withdrawal_requests_exists := ( + SELECT COUNT(*) + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = 'withdrawal_requests' +); +SET @sql := IF(@withdrawal_requests_exists > 0, + 'ALTER TABLE withdrawal_requests + ADD COLUMN IF NOT EXISTS amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT ''提现金额(分)'' AFTER amount, + ADD COLUMN IF NOT EXISTS fee_cent BIGINT NOT NULL DEFAULT 0 COMMENT ''手续费(分)'' AFTER fee, + ADD COLUMN IF NOT EXISTS actual_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT ''实际到账(分)'' AFTER actual_amount', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; -- ============================================ -- 数据迁移:从 DECIMAL 复制到 BIGINT(分) @@ -58,59 +70,66 @@ ALTER TABLE withdrawal_requests -- ============================================ -- 用户免押额度 -UPDATE users SET deposit_free_quota_cent = ROUND(deposit_free_quota * 100) WHERE deposit_free_quota > 0; +UPDATE users +SET deposit_free_quota_cent = ROUND(deposit_free_quota * 100) +WHERE deposit_free_quota > 0; -- 商品价格和押金 -UPDATE rental_listings SET - price_cent = ROUND(price * 100), - deposit_amount_cent = ROUND(deposit_amount * 100) +UPDATE rental_listings +SET price_cent = ROUND(price * 100), + deposit_amount_cent = ROUND(deposit_amount * 100) WHERE price > 0 OR deposit_amount > 0; -- 订单金额 -UPDATE rental_orders SET - rent_amount_cent = ROUND(rent_amount * 100), - owner_rent_amount_cent = ROUND(owner_rent_amount * 100), - deposit_amount_cent = ROUND(deposit_amount * 100), - deposit_original_amount_cent = ROUND(deposit_original_amount * 100), - deposit_waived_amount_cent = ROUND(deposit_waived_amount * 100), - platform_fee_cent = ROUND(platform_fee * 100) +UPDATE rental_orders +SET rent_amount_cent = ROUND(rent_amount * 100), + owner_rent_amount_cent = ROUND(owner_rent_amount * 100), + deposit_amount_cent = ROUND(deposit_amount * 100), + deposit_original_amount_cent = ROUND(deposit_original_amount * 100), + deposit_waived_amount_cent = ROUND(deposit_waived_amount * 100), + platform_fee_cent = ROUND(platform_fee * 100) WHERE rent_amount > 0 OR owner_rent_amount > 0 OR deposit_amount > 0 OR deposit_original_amount > 0 OR deposit_waived_amount > 0 OR platform_fee > 0; -- 结算记录 -UPDATE order_checkouts SET - rent_amount_cent = ROUND(rent_amount * 100), - owner_rent_amount_cent = ROUND(owner_rent_amount * 100), - platform_fee_cent = ROUND(platform_fee * 100), - deposit_amount_cent = ROUND(deposit_amount * 100), - consumable_amount_cent = ROUND(consumable_amount * 100), - other_amount_cent = ROUND(other_amount * 100), - deposit_deduct_amount_cent = ROUND(deposit_deduct_amount * 100), - renter_refund_amount_cent = ROUND(renter_refund_amount * 100), - owner_income_amount_cent = ROUND(owner_income_amount * 100) +UPDATE order_checkouts +SET rent_amount_cent = ROUND(rent_amount * 100), + owner_rent_amount_cent = ROUND(owner_rent_amount * 100), + platform_fee_cent = ROUND(platform_fee * 100), + deposit_amount_cent = ROUND(deposit_amount * 100), + consumable_amount_cent = ROUND(consumable_amount * 100), + other_amount_cent = ROUND(other_amount * 100), + deposit_deduct_amount_cent = ROUND(deposit_deduct_amount * 100), + renter_refund_amount_cent = ROUND(renter_refund_amount * 100), + owner_income_amount_cent = ROUND(owner_income_amount * 100) WHERE rent_amount > 0 OR owner_rent_amount > 0 OR platform_fee > 0 OR deposit_amount > 0 OR consumable_amount > 0 OR other_amount > 0 OR deposit_deduct_amount > 0 OR renter_refund_amount > 0 OR owner_income_amount > 0; -- 钱包余额 -UPDATE wallet_accounts SET - available_balance_cent = ROUND(available_balance * 100), - frozen_balance_cent = ROUND(frozen_balance * 100) +UPDATE wallet_accounts +SET available_balance_cent = ROUND(available_balance * 100), + frozen_balance_cent = ROUND(frozen_balance * 100) WHERE available_balance > 0 OR frozen_balance > 0; -- 钱包流水 -UPDATE wallet_ledger SET - amount_cent = ROUND(amount * 100), - balance_after_cent = ROUND(balance_after * 100) +UPDATE wallet_ledger +SET amount_cent = ROUND(amount * 100), + balance_after_cent = ROUND(balance_after * 100) WHERE amount != 0 OR balance_after != 0; -- 提现记录 -UPDATE withdrawal_requests SET - amount_cent = ROUND(amount * 100), - fee_cent = ROUND(fee * 100), - actual_amount_cent = ROUND(actual_amount * 100) -WHERE amount > 0 OR fee > 0 OR actual_amount > 0; - +SET @sql := IF(@withdrawal_requests_exists > 0, + 'UPDATE withdrawal_requests + SET amount_cent = ROUND(amount * 100), + fee_cent = ROUND(fee * 100), + actual_amount_cent = ROUND(actual_amount * 100) + WHERE amount > 0 OR fee > 0 OR actual_amount > 0', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; -- +goose StatementEnd -- +goose Down @@ -119,42 +138,53 @@ WHERE amount > 0 OR fee > 0 OR actual_amount > 0; -- 回滚:删除所有新增的分字段 -- ============================================ -ALTER TABLE users DROP COLUMN deposit_free_quota_cent; +ALTER TABLE users DROP COLUMN IF EXISTS deposit_free_quota_cent; ALTER TABLE rental_listings - DROP COLUMN price_cent, - DROP COLUMN deposit_amount_cent; + DROP COLUMN IF EXISTS price_cent, + DROP COLUMN IF EXISTS deposit_amount_cent; ALTER TABLE rental_orders - DROP COLUMN rent_amount_cent, - DROP COLUMN owner_rent_amount_cent, - DROP COLUMN deposit_amount_cent, - DROP COLUMN deposit_original_amount_cent, - DROP COLUMN deposit_waived_amount_cent, - DROP COLUMN platform_fee_cent; + DROP COLUMN IF EXISTS rent_amount_cent, + DROP COLUMN IF EXISTS owner_rent_amount_cent, + DROP COLUMN IF EXISTS deposit_amount_cent, + DROP COLUMN IF EXISTS deposit_original_amount_cent, + DROP COLUMN IF EXISTS deposit_waived_amount_cent, + DROP COLUMN IF EXISTS platform_fee_cent; ALTER TABLE order_checkouts - DROP COLUMN rent_amount_cent, - DROP COLUMN owner_rent_amount_cent, - DROP COLUMN platform_fee_cent, - DROP COLUMN deposit_amount_cent, - DROP COLUMN consumable_amount_cent, - DROP COLUMN other_amount_cent, - DROP COLUMN deposit_deduct_amount_cent, - DROP COLUMN renter_refund_amount_cent, - DROP COLUMN owner_income_amount_cent; + DROP COLUMN IF EXISTS rent_amount_cent, + DROP COLUMN IF EXISTS owner_rent_amount_cent, + DROP COLUMN IF EXISTS platform_fee_cent, + DROP COLUMN IF EXISTS deposit_amount_cent, + DROP COLUMN IF EXISTS consumable_amount_cent, + DROP COLUMN IF EXISTS other_amount_cent, + DROP COLUMN IF EXISTS deposit_deduct_amount_cent, + DROP COLUMN IF EXISTS renter_refund_amount_cent, + DROP COLUMN IF EXISTS owner_income_amount_cent; ALTER TABLE wallet_accounts - DROP COLUMN available_balance_cent, - DROP COLUMN frozen_balance_cent; + DROP COLUMN IF EXISTS available_balance_cent, + DROP COLUMN IF EXISTS frozen_balance_cent; ALTER TABLE wallet_ledger - DROP COLUMN amount_cent, - DROP COLUMN balance_after_cent; - -ALTER TABLE withdrawal_requests - DROP COLUMN amount_cent, - DROP COLUMN fee_cent, - DROP COLUMN actual_amount_cent; + DROP COLUMN IF EXISTS amount_cent, + DROP COLUMN IF EXISTS balance_after_cent; +SET @withdrawal_requests_exists := ( + SELECT COUNT(*) + FROM information_schema.tables + WHERE table_schema = DATABASE() + AND table_name = 'withdrawal_requests' +); +SET @sql := IF(@withdrawal_requests_exists > 0, + 'ALTER TABLE withdrawal_requests + DROP COLUMN IF EXISTS amount_cent, + DROP COLUMN IF EXISTS fee_cent, + DROP COLUMN IF EXISTS actual_amount_cent', + 'SELECT 1' +); +PREPARE stmt FROM @sql; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; -- +goose StatementEnd