优化仲裁

This commit is contained in:
yml2213
2026-07-29 18:59:07 +08:00
parent c9d0803b53
commit 8c2431c4f2
15 changed files with 387 additions and 142 deletions
+1
View File
@@ -32,6 +32,7 @@ type RentalOrder struct {
ActualCoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"actual_coin_consumed_m"`
ActualPureCoinAmountCent int64 `gorm:"not null;default:0" json:"-"`
ActualPureCoinDiscountCent int64 `gorm:"not null;default:0" json:"-"`
ActualExtraItemAmountCent int64 `gorm:"not null;default:0" json:"-"`
RenterGrowthLevel string `gorm:"size:32;not null;default:'normal'" json:"renter_growth_level"`
RenterGrowthLevelName string `gorm:"size:32;not null;default:'普通'" json:"renter_growth_level_name"`
RenterDiscountBps int `gorm:"not null;default:10000" json:"renter_discount_bps"`
+168 -48
View File
@@ -53,7 +53,7 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
if err != nil {
return err
}
settlement, err := buildArbitrationSettlement(order, req, frozenBalance)
settlement, actualSettlement, linkedCheckout, depositDeductAmountCent, err := buildArbitrationDecision(tx, row, order, req, frozenBalance)
if err != nil {
return err
}
@@ -114,23 +114,16 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
return err
}
if order.Status == "completed" {
coinConsumedM, linkedCheckout, err := arbitrationCoinConsumedM(tx, row, req)
if err != nil {
return err
if actualSettlement == nil {
return ErrInvalidDispute
}
pureCoin := ordermodule.CalculateActualPureCoinAmounts(order, coinConsumedM)
order.ActualCoinConsumedM = pureCoin.ConsumedM
order.ActualPureCoinAmountCent = pureCoin.AmountCent
order.ActualPureCoinDiscountCent = pureCoin.DiscountCent
applyArbitrationActualSettlement(&order, linkedCheckout, *actualSettlement, depositDeductAmountCent)
if linkedCheckout != nil {
linkedCheckout.PureCoinAmountCent = pureCoin.AmountCent
linkedCheckout.PureCoinDiscountCent = pureCoin.DiscountCent
linkedCheckout.PureCoinPayableCent = pureCoin.PayableCent
if err := tx.Save(linkedCheckout).Error; err != nil {
return err
}
}
if err := rentergrowth.AwardOrderCompleted(tx, &order, pureCoin.AmountCent, rentergrowth.SourceOrderCompleted); err != nil {
if err := rentergrowth.AwardOrderCompleted(tx, &order, actualSettlement.PureCoinAmountCent, rentergrowth.SourceOrderCompleted); err != nil {
return err
}
}
@@ -166,6 +159,7 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
"deposit_deduct_amount_cent": settlement.DepositDeductAmountCent,
"actual_coin_consumed_m": order.ActualCoinConsumedM,
"actual_pure_coin_cent": order.ActualPureCoinAmountCent,
"actual_extra_item_cent": order.ActualExtraItemAmountCent,
"before_order_status": beforeOrderStatus,
"after_order_status": order.Status,
"before_handoff_status": beforeHandoffStatus,
@@ -220,22 +214,131 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
return &dto, nil
}
func arbitrationCoinConsumedM(tx *gorm.DB, row model.Dispute, req ArbitrateRequest) (float64, *model.OrderCheckout, error) {
var checkout *model.OrderCheckout
if row.CheckoutID != nil && *row.CheckoutID > 0 {
var saved model.OrderCheckout
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&saved, *row.CheckoutID).Error; err != nil {
return 0, nil, err
}
checkout = &saved
func buildArbitrationDecision(tx *gorm.DB, row model.Dispute, order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, *ordermodule.ActualCheckoutSettlement, *model.OrderCheckout, int64, error) {
if arbitrationUsesActualCheckout(req.Result) {
return buildActualCheckoutArbitrationSettlement(tx, row, order, req, renterFrozenBalanceCent)
}
settlement, err := buildArbitrationSettlement(order, req, renterFrozenBalanceCent)
return settlement, nil, nil, 0, err
}
func arbitrationUsesActualCheckout(result string) bool {
switch result {
case "release_deposit", "deduct_deposit", "compensate_owner":
return true
default:
return false
}
}
func buildActualCheckoutArbitrationSettlement(tx *gorm.DB, row model.Dispute, order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, *ordermodule.ActualCheckoutSettlement, *model.OrderCheckout, int64, error) {
settlement := newArbitrationSettlement(order, renterFrozenBalanceCent)
linkedCheckout, err := lockArbitrationCheckout(tx, row)
if err != nil {
return settlement, nil, nil, 0, err
}
coinConsumedM, err := arbitrationCoinConsumedM(linkedCheckout, req)
if err != nil {
return settlement, nil, linkedCheckout, 0, err
}
consumableAmountCent, err := arbitrationConsumableAmountCent(order, linkedCheckout, req)
if err != nil {
return settlement, nil, linkedCheckout, 0, err
}
depositDeductAmountCent, err := arbitrationDepositDeductAmountCent(order, req)
if err != nil {
return settlement, nil, linkedCheckout, 0, err
}
actualSettlement := ordermodule.CalculateActualCheckoutSettlement(order, consumableAmountCent, coinConsumedM, depositDeductAmountCent)
if actualSettlement.ShortfallCent > 0 {
return settlement, nil, linkedCheckout, 0, ErrInvalidDispute
}
if actualSettlement.OwnerIncomeCent > 0 {
settlement.addOwnerIncome(order, actualSettlement.OwnerIncomeCent, "仲裁按实际消耗结算给号主")
}
if actualSettlement.RenterRefundCent > 0 {
settlement.RenterRefundAmountCent = actualSettlement.RenterRefundCent
settlement.RenterDepositRefundCent = actualSettlement.DepositRefundCent
}
settlement.DepositDeductAmountCent = depositDeductAmountCent + actualSettlement.OvershootAmountCent
settlement.ActualExtraItemAmountCent = actualSettlement.ExtraItemAmountCent
return settlement, &actualSettlement, linkedCheckout, depositDeductAmountCent, nil
}
func lockArbitrationCheckout(tx *gorm.DB, row model.Dispute) (*model.OrderCheckout, error) {
if row.CheckoutID == nil || *row.CheckoutID == 0 {
return nil, nil
}
var checkout model.OrderCheckout
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&checkout, *row.CheckoutID).Error; err != nil {
return nil, err
}
return &checkout, nil
}
func arbitrationCoinConsumedM(checkout *model.OrderCheckout, req ArbitrateRequest) (float64, error) {
if req.ActualCoinConsumedM != nil {
return *req.ActualCoinConsumedM, checkout, nil
return *req.ActualCoinConsumedM, nil
}
if checkout != nil {
return checkout.CoinConsumedM, checkout, nil
return checkout.CoinConsumedM, nil
}
return 0, nil, ErrInvalidDispute
return 0, ErrInvalidDispute
}
func arbitrationConsumableAmountCent(order model.RentalOrder, checkout *model.OrderCheckout, req ArbitrateRequest) (int64, error) {
if req.ActualConsumableAmountCent != nil {
return *req.ActualConsumableAmountCent, nil
}
if checkout != nil {
return checkout.ConsumableAmountCent, nil
}
if order.ExtraItemOriginalAmountCent <= 0 {
return 0, nil
}
return 0, ErrInvalidDispute
}
func arbitrationDepositDeductAmountCent(order model.RentalOrder, req ArbitrateRequest) (int64, error) {
switch req.Result {
case "release_deposit":
return 0, nil
case "deduct_deposit", "compensate_owner":
deductAmountCent := req.AmountCent
if deductAmountCent <= 0 {
deductAmountCent = order.DepositAmountCent
}
if deductAmountCent > order.DepositAmountCent {
return 0, ErrInvalidDispute
}
return deductAmountCent, nil
default:
return 0, ErrInvalidDispute
}
}
func applyArbitrationActualSettlement(order *model.RentalOrder, checkout *model.OrderCheckout, settlement ordermodule.ActualCheckoutSettlement, depositDeductAmountCent int64) {
order.ActualCoinConsumedM = settlement.CoinConsumedM
order.ActualPureCoinAmountCent = settlement.PureCoinAmountCent
order.ActualPureCoinDiscountCent = settlement.PureCoinDiscountCent
order.ActualExtraItemAmountCent = settlement.ExtraItemAmountCent
if checkout == nil {
return
}
checkout.RentAmountCent = settlement.ActualRentAmountCent
checkout.OwnerRentAmountCent = settlement.OwnerRentIncomeCent
checkout.PlatformFeeCent = settlement.PlatformFeeCent
checkout.PureCoinAmountCent = settlement.PureCoinAmountCent
checkout.PureCoinDiscountCent = settlement.PureCoinDiscountCent
checkout.PureCoinPayableCent = settlement.PureCoinPayableCent
checkout.ConsumableAmountCent = settlement.ExtraItemAmountCent
checkout.CoinConsumedM = settlement.CoinConsumedM
checkout.DepositDeductAmountCent = depositDeductAmountCent
checkout.OtherAmountCent = depositDeductAmountCent
checkout.RenterRefundAmountCent = settlement.RenterRefundCent
checkout.OwnerIncomeAmountCent = settlement.OwnerIncomeCent
checkout.ShortfallCent = settlement.ShortfallCent
checkout.OvershootAmountCent = settlement.OvershootAmountCent
}
func isPlatformManagedOrder(order model.RentalOrder) bool {
@@ -275,24 +378,18 @@ func applyPlatformManagedSettlement(order *model.RentalOrder, settlement *arbitr
}
type arbitrationSettlement struct {
Entries []wallet.Entry
RenterRefundAmountCent int64
RenterDepositRefundCent int64
OwnerIncomeAmountCent int64
DepositDeductAmountCent int64
Entries []wallet.Entry
RenterRefundAmountCent int64
RenterDepositRefundCent int64
OwnerIncomeAmountCent int64
DepositDeductAmountCent int64
ActualExtraItemAmountCent int64
}
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
}
totalCent := rentAmountCent + depositAmountCent
func newArbitrationSettlement(order model.RentalOrder, renterFrozenBalanceCent int64) arbitrationSettlement {
settlement := arbitrationSettlement{}
orderID := order.ID
releaseFrozenAmountCent := money.MinCent(totalCent, renterFrozenBalanceCent)
releaseFrozenAmountCent := money.MinCent(order.RentAmountCent+order.DepositAmountCent, renterFrozenBalanceCent)
if releaseFrozenAmountCent > 0 {
settlement.Entries = append(settlement.Entries, wallet.Entry{
UserID: order.RenterID,
@@ -305,6 +402,36 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
Remark: "仲裁释放冻结金额",
})
}
return settlement
}
func (settlement *arbitrationSettlement) addOwnerIncome(order model.RentalOrder, amountCent int64, remark string) {
if settlement == nil || amountCent <= 0 {
return
}
orderID := order.ID
settlement.OwnerIncomeAmountCent += amountCent
settlement.Entries = append(settlement.Entries, wallet.Entry{
UserID: order.OwnerID,
OrderID: &orderID,
Direction: "in",
AmountCent: amountCent,
BalanceType: "available",
BizType: "arbitration_owner_income",
BizNo: order.OrderNo,
Remark: remark,
})
}
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
}
totalCent := rentAmountCent + depositAmountCent
settlement := newArbitrationSettlement(order, renterFrozenBalanceCent)
addRenterRefund := func(amountCent int64, remark string) {
if amountCent <= 0 {
@@ -316,17 +443,7 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
if amountCent <= 0 {
return
}
settlement.OwnerIncomeAmountCent += amountCent
settlement.Entries = append(settlement.Entries, wallet.Entry{
UserID: order.OwnerID,
OrderID: &orderID,
Direction: "in",
AmountCent: amountCent,
BalanceType: "available",
BizType: "arbitration_owner_income",
BizNo: order.OrderNo,
Remark: remark,
})
settlement.addOwnerIncome(order, amountCent, remark)
}
switch req.Result {
@@ -503,6 +620,9 @@ func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitration
if settlement.DepositDeductAmountCent > 0 {
content += "\n押金赔付扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent)
}
if settlement.ActualExtraItemAmountCent > 0 {
content += "\n额外物品消耗:" + money.FormatWithSymbol(settlement.ActualExtraItemAmountCent)
}
return content
}
+39 -36
View File
@@ -9,38 +9,40 @@ import (
)
type DisputeDTO struct {
ID uint64 `json:"id"`
OrderID uint64 `json:"order_id"`
OrderNo string `json:"order_no"`
OrderStatus string `json:"order_status"`
HandoffStatus string `json:"handoff_status"`
SettlementStatus string `json:"settlement_status"`
ListingNo string `json:"listing_no"`
Title string `json:"title"`
OwnerID uint64 `json:"owner_id"`
RenterID uint64 `json:"renter_id"`
OwnerPhone string `json:"owner_phone"`
RenterPhone string `json:"renter_phone"`
InitiatorID uint64 `json:"initiator_id"`
InitiatorType string `json:"initiator_type"`
InitiatorAdminID *uint64 `json:"initiator_admin_id,omitempty"`
TargetUserID uint64 `json:"target_user_id"`
Type string `json:"type"`
Status string `json:"status"`
Description string `json:"description"`
EvidenceURLS datatypes.JSON `json:"evidence_urls"`
PreviousOrderStatus string `json:"previous_order_status"`
PreviousHandoffStatus string `json:"previous_handoff_status"`
PreviousSettlementStatus string `json:"previous_settlement_status"`
CheckoutID *uint64 `json:"checkout_id"`
CheckoutCoinConsumedM *float64 `json:"checkout_coin_consumed_m,omitempty"`
PreviousCheckoutStatus string `json:"previous_checkout_status"`
ArbitrationResult string `json:"arbitration_result"`
ArbitrationRemark string `json:"arbitration_remark"`
HandledBy *uint64 `json:"handled_by"`
HandledAt *time.Time `json:"handled_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID uint64 `json:"id"`
OrderID uint64 `json:"order_id"`
OrderNo string `json:"order_no"`
OrderStatus string `json:"order_status"`
HandoffStatus string `json:"handoff_status"`
SettlementStatus string `json:"settlement_status"`
ExtraItemOriginalAmountCent int64 `json:"extra_item_original_amount_cent,omitempty"`
ListingNo string `json:"listing_no"`
Title string `json:"title"`
OwnerID uint64 `json:"owner_id"`
RenterID uint64 `json:"renter_id"`
OwnerPhone string `json:"owner_phone"`
RenterPhone string `json:"renter_phone"`
InitiatorID uint64 `json:"initiator_id"`
InitiatorType string `json:"initiator_type"`
InitiatorAdminID *uint64 `json:"initiator_admin_id,omitempty"`
TargetUserID uint64 `json:"target_user_id"`
Type string `json:"type"`
Status string `json:"status"`
Description string `json:"description"`
EvidenceURLS datatypes.JSON `json:"evidence_urls"`
PreviousOrderStatus string `json:"previous_order_status"`
PreviousHandoffStatus string `json:"previous_handoff_status"`
PreviousSettlementStatus string `json:"previous_settlement_status"`
CheckoutID *uint64 `json:"checkout_id"`
CheckoutCoinConsumedM *float64 `json:"checkout_coin_consumed_m,omitempty"`
CheckoutConsumableAmountCent *int64 `json:"checkout_consumable_amount_cent,omitempty"`
PreviousCheckoutStatus string `json:"previous_checkout_status"`
ArbitrationResult string `json:"arbitration_result"`
ArbitrationRemark string `json:"arbitration_remark"`
HandledBy *uint64 `json:"handled_by"`
HandledAt *time.Time `json:"handled_at"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type CreateRequest struct {
@@ -57,10 +59,11 @@ type AdminCreateRequest struct {
}
type ArbitrateRequest struct {
Result string `json:"result" binding:"required"`
Remark string `json:"remark" binding:"required"`
AmountCent int64 `json:"amount_cent"`
ActualCoinConsumedM *float64 `json:"actual_coin_consumed_m"`
Result string `json:"result" binding:"required"`
Remark string `json:"remark" binding:"required"`
AmountCent int64 `json:"amount_cent"`
ActualCoinConsumedM *float64 `json:"actual_coin_consumed_m"`
ActualConsumableAmountCent *int64 `json:"actual_consumable_amount_cent"`
}
type AdminListQuery struct {
+47 -43
View File
@@ -10,53 +10,57 @@ import (
type disputeRow struct {
model.Dispute
OrderNo string
OrderStatus string
HandoffStatus string
SettlementStatus string
ListingNo string
Title string
OwnerID uint64
RenterID uint64
OwnerPhone string
RenterPhone string
CheckoutCoinConsumedM *float64
OrderNo string
OrderStatus string
HandoffStatus string
SettlementStatus string
ExtraItemOriginalAmountCent int64
ListingNo string
Title string
OwnerID uint64
RenterID uint64
OwnerPhone string
RenterPhone string
CheckoutCoinConsumedM *float64
CheckoutConsumableAmountCent *int64
}
func (row disputeRow) toDTO() DisputeDTO {
return DisputeDTO{
ID: row.ID,
OrderID: row.OrderID,
OrderNo: row.OrderNo,
OrderStatus: row.OrderStatus,
HandoffStatus: row.HandoffStatus,
SettlementStatus: row.SettlementStatus,
ListingNo: row.ListingNo,
Title: row.Title,
OwnerID: row.OwnerID,
RenterID: row.RenterID,
OwnerPhone: row.OwnerPhone,
RenterPhone: row.RenterPhone,
InitiatorID: row.InitiatorID,
InitiatorType: effectiveInitiatorType(row.Dispute),
InitiatorAdminID: row.InitiatorAdminID,
TargetUserID: row.TargetUserID,
Type: row.Type,
Status: row.Status,
Description: row.Description,
EvidenceURLS: row.EvidenceURLS,
PreviousOrderStatus: row.PreviousOrderStatus,
PreviousHandoffStatus: row.PreviousHandoffStatus,
PreviousSettlementStatus: row.PreviousSettlementStatus,
CheckoutID: row.CheckoutID,
CheckoutCoinConsumedM: row.CheckoutCoinConsumedM,
PreviousCheckoutStatus: row.PreviousCheckoutStatus,
ArbitrationResult: row.ArbitrationResult,
ArbitrationRemark: row.ArbitrationRemark,
HandledBy: row.HandledBy,
HandledAt: row.HandledAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
ID: row.ID,
OrderID: row.OrderID,
OrderNo: row.OrderNo,
OrderStatus: row.OrderStatus,
HandoffStatus: row.HandoffStatus,
SettlementStatus: row.SettlementStatus,
ExtraItemOriginalAmountCent: row.ExtraItemOriginalAmountCent,
ListingNo: row.ListingNo,
Title: row.Title,
OwnerID: row.OwnerID,
RenterID: row.RenterID,
OwnerPhone: row.OwnerPhone,
RenterPhone: row.RenterPhone,
InitiatorID: row.InitiatorID,
InitiatorType: effectiveInitiatorType(row.Dispute),
InitiatorAdminID: row.InitiatorAdminID,
TargetUserID: row.TargetUserID,
Type: row.Type,
Status: row.Status,
Description: row.Description,
EvidenceURLS: row.EvidenceURLS,
PreviousOrderStatus: row.PreviousOrderStatus,
PreviousHandoffStatus: row.PreviousHandoffStatus,
PreviousSettlementStatus: row.PreviousSettlementStatus,
CheckoutID: row.CheckoutID,
CheckoutCoinConsumedM: row.CheckoutCoinConsumedM,
CheckoutConsumableAmountCent: row.CheckoutConsumableAmountCent,
PreviousCheckoutStatus: row.PreviousCheckoutStatus,
ArbitrationResult: row.ArbitrationResult,
ArbitrationRemark: row.ArbitrationRemark,
HandledBy: row.HandledBy,
HandledAt: row.HandledAt,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
}
}
+4 -2
View File
@@ -91,8 +91,10 @@ func applyUserVisibleFilter(db *gorm.DB, userID uint64) *gorm.DB {
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
return r.adminFilterQuery(ctx).
Select(`d.*, o.order_no, o.status AS order_status, o.handoff_status, o.settlement_status,
o.owner_id, o.renter_id, owner.phone AS owner_phone, renter.phone AS renter_phone,
l.listing_no, a.title, c.coin_consumed_m AS checkout_coin_consumed_m`)
o.owner_id, o.renter_id, o.extra_item_original_amount_cent,
owner.phone AS owner_phone, renter.phone AS renter_phone,
l.listing_no, a.title, c.coin_consumed_m AS checkout_coin_consumed_m,
c.consumable_amount_cent AS checkout_consumable_amount_cent`)
}
func (r *Repository) adminFilterQuery(ctx context.Context) *gorm.DB {
@@ -345,10 +345,21 @@ func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) {
}
actualCoinConsumedM := 50.0
if _, err := repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{
actualConsumableAmountCent := int64(12000)
_, err = repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{
Result: "release_deposit",
Remark: "确认实际消耗 50M",
ActualCoinConsumedM: &actualCoinConsumedM,
}, AuditMeta{})
if !errors.Is(err, ErrInvalidDispute) {
t.Fatalf("缺少实际额外物品金额的仲裁错误 = %v, want ErrInvalidDispute", err)
}
if _, err := repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{
Result: "release_deposit",
Remark: "确认实际消耗 50M 和额外物品",
ActualCoinConsumedM: &actualCoinConsumedM,
ActualConsumableAmountCent: &actualConsumableAmountCent,
}, AuditMeta{}); err != nil {
t.Fatalf("带实际消耗 M 仲裁失败: %v", err)
}
@@ -356,8 +367,8 @@ func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) {
if err := db.First(&saved, order.ID).Error; err != nil {
t.Fatalf("读取订单失败: %v", err)
}
if saved.ActualCoinConsumedM != 50 || saved.ActualPureCoinAmountCent != 13150 || saved.GrowthPointsAwarded != 131 {
t.Fatalf("最终 M/纯币/积分 = %.2f/%d/%d, want 50/13150/131", saved.ActualCoinConsumedM, saved.ActualPureCoinAmountCent, saved.GrowthPointsAwarded)
if saved.ActualCoinConsumedM != 50 || saved.ActualPureCoinAmountCent != 13150 || saved.ActualExtraItemAmountCent != 12000 || saved.GrowthPointsAwarded != 131 {
t.Fatalf("最终 M/纯币/额外物品/积分 = %.2f/%d/%d/%d, want 50/13150/12000/131", saved.ActualCoinConsumedM, saved.ActualPureCoinAmountCent, saved.ActualExtraItemAmountCent, saved.GrowthPointsAwarded)
}
}
@@ -372,10 +383,11 @@ func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) {
inputOrder.SettlementStatus = "pending"
_, renter, order := createDisputeOrderFixture(t, db, inputOrder)
checkout := model.OrderCheckout{
OrderID: order.ID,
InitiatedBy: renter.ID,
Status: "submitted",
CoinConsumedM: 25,
OrderID: order.ID,
InitiatedBy: renter.ID,
Status: "submitted",
ConsumableAmountCent: 7000,
CoinConsumedM: 25,
}
if err := db.Create(&checkout).Error; err != nil {
t.Fatalf("创建结账记录失败: %v", err)
@@ -387,8 +399,8 @@ func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) {
if err != nil {
t.Fatalf("创建结账争议失败: %v", err)
}
if created.CheckoutID == nil || *created.CheckoutID != checkout.ID || created.CheckoutCoinConsumedM == nil || *created.CheckoutCoinConsumedM != 25 {
t.Fatalf("关联结账/M = %v/%v, want %d/25", created.CheckoutID, created.CheckoutCoinConsumedM, checkout.ID)
if created.CheckoutID == nil || *created.CheckoutID != checkout.ID || created.CheckoutCoinConsumedM == nil || *created.CheckoutCoinConsumedM != 25 || created.CheckoutConsumableAmountCent == nil || *created.CheckoutConsumableAmountCent != 7000 {
t.Fatalf("关联结账/M/额外物品 = %v/%v/%v, want %d/25/7000", created.CheckoutID, created.CheckoutCoinConsumedM, created.CheckoutConsumableAmountCent, checkout.ID)
}
if _, err := repo.Arbitrate(t.Context(), 77, created.ID, ArbitrateRequest{
@@ -401,8 +413,15 @@ func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) {
if err := db.First(&saved, order.ID).Error; err != nil {
t.Fatalf("读取订单失败: %v", err)
}
if saved.ActualCoinConsumedM != 25 || saved.ActualPureCoinAmountCent != 6575 || saved.GrowthPointsAwarded != 65 {
t.Fatalf("最终 M/纯币/积分 = %.2f/%d/%d, want 25/6575/65", saved.ActualCoinConsumedM, saved.ActualPureCoinAmountCent, saved.GrowthPointsAwarded)
if saved.ActualCoinConsumedM != 25 || saved.ActualPureCoinAmountCent != 6575 || saved.ActualExtraItemAmountCent != 7000 || saved.GrowthPointsAwarded != 65 {
t.Fatalf("最终 M/纯币/额外物品/积分 = %.2f/%d/%d/%d, want 25/6575/7000/65", saved.ActualCoinConsumedM, saved.ActualPureCoinAmountCent, saved.ActualExtraItemAmountCent, saved.GrowthPointsAwarded)
}
var savedCheckout model.OrderCheckout
if err := db.First(&savedCheckout, checkout.ID).Error; err != nil {
t.Fatalf("读取结账记录失败: %v", err)
}
if savedCheckout.ConsumableAmountCent != 7000 || savedCheckout.RentAmountCent != 13509 || savedCheckout.RenterRefundAmountCent != 39528 {
t.Fatalf("仲裁后结账消耗品/租金/退款 = %d/%d/%d, want 7000/13509/39528", savedCheckout.ConsumableAmountCent, savedCheckout.RentAmountCent, savedCheckout.RenterRefundAmountCent)
}
}
@@ -93,5 +93,8 @@ func (s *Service) Arbitrate(ctx context.Context, adminID uint64, id uint64, req
if req.ActualCoinConsumedM != nil && (*req.ActualCoinConsumedM < 0 || *req.ActualCoinConsumedM > 1_000_000_000) {
return nil, ErrInvalidDispute
}
if req.ActualConsumableAmountCent != nil && (*req.ActualConsumableAmountCent < 0 || *req.ActualConsumableAmountCent > 100_000_000_000) {
return nil, ErrInvalidDispute
}
return s.repo.Arbitrate(ctx, adminID, id, req, meta)
}
@@ -43,6 +43,7 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
order.ActualCoinConsumedM = settlement.CoinConsumedM
order.ActualPureCoinAmountCent = settlement.PureCoinAmountCent
order.ActualPureCoinDiscountCent = settlement.PureCoinDiscountCent
order.ActualExtraItemAmountCent = settlement.ExtraItemAmountCent
if err := appendCheckoutCompletedNotifications(tx, order, renterContent); err != nil {
return nil, err
}
+1
View File
@@ -42,6 +42,7 @@ type OrderDTO struct {
ActualCoinConsumedM float64 `json:"actual_coin_consumed_m,omitempty"`
ActualPureCoinAmountCent int64 `json:"actual_pure_coin_amount_cent,omitempty"`
ActualPureCoinDiscountCent int64 `json:"actual_pure_coin_discount_cent,omitempty"`
ActualExtraItemAmountCent int64 `json:"actual_extra_item_amount_cent,omitempty"`
RenterGrowthLevel string `json:"renter_growth_level,omitempty"`
RenterGrowthLevelName string `json:"renter_growth_level_name,omitempty"`
RenterDiscountBps int `json:"renter_discount_bps,omitempty"`
@@ -92,6 +92,7 @@ func (row orderRow) toAdminDTO() OrderDTO {
ActualCoinConsumedM: row.ActualCoinConsumedM,
ActualPureCoinAmountCent: row.ActualPureCoinAmountCent,
ActualPureCoinDiscountCent: row.ActualPureCoinDiscountCent,
ActualExtraItemAmountCent: row.ActualExtraItemAmountCent,
RenterGrowthLevel: effectiveRenterGrowthLevel(row.RentalOrder),
RenterGrowthLevelName: effectiveRenterGrowthLevelName(row.RentalOrder),
RenterDiscountBps: effectiveRenterDiscountBps(row.RentalOrder),
@@ -330,6 +331,7 @@ func applyOrderPriceView(dto *OrderDTO, order model.RentalOrder, userID uint64)
dto.ActualCoinConsumedM = 0
dto.ActualPureCoinAmountCent = 0
dto.ActualPureCoinDiscountCent = 0
dto.ActualExtraItemAmountCent = 0
dto.RenterGrowthLevel = ""
dto.RenterGrowthLevelName = ""
dto.RenterDiscountBps = 0
@@ -362,6 +364,7 @@ func applyOrderPriceView(dto *OrderDTO, order model.RentalOrder, userID uint64)
dto.ActualCoinConsumedM = 0
dto.ActualPureCoinAmountCent = 0
dto.ActualPureCoinDiscountCent = 0
dto.ActualExtraItemAmountCent = 0
dto.RenterGrowthLevel = ""
dto.RenterGrowthLevelName = ""
dto.RenterDiscountBps = 0
+45 -1
View File
@@ -50,9 +50,28 @@ type ActualPureCoinAmounts struct {
ConsumedM float64
}
// ActualCheckoutSettlement 是正常结账与仲裁共用的实际消耗结算结果。
type ActualCheckoutSettlement struct {
OwnerRentIncomeCent int64
DepositCompensationCent int64
OwnerIncomeCent int64
RentRefundCent int64
DepositRefundCent int64
RenterRefundCent int64
PlatformFeeCent int64
ActualRentAmountCent int64
PureCoinAmountCent int64
PureCoinDiscountCent int64
PureCoinPayableCent int64
ExtraItemAmountCent int64
CoinConsumedM float64
OvershootAmountCent int64
ShortfallCent int64
}
// CalculateActualPureCoinAmounts 按订单价格快照和实际消耗 M 计算纯币原价及等级优惠。
func CalculateActualPureCoinAmounts(order model.RentalOrder, coinConsumedM float64) ActualPureCoinAmounts {
settlement := calculateCheckoutSettlement(order, 0, coinConsumedM, 0)
settlement := CalculateActualCheckoutSettlement(order, 0, coinConsumedM, 0)
return ActualPureCoinAmounts{
AmountCent: settlement.PureCoinAmountCent,
DiscountCent: settlement.PureCoinDiscountCent,
@@ -61,6 +80,31 @@ func CalculateActualPureCoinAmounts(order model.RentalOrder, coinConsumedM float
}
}
// CalculateActualCheckoutSettlement 按实际纯币和额外物品消耗计算完整结算。
func CalculateActualCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) ActualCheckoutSettlement {
return toActualCheckoutSettlement(calculateCheckoutSettlement(order, consumableAmountCent, coinConsumedM, depositDeductAmountCent))
}
func toActualCheckoutSettlement(settlement checkoutSettlement) ActualCheckoutSettlement {
return ActualCheckoutSettlement{
OwnerRentIncomeCent: settlement.OwnerRentIncomeCent,
DepositCompensationCent: settlement.DepositCompensationCent,
OwnerIncomeCent: settlement.OwnerIncomeCent,
RentRefundCent: settlement.RentRefundCent,
DepositRefundCent: settlement.DepositRefundCent,
RenterRefundCent: settlement.RenterRefundCent,
PlatformFeeCent: settlement.PlatformFeeCent,
ActualRentAmountCent: settlement.ActualRentAmountCent,
PureCoinAmountCent: settlement.PureCoinAmountCent,
PureCoinDiscountCent: settlement.PureCoinDiscountCent,
PureCoinPayableCent: settlement.PureCoinPayableCent,
ExtraItemAmountCent: settlement.ExtraItemAmountCent,
CoinConsumedM: settlement.CoinConsumedM,
OvershootAmountCent: settlement.OvershootAmountCent,
ShortfallCent: settlement.ShortfallCent,
}
}
func orderDurationHours(order model.RentalOrder) int {
if order.EstimatedDurationHours > 0 {
return order.EstimatedDurationHours