649 lines
22 KiB
Go
649 lines
22 KiB
Go
package dispute
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"time"
|
|
|
|
"hfb_sys/backend/internal/listingstatus"
|
|
"hfb_sys/backend/internal/logging"
|
|
"hfb_sys/backend/internal/model"
|
|
"hfb_sys/backend/internal/modules/adminnotification"
|
|
"hfb_sys/backend/internal/modules/notification"
|
|
ordermodule "hfb_sys/backend/internal/modules/order"
|
|
"hfb_sys/backend/internal/modules/rentergrowth"
|
|
"hfb_sys/backend/internal/modules/wallet"
|
|
"hfb_sys/backend/pkg/money"
|
|
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, req ArbitrateRequest, meta AuditMeta) (*DisputeDTO, error) {
|
|
var refund *refundAction
|
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
var row model.Dispute
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, id).Error; err != nil {
|
|
return err
|
|
}
|
|
if row.Status != "open" && row.Status != "processing" {
|
|
return ErrDisputeCannotHandle
|
|
}
|
|
var order model.RentalOrder
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, row.OrderID).Error; err != nil {
|
|
return err
|
|
}
|
|
var listing model.RentalListing
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
|
return err
|
|
}
|
|
var account model.GameAccount
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
|
|
return err
|
|
}
|
|
beforeOrderStatus := order.Status
|
|
beforeHandoffStatus := order.HandoffStatus
|
|
beforeSettlementStatus := order.SettlementStatus
|
|
beforeListingStatus := listing.Status
|
|
beforeAccountStatus := account.Status
|
|
frozenBalance, err := renterFrozenBalance(tx, order.RenterID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
settlement, actualSettlement, linkedCheckout, depositDeductAmountCent, err := buildArbitrationDecision(tx, row, order, req, frozenBalance)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now()
|
|
row.Status = "resolved"
|
|
row.ArbitrationResult = req.Result
|
|
row.ArbitrationRemark = req.Remark
|
|
row.HandledBy = &adminID
|
|
row.HandledAt = &now
|
|
order.Status = arbitrateOrderStatus(req.Result)
|
|
order.SettlementStatus = "arbitrated"
|
|
order.SettledAt = &now
|
|
if isPlatformManagedOrder(order) {
|
|
order.OwnerSettledAt = nil
|
|
applyPlatformManagedSettlement(&order, &settlement)
|
|
} else {
|
|
order.OwnerSettledAt = &now
|
|
}
|
|
if order.HandoffStatus != "cancelled" && order.HandoffStatus != "returned" {
|
|
order.HandoffStatus = "arbitrated"
|
|
}
|
|
if req.Result == "mark_abnormal" {
|
|
listing.Status = "abnormal"
|
|
listing.InTransaction = false
|
|
listing.PublishedAt = nil
|
|
account.Status = "abnormal"
|
|
} else {
|
|
// 仲裁会终结当前订单,账号统一归档下架,避免已结案账号重新出现在公开列表。
|
|
listing.Status = "offline"
|
|
listing.InTransaction = false
|
|
listing.PublishedAt = nil
|
|
account.Status = "offline"
|
|
}
|
|
if err := listingstatus.AppendTransition(
|
|
tx,
|
|
&listing,
|
|
beforeListingStatus,
|
|
listingstatus.SourceDispute,
|
|
listingstatus.ActorAdmin,
|
|
adminID,
|
|
"仲裁结案",
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil {
|
|
return err
|
|
}
|
|
// 押金暂扣:若订单被暂扣,则把本次退款中的押金部分挂起不退,仅退还剩余部分。
|
|
refundAmountCent := order.ApplyDepositHold(settlement.RenterRefundAmountCent, settlement.RenterDepositRefundCent)
|
|
if refundAmountCent > 0 {
|
|
action, err := r.prepareRefund(&order, refundAmountCent, "arbitration_refund", "仲裁退款原路退还")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
refund = action
|
|
}
|
|
if err := tx.Save(&row).Error; err != nil {
|
|
return err
|
|
}
|
|
if order.Status == "completed" {
|
|
if actualSettlement == nil {
|
|
return ErrInvalidDispute
|
|
}
|
|
applyArbitrationActualSettlement(&order, linkedCheckout, *actualSettlement, depositDeductAmountCent)
|
|
if linkedCheckout != nil {
|
|
if err := tx.Save(linkedCheckout).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := rentergrowth.AwardOrderCompleted(tx, &order, actualSettlement.PureCoinAmountCent, rentergrowth.SourceOrderCompleted); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := tx.Save(&order).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Save(&listing).Error; err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Save(&account).Error; err != nil {
|
|
return err
|
|
}
|
|
handoffRecord := model.HandoffRecord{
|
|
OrderID: order.ID,
|
|
FromUserID: adminID,
|
|
ToUserID: order.RenterID,
|
|
Type: "admin_arbitration",
|
|
Content: buildArbitrationHandoffContent(req, settlement),
|
|
}
|
|
if err := tx.Create(&handoffRecord).Error; err != nil {
|
|
return err
|
|
}
|
|
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_cent": req.AmountCent,
|
|
"renter_refund_amount_cent": settlement.RenterRefundAmountCent,
|
|
"owner_income_amount_cent": settlement.OwnerIncomeAmountCent,
|
|
"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,
|
|
"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
|
|
}
|
|
renterNotification := notification.Entry{
|
|
UserID: order.RenterID,
|
|
Type: "arbitration",
|
|
Title: "申诉仲裁已完成",
|
|
Content: "客服已给出仲裁结果,请在订单和申诉记录中查看处理说明。",
|
|
BizType: "dispute",
|
|
BizID: &disputeID,
|
|
}
|
|
if isPlatformManagedOrder(order) {
|
|
if err := notification.Append(tx, renterNotification); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
if err := notification.Append(tx,
|
|
renterNotification,
|
|
notification.Entry{
|
|
UserID: order.OwnerID,
|
|
Type: "arbitration",
|
|
Title: "申诉仲裁已完成",
|
|
Content: "客服已给出仲裁结果,请在订单和申诉记录中查看处理说明。",
|
|
BizType: "dispute",
|
|
BizID: &disputeID,
|
|
},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.startRefundBestEffort(ctx, refund)
|
|
var row disputeRow
|
|
if err := r.baseQuery(ctx).Where("d.id = ?", id).First(&row).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
dto := row.toDTO()
|
|
return &dto, nil
|
|
}
|
|
|
|
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, nil
|
|
}
|
|
if checkout != nil {
|
|
return checkout.CoinConsumedM, nil
|
|
}
|
|
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 {
|
|
return order.SettlementMode == "platform_managed" || order.HandoffMode == "platform"
|
|
}
|
|
|
|
func appendPlatformManagedAdminNotification(tx *gorm.DB, order model.RentalOrder, typ string, title string, content string) error {
|
|
if order.ManagedAdminID == nil || *order.ManagedAdminID == 0 || title == "" {
|
|
return nil
|
|
}
|
|
return adminnotification.Append(tx, adminnotification.Entry{
|
|
AdminUserID: *order.ManagedAdminID,
|
|
Type: typ,
|
|
Title: title,
|
|
Content: content,
|
|
})
|
|
}
|
|
|
|
func applyPlatformManagedSettlement(order *model.RentalOrder, settlement *arbitrationSettlement) {
|
|
if order == nil || settlement == nil {
|
|
return
|
|
}
|
|
filtered := settlement.Entries[:0]
|
|
for _, entry := range settlement.Entries {
|
|
if entry.UserID == order.OwnerID && entry.Direction == "in" && entry.BizType == "arbitration_owner_income" {
|
|
continue
|
|
}
|
|
filtered = append(filtered, entry)
|
|
}
|
|
settlement.Entries = filtered
|
|
order.OfflineSettlementAmountCent = settlement.OwnerIncomeAmountCent
|
|
if settlement.OwnerIncomeAmountCent > 0 {
|
|
order.OfflineSettlementStatus = "pending"
|
|
} else {
|
|
order.OfflineSettlementStatus = "none"
|
|
}
|
|
}
|
|
|
|
type arbitrationSettlement struct {
|
|
Entries []wallet.Entry
|
|
RenterRefundAmountCent int64
|
|
RenterDepositRefundCent int64
|
|
OwnerIncomeAmountCent int64
|
|
DepositDeductAmountCent int64
|
|
ActualExtraItemAmountCent int64
|
|
}
|
|
|
|
func newArbitrationSettlement(order model.RentalOrder, renterFrozenBalanceCent int64) arbitrationSettlement {
|
|
settlement := arbitrationSettlement{}
|
|
orderID := order.ID
|
|
releaseFrozenAmountCent := money.MinCent(order.RentAmountCent+order.DepositAmountCent, renterFrozenBalanceCent)
|
|
if releaseFrozenAmountCent > 0 {
|
|
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
|
UserID: order.RenterID,
|
|
OrderID: &orderID,
|
|
Direction: "out",
|
|
AmountCent: releaseFrozenAmountCent,
|
|
BalanceType: "frozen",
|
|
BizType: "arbitration_release_frozen",
|
|
BizNo: order.OrderNo,
|
|
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 {
|
|
return
|
|
}
|
|
settlement.RenterRefundAmountCent += amountCent
|
|
}
|
|
addOwnerIncome := func(amountCent int64, remark string) {
|
|
if amountCent <= 0 {
|
|
return
|
|
}
|
|
settlement.addOwnerIncome(order, amountCent, remark)
|
|
}
|
|
|
|
switch req.Result {
|
|
case "full_refund":
|
|
addRenterRefund(totalCent, "仲裁全额退款")
|
|
settlement.RenterDepositRefundCent = depositAmountCent
|
|
case "partial_refund":
|
|
if req.AmountCent <= 0 || req.AmountCent > totalCent {
|
|
return settlement, ErrInvalidDispute
|
|
}
|
|
addRenterRefund(req.AmountCent, "仲裁部分退款")
|
|
// 部分退款为合并金额,无法精确拆分租金/押金,按押金优先归类以便暂扣。
|
|
settlement.RenterDepositRefundCent = money.MinCent(req.AmountCent, depositAmountCent)
|
|
// 号主仅拿「未退租金中的号主份额 + 未退押金」,平台加价按未退租金比例预留,避免整笔剩余进号主。
|
|
addOwnerIncome(
|
|
partialRefundOwnerIncomeCent(rentAmountCent, ownerRentAmountCent, depositAmountCent, req.AmountCent),
|
|
"仲裁剩余金额结算给号主",
|
|
)
|
|
case "release_deposit":
|
|
addOwnerIncome(ownerRentAmountCent, "仲裁确认订单金额结算给号主")
|
|
addRenterRefund(depositAmountCent, "仲裁释放押金给租客")
|
|
settlement.RenterDepositRefundCent = depositAmountCent
|
|
case "deduct_deposit", "compensate_owner":
|
|
deductAmountCent := req.AmountCent
|
|
if deductAmountCent <= 0 {
|
|
deductAmountCent = depositAmountCent
|
|
}
|
|
if deductAmountCent > depositAmountCent {
|
|
return settlement, ErrInvalidDispute
|
|
}
|
|
settlement.DepositDeductAmountCent = deductAmountCent
|
|
addOwnerIncome(ownerRentAmountCent+deductAmountCent, "仲裁订单金额及押金赔付结算给号主")
|
|
addRenterRefund(depositAmountCent-deductAmountCent, "仲裁退回剩余押金给租客")
|
|
settlement.RenterDepositRefundCent = depositAmountCent - deductAmountCent
|
|
case "order_close":
|
|
// Only release frozen funds. No available-balance settlement happens in development mode.
|
|
case "mark_abnormal":
|
|
// 标记异常只释放冻结账务,后续由客服继续线下复核。
|
|
default:
|
|
return settlement, ErrInvalidDispute
|
|
}
|
|
return settlement, nil
|
|
}
|
|
|
|
// partialRefundOwnerIncomeCent 计算部分退款后号主应得。
|
|
// 退款优先冲押金,其余冲租金;未退租金按预收结构拆分:
|
|
// 平台费 = 租客租金 − 号主租金,并按 未退租金/预收租金 比例预留,剩余才归号主。
|
|
// 这样可避免「剩余金额整笔给号主」把平台加价分给用户。
|
|
func partialRefundOwnerIncomeCent(rentAmountCent, ownerRentAmountCent, depositAmountCent, renterRefundCent int64) int64 {
|
|
if renterRefundCent < 0 {
|
|
renterRefundCent = 0
|
|
}
|
|
totalCent := rentAmountCent + depositAmountCent
|
|
if renterRefundCent > totalCent {
|
|
renterRefundCent = totalCent
|
|
}
|
|
if ownerRentAmountCent < 0 {
|
|
ownerRentAmountCent = 0
|
|
}
|
|
if ownerRentAmountCent > rentAmountCent {
|
|
ownerRentAmountCent = rentAmountCent
|
|
}
|
|
|
|
// 退款优先视为押金退还,其余视为租金退还(与 RenterDepositRefundCent 归类一致)
|
|
depositRefunded := money.MinCent(renterRefundCent, depositAmountCent)
|
|
rentRefunded := renterRefundCent - depositRefunded
|
|
rentLeft := rentAmountCent - rentRefunded
|
|
if rentLeft < 0 {
|
|
rentLeft = 0
|
|
}
|
|
depositLeft := depositAmountCent - depositRefunded
|
|
|
|
platformFeeCent := rentAmountCent - ownerRentAmountCent
|
|
if platformFeeCent < 0 {
|
|
platformFeeCent = 0
|
|
}
|
|
|
|
// 按未退租金占比预留平台费
|
|
platformKeepCent := int64(0)
|
|
if rentAmountCent > 0 && rentLeft > 0 && platformFeeCent > 0 {
|
|
platformKeepCent = int64(math.Round(float64(platformFeeCent) * float64(rentLeft) / float64(rentAmountCent)))
|
|
platformKeepCent = money.MinCent(platformKeepCent, rentLeft)
|
|
platformKeepCent = money.MinCent(platformKeepCent, platformFeeCent)
|
|
}
|
|
|
|
ownerFromRent := rentLeft - platformKeepCent
|
|
if ownerFromRent > ownerRentAmountCent {
|
|
ownerFromRent = ownerRentAmountCent
|
|
}
|
|
if ownerFromRent < 0 {
|
|
ownerFromRent = 0
|
|
}
|
|
|
|
ownerIncome := ownerFromRent + depositLeft
|
|
remaining := totalCent - renterRefundCent
|
|
if ownerIncome > remaining {
|
|
ownerIncome = remaining
|
|
}
|
|
if ownerIncome < 0 {
|
|
return 0
|
|
}
|
|
return ownerIncome
|
|
}
|
|
|
|
func (r *Repository) prepareRefund(order *model.RentalOrder, amountCent int64, bizType string, remark string) (*refundAction, error) {
|
|
if amountCent <= 0 {
|
|
return nil, nil
|
|
}
|
|
if r.refundStarter == nil {
|
|
return nil, ErrDependencyUnavailable
|
|
}
|
|
order.RefundStatus = "pending"
|
|
order.RefundAmountCent = amountCent
|
|
order.RefundedAt = nil
|
|
return &refundAction{
|
|
OrderID: order.ID,
|
|
RefundAmountCent: amountCent,
|
|
BizType: bizType,
|
|
Remark: remark,
|
|
}, nil
|
|
}
|
|
|
|
func (r *Repository) startRefundBestEffort(ctx context.Context, action *refundAction) {
|
|
if action == nil || r.refundStarter == nil {
|
|
return
|
|
}
|
|
if _, err := r.refundStarter.StartRefund(ctx, action.OrderID, action.RefundAmountCent, action.BizType, action.Remark); err != nil {
|
|
logging.FromContext(ctx).Error("争议退款提交失败",
|
|
zap.String("module", "dispute"),
|
|
zap.Uint64("order_id", action.OrderID),
|
|
zap.String("biz_type", action.BizType),
|
|
zap.Int64("amount_cent", action.RefundAmountCent),
|
|
zap.Error(err),
|
|
)
|
|
}
|
|
}
|
|
|
|
func renterFrozenBalance(tx *gorm.DB, renterID uint64) (int64, error) {
|
|
var account model.WalletAccount
|
|
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
|
Where("user_id = ?", renterID).
|
|
First(&account).Error
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return 0, nil
|
|
}
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return account.FrozenBalanceCent, nil
|
|
}
|
|
|
|
func arbitrateOrderStatus(result string) string {
|
|
switch result {
|
|
case "full_refund", "partial_refund", "order_close":
|
|
return "closed"
|
|
case "mark_abnormal":
|
|
return "abnormal"
|
|
default:
|
|
return "completed"
|
|
}
|
|
}
|
|
|
|
func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitrationSettlement) string {
|
|
content := fmt.Sprintf("客服仲裁结果:%s", arbitrationResultLabel(req.Result))
|
|
if req.Remark != "" {
|
|
content += "\n处理说明:" + req.Remark
|
|
}
|
|
if settlement.RenterRefundAmountCent > 0 {
|
|
content += "\n退款给租客:" + money.FormatWithSymbol(settlement.RenterRefundAmountCent)
|
|
}
|
|
if settlement.OwnerIncomeAmountCent > 0 {
|
|
content += "\n结算给号主:" + money.FormatWithSymbol(settlement.OwnerIncomeAmountCent)
|
|
}
|
|
if settlement.DepositDeductAmountCent > 0 {
|
|
content += "\n押金赔付扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent)
|
|
}
|
|
if settlement.ActualExtraItemAmountCent > 0 {
|
|
content += "\n额外物品消耗:" + money.FormatWithSymbol(settlement.ActualExtraItemAmountCent)
|
|
}
|
|
return content
|
|
}
|
|
|
|
func arbitrationResultLabel(result string) string {
|
|
switch result {
|
|
case "full_refund":
|
|
return "全额退款"
|
|
case "partial_refund":
|
|
return "部分退款"
|
|
case "deduct_deposit":
|
|
return "扣押金"
|
|
case "release_deposit":
|
|
return "释放押金"
|
|
case "compensate_owner":
|
|
return "赔付号主"
|
|
case "order_close":
|
|
return "关闭订单"
|
|
case "mark_abnormal":
|
|
return "标记异常"
|
|
default:
|
|
return result
|
|
}
|
|
}
|