统一金额分制重构
This commit is contained in:
@@ -35,9 +35,9 @@ type CreateRequest struct {
|
||||
}
|
||||
|
||||
type ArbitrateRequest struct {
|
||||
Result string `json:"result" binding:"required"`
|
||||
Remark string `json:"remark" binding:"required"`
|
||||
Amount float64 `json:"amount"`
|
||||
Result string `json:"result" binding:"required"`
|
||||
Remark string `json:"remark" binding:"required"`
|
||||
AmountCent int64 `json:"amount_cent"`
|
||||
}
|
||||
type AuditMeta = auditlog.Meta
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
@@ -249,9 +248,8 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
|
||||
if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil {
|
||||
return err
|
||||
}
|
||||
if settlement.RenterRefundAmount > 0 {
|
||||
refundCent := int64(math.Round(settlement.RenterRefundAmount * 100))
|
||||
action, err := r.prepareRefund(&order, refundCent, "arbitration_refund", "仲裁退款原路退还")
|
||||
if settlement.RenterRefundAmountCent > 0 {
|
||||
action, err := r.prepareRefund(&order, settlement.RenterRefundAmountCent, "arbitration_refund", "仲裁退款原路退还")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -281,25 +279,25 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
|
||||
}
|
||||
disputeID := row.ID
|
||||
if err := appendAuditLog(tx, adminID, "dispute.arbitrate", "dispute", row.ID, meta, map[string]any{
|
||||
"dispute_id": row.ID,
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"result": req.Result,
|
||||
"remark": req.Remark,
|
||||
"input_amount": req.Amount,
|
||||
"renter_refund_amount": settlement.RenterRefundAmount,
|
||||
"owner_income_amount": settlement.OwnerIncomeAmount,
|
||||
"deposit_deduct_amount": settlement.DepositDeductAmount,
|
||||
"before_order_status": beforeOrderStatus,
|
||||
"after_order_status": order.Status,
|
||||
"before_handoff_status": beforeHandoffStatus,
|
||||
"after_handoff_status": order.HandoffStatus,
|
||||
"before_settlement_status": beforeSettlementStatus,
|
||||
"after_settlement_status": order.SettlementStatus,
|
||||
"before_listing_status": beforeListingStatus,
|
||||
"after_listing_status": listing.Status,
|
||||
"before_account_status": beforeAccountStatus,
|
||||
"after_account_status": account.Status,
|
||||
"dispute_id": row.ID,
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"result": req.Result,
|
||||
"remark": req.Remark,
|
||||
"input_amount_cent": req.AmountCent,
|
||||
"renter_refund_amount_cent": settlement.RenterRefundAmountCent,
|
||||
"owner_income_amount_cent": settlement.OwnerIncomeAmountCent,
|
||||
"deposit_deduct_amount_cent": settlement.DepositDeductAmountCent,
|
||||
"before_order_status": beforeOrderStatus,
|
||||
"after_order_status": order.Status,
|
||||
"before_handoff_status": beforeHandoffStatus,
|
||||
"after_handoff_status": order.HandoffStatus,
|
||||
"before_settlement_status": beforeSettlementStatus,
|
||||
"after_settlement_status": order.SettlementStatus,
|
||||
"before_listing_status": beforeListingStatus,
|
||||
"after_listing_status": listing.Status,
|
||||
"before_account_status": beforeAccountStatus,
|
||||
"after_account_status": account.Status,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -338,36 +336,29 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest,
|
||||
}
|
||||
|
||||
type arbitrationSettlement struct {
|
||||
Entries []wallet.Entry
|
||||
RenterRefundAmount float64
|
||||
OwnerIncomeAmount float64
|
||||
DepositDeductAmount float64
|
||||
Entries []wallet.Entry
|
||||
RenterRefundAmountCent int64
|
||||
OwnerIncomeAmountCent int64
|
||||
DepositDeductAmountCent int64
|
||||
}
|
||||
|
||||
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalance float64) (arbitrationSettlement, error) {
|
||||
rentAmount := float64(order.RentAmountCent) / 100
|
||||
if rentAmount <= 0 {
|
||||
rentAmount = order.RentAmount
|
||||
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, error) {
|
||||
rentAmountCent := order.RentAmountCent
|
||||
depositAmountCent := order.DepositAmountCent
|
||||
ownerRentAmountCent := order.OwnerRentAmountCent
|
||||
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
|
||||
ownerRentAmountCent = rentAmountCent
|
||||
}
|
||||
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)
|
||||
totalCent := rentAmountCent + depositAmountCent
|
||||
settlement := arbitrationSettlement{}
|
||||
orderID := order.ID
|
||||
releaseFrozenAmount := minMoney(total, roundMoney(renterFrozenBalance))
|
||||
if releaseFrozenAmount > 0 {
|
||||
releaseFrozenAmountCent := money.MinCent(totalCent, renterFrozenBalanceCent)
|
||||
if releaseFrozenAmountCent > 0 {
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
Direction: "out",
|
||||
AmountCent: int64(math.Round(releaseFrozenAmount * 100)),
|
||||
AmountCent: releaseFrozenAmountCent,
|
||||
BalanceType: "frozen",
|
||||
BizType: "arbitration_release_frozen",
|
||||
BizNo: order.OrderNo,
|
||||
@@ -375,22 +366,22 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
|
||||
})
|
||||
}
|
||||
|
||||
addRenterRefund := func(amount float64, remark string) {
|
||||
if amount <= 0 {
|
||||
addRenterRefund := func(amountCent int64, remark string) {
|
||||
if amountCent <= 0 {
|
||||
return
|
||||
}
|
||||
settlement.RenterRefundAmount += amount
|
||||
settlement.RenterRefundAmountCent += amountCent
|
||||
}
|
||||
addOwnerIncome := func(amount float64, remark string) {
|
||||
if amount <= 0 {
|
||||
addOwnerIncome := func(amountCent int64, remark string) {
|
||||
if amountCent <= 0 {
|
||||
return
|
||||
}
|
||||
settlement.OwnerIncomeAmount += amount
|
||||
settlement.OwnerIncomeAmountCent += amountCent
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
AmountCent: int64(math.Round(amount * 100)),
|
||||
AmountCent: amountCent,
|
||||
BalanceType: "available",
|
||||
BizType: "arbitration_owner_income",
|
||||
BizNo: order.OrderNo,
|
||||
@@ -400,28 +391,27 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
|
||||
|
||||
switch req.Result {
|
||||
case "full_refund":
|
||||
addRenterRefund(total, "仲裁全额退款")
|
||||
addRenterRefund(totalCent, "仲裁全额退款")
|
||||
case "partial_refund":
|
||||
req.Amount = roundMoney(req.Amount)
|
||||
if req.Amount <= 0 || req.Amount > total {
|
||||
if req.AmountCent <= 0 || req.AmountCent > totalCent {
|
||||
return settlement, ErrInvalidDispute
|
||||
}
|
||||
addRenterRefund(req.Amount, "仲裁部分退款")
|
||||
addOwnerIncome(minMoney(total-req.Amount, ownerRentAmount+depositAmount), "仲裁剩余金额结算给号主")
|
||||
addRenterRefund(req.AmountCent, "仲裁部分退款")
|
||||
addOwnerIncome(money.MinCent(totalCent-req.AmountCent, ownerRentAmountCent+depositAmountCent), "仲裁剩余金额结算给号主")
|
||||
case "release_deposit":
|
||||
addOwnerIncome(ownerRentAmount, "仲裁确认订单金额结算给号主")
|
||||
addRenterRefund(depositAmount, "仲裁释放押金给租客")
|
||||
addOwnerIncome(ownerRentAmountCent, "仲裁确认订单金额结算给号主")
|
||||
addRenterRefund(depositAmountCent, "仲裁释放押金给租客")
|
||||
case "deduct_deposit", "compensate_owner":
|
||||
deductAmount := roundMoney(req.Amount)
|
||||
if deductAmount <= 0 {
|
||||
deductAmount = depositAmount
|
||||
deductAmountCent := req.AmountCent
|
||||
if deductAmountCent <= 0 {
|
||||
deductAmountCent = depositAmountCent
|
||||
}
|
||||
if deductAmount > depositAmount {
|
||||
if deductAmountCent > depositAmountCent {
|
||||
return settlement, ErrInvalidDispute
|
||||
}
|
||||
settlement.DepositDeductAmount = deductAmount
|
||||
addOwnerIncome(ownerRentAmount+deductAmount, "仲裁订单金额及押金赔付结算给号主")
|
||||
addRenterRefund(depositAmount-deductAmount, "仲裁退回剩余押金给租客")
|
||||
settlement.DepositDeductAmountCent = deductAmountCent
|
||||
addOwnerIncome(ownerRentAmountCent+deductAmountCent, "仲裁订单金额及押金赔付结算给号主")
|
||||
addRenterRefund(depositAmountCent-deductAmountCent, "仲裁退回剩余押金给租客")
|
||||
case "order_close":
|
||||
// Only release frozen funds. No available-balance settlement happens in development mode.
|
||||
case "mark_abnormal":
|
||||
@@ -457,7 +447,7 @@ func (r *Repository) startRefundBestEffort(action *refundAction) {
|
||||
_, _ = r.refundFunc(action.OrderID, action.RefundAmountCent, action.BizType, action.Remark)
|
||||
}
|
||||
|
||||
func renterFrozenBalance(tx *gorm.DB, renterID uint64) (float64, error) {
|
||||
func renterFrozenBalance(tx *gorm.DB, renterID uint64) (int64, error) {
|
||||
var account model.WalletAccount
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("user_id = ?", renterID).
|
||||
@@ -468,17 +458,7 @@ func renterFrozenBalance(tx *gorm.DB, renterID uint64) (float64, error) {
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return account.FrozenBalance, nil
|
||||
}
|
||||
|
||||
// minMoney 返回较小金额(角精度)
|
||||
func minMoney(a float64, b float64) float64 {
|
||||
return money.Min(a, b)
|
||||
}
|
||||
|
||||
// roundMoney 使用统一的角精度(0.1元)
|
||||
func roundMoney(value float64) float64 {
|
||||
return money.Round(value)
|
||||
return account.FrozenBalanceCent, nil
|
||||
}
|
||||
|
||||
func (r *Repository) baseQuery() *gorm.DB {
|
||||
@@ -550,14 +530,14 @@ func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitration
|
||||
if req.Remark != "" {
|
||||
content += "\n处理说明:" + req.Remark
|
||||
}
|
||||
if settlement.RenterRefundAmount > 0 {
|
||||
content += fmt.Sprintf("\n退款给租客:¥%.2f", settlement.RenterRefundAmount)
|
||||
if settlement.RenterRefundAmountCent > 0 {
|
||||
content += "\n退款给租客:" + money.FormatWithSymbol(settlement.RenterRefundAmountCent)
|
||||
}
|
||||
if settlement.OwnerIncomeAmount > 0 {
|
||||
content += fmt.Sprintf("\n结算给号主:¥%.2f", settlement.OwnerIncomeAmount)
|
||||
if settlement.OwnerIncomeAmountCent > 0 {
|
||||
content += "\n结算给号主:" + money.FormatWithSymbol(settlement.OwnerIncomeAmountCent)
|
||||
}
|
||||
if settlement.DepositDeductAmount > 0 {
|
||||
content += fmt.Sprintf("\n押金扣除:¥%.2f", settlement.DepositDeductAmount)
|
||||
if settlement.DepositDeductAmountCent > 0 {
|
||||
content += "\n押金扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -31,11 +31,11 @@ func TestBuildArbitrationSettlementSkipsFrozenReleaseWhenNoFrozenBalance(t *test
|
||||
t.Fatalf("不应在无冻结余额时生成解冻流水: %+v", entry)
|
||||
}
|
||||
}
|
||||
if settlement.OwnerIncomeAmount != 180 {
|
||||
t.Fatalf("OwnerIncomeAmount = %.1f, want 180.0", settlement.OwnerIncomeAmount)
|
||||
if settlement.OwnerIncomeAmountCent != 18000 {
|
||||
t.Fatalf("OwnerIncomeAmountCent = %d, want 18000", settlement.OwnerIncomeAmountCent)
|
||||
}
|
||||
if settlement.RenterRefundAmount != 100 {
|
||||
t.Fatalf("RenterRefundAmount = %.1f, want 100.0", settlement.RenterRefundAmount)
|
||||
if settlement.RenterRefundAmountCent != 10000 {
|
||||
t.Fatalf("RenterRefundAmountCent = %d, want 10000", settlement.RenterRefundAmountCent)
|
||||
}
|
||||
for _, entry := range settlement.Entries {
|
||||
if entry.UserID == order.RenterID && entry.BizType == "arbitration_renter_refund" {
|
||||
@@ -58,7 +58,7 @@ func TestBuildArbitrationSettlementLimitsFrozenReleaseToExistingBalance(t *testi
|
||||
settlement, err := buildArbitrationSettlement(order, ArbitrateRequest{
|
||||
Result: "order_close",
|
||||
Remark: "测试裁决",
|
||||
}, 120)
|
||||
}, 12000)
|
||||
if err != nil {
|
||||
t.Fatalf("buildArbitrationSettlement() error = %v", err)
|
||||
}
|
||||
@@ -83,15 +83,15 @@ func TestBuildArbitrationHandoffContent(t *testing.T) {
|
||||
Result: "partial_refund",
|
||||
Remark: "账号异常,退还部分租金。",
|
||||
}, arbitrationSettlement{
|
||||
RenterRefundAmount: 80,
|
||||
OwnerIncomeAmount: 120,
|
||||
RenterRefundAmountCent: 8000,
|
||||
OwnerIncomeAmountCent: 12000,
|
||||
})
|
||||
|
||||
wantParts := []string{
|
||||
"客服仲裁结果:部分退款",
|
||||
"处理说明:账号异常,退还部分租金。",
|
||||
"退款给租客:¥80.00",
|
||||
"结算给号主:¥120.00",
|
||||
"退款给租客:¥80.0",
|
||||
"结算给号主:¥120.0",
|
||||
}
|
||||
for _, part := range wantParts {
|
||||
if !strings.Contains(content, part) {
|
||||
|
||||
Reference in New Issue
Block a user