优化仲裁
This commit is contained in:
@@ -32,6 +32,7 @@ type RentalOrder struct {
|
|||||||
ActualCoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"actual_coin_consumed_m"`
|
ActualCoinConsumedM float64 `gorm:"type:decimal(12,2);not null;default:0" json:"actual_coin_consumed_m"`
|
||||||
ActualPureCoinAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
ActualPureCoinAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||||
ActualPureCoinDiscountCent 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"`
|
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"`
|
RenterGrowthLevelName string `gorm:"size:32;not null;default:'普通'" json:"renter_growth_level_name"`
|
||||||
RenterDiscountBps int `gorm:"not null;default:10000" json:"renter_discount_bps"`
|
RenterDiscountBps int `gorm:"not null;default:10000" json:"renter_discount_bps"`
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
settlement, err := buildArbitrationSettlement(order, req, frozenBalance)
|
settlement, actualSettlement, linkedCheckout, depositDeductAmountCent, err := buildArbitrationDecision(tx, row, order, req, frozenBalance)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -114,23 +114,16 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if order.Status == "completed" {
|
if order.Status == "completed" {
|
||||||
coinConsumedM, linkedCheckout, err := arbitrationCoinConsumedM(tx, row, req)
|
if actualSettlement == nil {
|
||||||
if err != nil {
|
return ErrInvalidDispute
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
pureCoin := ordermodule.CalculateActualPureCoinAmounts(order, coinConsumedM)
|
applyArbitrationActualSettlement(&order, linkedCheckout, *actualSettlement, depositDeductAmountCent)
|
||||||
order.ActualCoinConsumedM = pureCoin.ConsumedM
|
|
||||||
order.ActualPureCoinAmountCent = pureCoin.AmountCent
|
|
||||||
order.ActualPureCoinDiscountCent = pureCoin.DiscountCent
|
|
||||||
if linkedCheckout != nil {
|
if linkedCheckout != nil {
|
||||||
linkedCheckout.PureCoinAmountCent = pureCoin.AmountCent
|
|
||||||
linkedCheckout.PureCoinDiscountCent = pureCoin.DiscountCent
|
|
||||||
linkedCheckout.PureCoinPayableCent = pureCoin.PayableCent
|
|
||||||
if err := tx.Save(linkedCheckout).Error; err != nil {
|
if err := tx.Save(linkedCheckout).Error; err != nil {
|
||||||
return err
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,6 +159,7 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
|||||||
"deposit_deduct_amount_cent": settlement.DepositDeductAmountCent,
|
"deposit_deduct_amount_cent": settlement.DepositDeductAmountCent,
|
||||||
"actual_coin_consumed_m": order.ActualCoinConsumedM,
|
"actual_coin_consumed_m": order.ActualCoinConsumedM,
|
||||||
"actual_pure_coin_cent": order.ActualPureCoinAmountCent,
|
"actual_pure_coin_cent": order.ActualPureCoinAmountCent,
|
||||||
|
"actual_extra_item_cent": order.ActualExtraItemAmountCent,
|
||||||
"before_order_status": beforeOrderStatus,
|
"before_order_status": beforeOrderStatus,
|
||||||
"after_order_status": order.Status,
|
"after_order_status": order.Status,
|
||||||
"before_handoff_status": beforeHandoffStatus,
|
"before_handoff_status": beforeHandoffStatus,
|
||||||
@@ -220,22 +214,131 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
|||||||
return &dto, nil
|
return &dto, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func arbitrationCoinConsumedM(tx *gorm.DB, row model.Dispute, req ArbitrateRequest) (float64, *model.OrderCheckout, error) {
|
func buildArbitrationDecision(tx *gorm.DB, row model.Dispute, order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, *ordermodule.ActualCheckoutSettlement, *model.OrderCheckout, int64, error) {
|
||||||
var checkout *model.OrderCheckout
|
if arbitrationUsesActualCheckout(req.Result) {
|
||||||
if row.CheckoutID != nil && *row.CheckoutID > 0 {
|
return buildActualCheckoutArbitrationSettlement(tx, row, order, req, renterFrozenBalanceCent)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
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 {
|
if req.ActualCoinConsumedM != nil {
|
||||||
return *req.ActualCoinConsumedM, checkout, nil
|
return *req.ActualCoinConsumedM, nil
|
||||||
}
|
}
|
||||||
if checkout != 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 {
|
func isPlatformManagedOrder(order model.RentalOrder) bool {
|
||||||
@@ -275,24 +378,18 @@ func applyPlatformManagedSettlement(order *model.RentalOrder, settlement *arbitr
|
|||||||
}
|
}
|
||||||
|
|
||||||
type arbitrationSettlement struct {
|
type arbitrationSettlement struct {
|
||||||
Entries []wallet.Entry
|
Entries []wallet.Entry
|
||||||
RenterRefundAmountCent int64
|
RenterRefundAmountCent int64
|
||||||
RenterDepositRefundCent int64
|
RenterDepositRefundCent int64
|
||||||
OwnerIncomeAmountCent int64
|
OwnerIncomeAmountCent int64
|
||||||
DepositDeductAmountCent int64
|
DepositDeductAmountCent int64
|
||||||
|
ActualExtraItemAmountCent int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, error) {
|
func newArbitrationSettlement(order model.RentalOrder, renterFrozenBalanceCent int64) arbitrationSettlement {
|
||||||
rentAmountCent := order.RentAmountCent
|
|
||||||
depositAmountCent := order.DepositAmountCent
|
|
||||||
ownerRentAmountCent := order.OwnerRentAmountCent
|
|
||||||
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
|
|
||||||
ownerRentAmountCent = rentAmountCent
|
|
||||||
}
|
|
||||||
totalCent := rentAmountCent + depositAmountCent
|
|
||||||
settlement := arbitrationSettlement{}
|
settlement := arbitrationSettlement{}
|
||||||
orderID := order.ID
|
orderID := order.ID
|
||||||
releaseFrozenAmountCent := money.MinCent(totalCent, renterFrozenBalanceCent)
|
releaseFrozenAmountCent := money.MinCent(order.RentAmountCent+order.DepositAmountCent, renterFrozenBalanceCent)
|
||||||
if releaseFrozenAmountCent > 0 {
|
if releaseFrozenAmountCent > 0 {
|
||||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||||
UserID: order.RenterID,
|
UserID: order.RenterID,
|
||||||
@@ -305,6 +402,36 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
|
|||||||
Remark: "仲裁释放冻结金额",
|
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) {
|
addRenterRefund := func(amountCent int64, remark string) {
|
||||||
if amountCent <= 0 {
|
if amountCent <= 0 {
|
||||||
@@ -316,17 +443,7 @@ func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, r
|
|||||||
if amountCent <= 0 {
|
if amountCent <= 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
settlement.OwnerIncomeAmountCent += amountCent
|
settlement.addOwnerIncome(order, amountCent, remark)
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch req.Result {
|
switch req.Result {
|
||||||
@@ -503,6 +620,9 @@ func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitration
|
|||||||
if settlement.DepositDeductAmountCent > 0 {
|
if settlement.DepositDeductAmountCent > 0 {
|
||||||
content += "\n押金赔付扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent)
|
content += "\n押金赔付扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent)
|
||||||
}
|
}
|
||||||
|
if settlement.ActualExtraItemAmountCent > 0 {
|
||||||
|
content += "\n额外物品消耗:" + money.FormatWithSymbol(settlement.ActualExtraItemAmountCent)
|
||||||
|
}
|
||||||
return content
|
return content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,38 +9,40 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DisputeDTO struct {
|
type DisputeDTO struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
OrderID uint64 `json:"order_id"`
|
OrderID uint64 `json:"order_id"`
|
||||||
OrderNo string `json:"order_no"`
|
OrderNo string `json:"order_no"`
|
||||||
OrderStatus string `json:"order_status"`
|
OrderStatus string `json:"order_status"`
|
||||||
HandoffStatus string `json:"handoff_status"`
|
HandoffStatus string `json:"handoff_status"`
|
||||||
SettlementStatus string `json:"settlement_status"`
|
SettlementStatus string `json:"settlement_status"`
|
||||||
ListingNo string `json:"listing_no"`
|
ExtraItemOriginalAmountCent int64 `json:"extra_item_original_amount_cent,omitempty"`
|
||||||
Title string `json:"title"`
|
ListingNo string `json:"listing_no"`
|
||||||
OwnerID uint64 `json:"owner_id"`
|
Title string `json:"title"`
|
||||||
RenterID uint64 `json:"renter_id"`
|
OwnerID uint64 `json:"owner_id"`
|
||||||
OwnerPhone string `json:"owner_phone"`
|
RenterID uint64 `json:"renter_id"`
|
||||||
RenterPhone string `json:"renter_phone"`
|
OwnerPhone string `json:"owner_phone"`
|
||||||
InitiatorID uint64 `json:"initiator_id"`
|
RenterPhone string `json:"renter_phone"`
|
||||||
InitiatorType string `json:"initiator_type"`
|
InitiatorID uint64 `json:"initiator_id"`
|
||||||
InitiatorAdminID *uint64 `json:"initiator_admin_id,omitempty"`
|
InitiatorType string `json:"initiator_type"`
|
||||||
TargetUserID uint64 `json:"target_user_id"`
|
InitiatorAdminID *uint64 `json:"initiator_admin_id,omitempty"`
|
||||||
Type string `json:"type"`
|
TargetUserID uint64 `json:"target_user_id"`
|
||||||
Status string `json:"status"`
|
Type string `json:"type"`
|
||||||
Description string `json:"description"`
|
Status string `json:"status"`
|
||||||
EvidenceURLS datatypes.JSON `json:"evidence_urls"`
|
Description string `json:"description"`
|
||||||
PreviousOrderStatus string `json:"previous_order_status"`
|
EvidenceURLS datatypes.JSON `json:"evidence_urls"`
|
||||||
PreviousHandoffStatus string `json:"previous_handoff_status"`
|
PreviousOrderStatus string `json:"previous_order_status"`
|
||||||
PreviousSettlementStatus string `json:"previous_settlement_status"`
|
PreviousHandoffStatus string `json:"previous_handoff_status"`
|
||||||
CheckoutID *uint64 `json:"checkout_id"`
|
PreviousSettlementStatus string `json:"previous_settlement_status"`
|
||||||
CheckoutCoinConsumedM *float64 `json:"checkout_coin_consumed_m,omitempty"`
|
CheckoutID *uint64 `json:"checkout_id"`
|
||||||
PreviousCheckoutStatus string `json:"previous_checkout_status"`
|
CheckoutCoinConsumedM *float64 `json:"checkout_coin_consumed_m,omitempty"`
|
||||||
ArbitrationResult string `json:"arbitration_result"`
|
CheckoutConsumableAmountCent *int64 `json:"checkout_consumable_amount_cent,omitempty"`
|
||||||
ArbitrationRemark string `json:"arbitration_remark"`
|
PreviousCheckoutStatus string `json:"previous_checkout_status"`
|
||||||
HandledBy *uint64 `json:"handled_by"`
|
ArbitrationResult string `json:"arbitration_result"`
|
||||||
HandledAt *time.Time `json:"handled_at"`
|
ArbitrationRemark string `json:"arbitration_remark"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
HandledBy *uint64 `json:"handled_by"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
HandledAt *time.Time `json:"handled_at"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateRequest struct {
|
type CreateRequest struct {
|
||||||
@@ -57,10 +59,11 @@ type AdminCreateRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ArbitrateRequest struct {
|
type ArbitrateRequest struct {
|
||||||
Result string `json:"result" binding:"required"`
|
Result string `json:"result" binding:"required"`
|
||||||
Remark string `json:"remark" binding:"required"`
|
Remark string `json:"remark" binding:"required"`
|
||||||
AmountCent int64 `json:"amount_cent"`
|
AmountCent int64 `json:"amount_cent"`
|
||||||
ActualCoinConsumedM *float64 `json:"actual_coin_consumed_m"`
|
ActualCoinConsumedM *float64 `json:"actual_coin_consumed_m"`
|
||||||
|
ActualConsumableAmountCent *int64 `json:"actual_consumable_amount_cent"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminListQuery struct {
|
type AdminListQuery struct {
|
||||||
|
|||||||
@@ -10,53 +10,57 @@ import (
|
|||||||
|
|
||||||
type disputeRow struct {
|
type disputeRow struct {
|
||||||
model.Dispute
|
model.Dispute
|
||||||
OrderNo string
|
OrderNo string
|
||||||
OrderStatus string
|
OrderStatus string
|
||||||
HandoffStatus string
|
HandoffStatus string
|
||||||
SettlementStatus string
|
SettlementStatus string
|
||||||
ListingNo string
|
ExtraItemOriginalAmountCent int64
|
||||||
Title string
|
ListingNo string
|
||||||
OwnerID uint64
|
Title string
|
||||||
RenterID uint64
|
OwnerID uint64
|
||||||
OwnerPhone string
|
RenterID uint64
|
||||||
RenterPhone string
|
OwnerPhone string
|
||||||
CheckoutCoinConsumedM *float64
|
RenterPhone string
|
||||||
|
CheckoutCoinConsumedM *float64
|
||||||
|
CheckoutConsumableAmountCent *int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (row disputeRow) toDTO() DisputeDTO {
|
func (row disputeRow) toDTO() DisputeDTO {
|
||||||
return DisputeDTO{
|
return DisputeDTO{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
OrderID: row.OrderID,
|
OrderID: row.OrderID,
|
||||||
OrderNo: row.OrderNo,
|
OrderNo: row.OrderNo,
|
||||||
OrderStatus: row.OrderStatus,
|
OrderStatus: row.OrderStatus,
|
||||||
HandoffStatus: row.HandoffStatus,
|
HandoffStatus: row.HandoffStatus,
|
||||||
SettlementStatus: row.SettlementStatus,
|
SettlementStatus: row.SettlementStatus,
|
||||||
ListingNo: row.ListingNo,
|
ExtraItemOriginalAmountCent: row.ExtraItemOriginalAmountCent,
|
||||||
Title: row.Title,
|
ListingNo: row.ListingNo,
|
||||||
OwnerID: row.OwnerID,
|
Title: row.Title,
|
||||||
RenterID: row.RenterID,
|
OwnerID: row.OwnerID,
|
||||||
OwnerPhone: row.OwnerPhone,
|
RenterID: row.RenterID,
|
||||||
RenterPhone: row.RenterPhone,
|
OwnerPhone: row.OwnerPhone,
|
||||||
InitiatorID: row.InitiatorID,
|
RenterPhone: row.RenterPhone,
|
||||||
InitiatorType: effectiveInitiatorType(row.Dispute),
|
InitiatorID: row.InitiatorID,
|
||||||
InitiatorAdminID: row.InitiatorAdminID,
|
InitiatorType: effectiveInitiatorType(row.Dispute),
|
||||||
TargetUserID: row.TargetUserID,
|
InitiatorAdminID: row.InitiatorAdminID,
|
||||||
Type: row.Type,
|
TargetUserID: row.TargetUserID,
|
||||||
Status: row.Status,
|
Type: row.Type,
|
||||||
Description: row.Description,
|
Status: row.Status,
|
||||||
EvidenceURLS: row.EvidenceURLS,
|
Description: row.Description,
|
||||||
PreviousOrderStatus: row.PreviousOrderStatus,
|
EvidenceURLS: row.EvidenceURLS,
|
||||||
PreviousHandoffStatus: row.PreviousHandoffStatus,
|
PreviousOrderStatus: row.PreviousOrderStatus,
|
||||||
PreviousSettlementStatus: row.PreviousSettlementStatus,
|
PreviousHandoffStatus: row.PreviousHandoffStatus,
|
||||||
CheckoutID: row.CheckoutID,
|
PreviousSettlementStatus: row.PreviousSettlementStatus,
|
||||||
CheckoutCoinConsumedM: row.CheckoutCoinConsumedM,
|
CheckoutID: row.CheckoutID,
|
||||||
PreviousCheckoutStatus: row.PreviousCheckoutStatus,
|
CheckoutCoinConsumedM: row.CheckoutCoinConsumedM,
|
||||||
ArbitrationResult: row.ArbitrationResult,
|
CheckoutConsumableAmountCent: row.CheckoutConsumableAmountCent,
|
||||||
ArbitrationRemark: row.ArbitrationRemark,
|
PreviousCheckoutStatus: row.PreviousCheckoutStatus,
|
||||||
HandledBy: row.HandledBy,
|
ArbitrationResult: row.ArbitrationResult,
|
||||||
HandledAt: row.HandledAt,
|
ArbitrationRemark: row.ArbitrationRemark,
|
||||||
CreatedAt: row.CreatedAt,
|
HandledBy: row.HandledBy,
|
||||||
UpdatedAt: row.UpdatedAt,
|
HandledAt: row.HandledAt,
|
||||||
|
CreatedAt: row.CreatedAt,
|
||||||
|
UpdatedAt: row.UpdatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,8 +91,10 @@ func applyUserVisibleFilter(db *gorm.DB, userID uint64) *gorm.DB {
|
|||||||
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
||||||
return r.adminFilterQuery(ctx).
|
return r.adminFilterQuery(ctx).
|
||||||
Select(`d.*, o.order_no, o.status AS order_status, o.handoff_status, o.settlement_status,
|
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,
|
o.owner_id, o.renter_id, o.extra_item_original_amount_cent,
|
||||||
l.listing_no, a.title, c.coin_consumed_m AS checkout_coin_consumed_m`)
|
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 {
|
func (r *Repository) adminFilterQuery(ctx context.Context) *gorm.DB {
|
||||||
|
|||||||
@@ -345,10 +345,21 @@ func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actualCoinConsumedM := 50.0
|
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",
|
Result: "release_deposit",
|
||||||
Remark: "确认实际消耗 50M",
|
Remark: "确认实际消耗 50M",
|
||||||
ActualCoinConsumedM: &actualCoinConsumedM,
|
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 {
|
}, AuditMeta{}); err != nil {
|
||||||
t.Fatalf("带实际消耗 M 仲裁失败: %v", err)
|
t.Fatalf("带实际消耗 M 仲裁失败: %v", err)
|
||||||
}
|
}
|
||||||
@@ -356,8 +367,8 @@ func TestArbitrationCompletedOrderRequiresActualCoinConsumedM(t *testing.T) {
|
|||||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||||
t.Fatalf("读取订单失败: %v", err)
|
t.Fatalf("读取订单失败: %v", err)
|
||||||
}
|
}
|
||||||
if saved.ActualCoinConsumedM != 50 || saved.ActualPureCoinAmountCent != 13150 || saved.GrowthPointsAwarded != 131 {
|
if saved.ActualCoinConsumedM != 50 || saved.ActualPureCoinAmountCent != 13150 || saved.ActualExtraItemAmountCent != 12000 || saved.GrowthPointsAwarded != 131 {
|
||||||
t.Fatalf("最终 M/纯币/积分 = %.2f/%d/%d, want 50/13150/131", saved.ActualCoinConsumedM, saved.ActualPureCoinAmountCent, saved.GrowthPointsAwarded)
|
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"
|
inputOrder.SettlementStatus = "pending"
|
||||||
_, renter, order := createDisputeOrderFixture(t, db, inputOrder)
|
_, renter, order := createDisputeOrderFixture(t, db, inputOrder)
|
||||||
checkout := model.OrderCheckout{
|
checkout := model.OrderCheckout{
|
||||||
OrderID: order.ID,
|
OrderID: order.ID,
|
||||||
InitiatedBy: renter.ID,
|
InitiatedBy: renter.ID,
|
||||||
Status: "submitted",
|
Status: "submitted",
|
||||||
CoinConsumedM: 25,
|
ConsumableAmountCent: 7000,
|
||||||
|
CoinConsumedM: 25,
|
||||||
}
|
}
|
||||||
if err := db.Create(&checkout).Error; err != nil {
|
if err := db.Create(&checkout).Error; err != nil {
|
||||||
t.Fatalf("创建结账记录失败: %v", err)
|
t.Fatalf("创建结账记录失败: %v", err)
|
||||||
@@ -387,8 +399,8 @@ func TestCheckoutDisputeArbitrationReusesCheckoutCoinConsumedM(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("创建结账争议失败: %v", err)
|
t.Fatalf("创建结账争议失败: %v", err)
|
||||||
}
|
}
|
||||||
if created.CheckoutID == nil || *created.CheckoutID != checkout.ID || created.CheckoutCoinConsumedM == nil || *created.CheckoutCoinConsumedM != 25 {
|
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, want %d/25", created.CheckoutID, created.CheckoutCoinConsumedM, checkout.ID)
|
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{
|
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 {
|
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||||
t.Fatalf("读取订单失败: %v", err)
|
t.Fatalf("读取订单失败: %v", err)
|
||||||
}
|
}
|
||||||
if saved.ActualCoinConsumedM != 25 || saved.ActualPureCoinAmountCent != 6575 || saved.GrowthPointsAwarded != 65 {
|
if saved.ActualCoinConsumedM != 25 || saved.ActualPureCoinAmountCent != 6575 || saved.ActualExtraItemAmountCent != 7000 || saved.GrowthPointsAwarded != 65 {
|
||||||
t.Fatalf("最终 M/纯币/积分 = %.2f/%d/%d, want 25/6575/65", saved.ActualCoinConsumedM, saved.ActualPureCoinAmountCent, saved.GrowthPointsAwarded)
|
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) {
|
if req.ActualCoinConsumedM != nil && (*req.ActualCoinConsumedM < 0 || *req.ActualCoinConsumedM > 1_000_000_000) {
|
||||||
return nil, ErrInvalidDispute
|
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)
|
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.ActualCoinConsumedM = settlement.CoinConsumedM
|
||||||
order.ActualPureCoinAmountCent = settlement.PureCoinAmountCent
|
order.ActualPureCoinAmountCent = settlement.PureCoinAmountCent
|
||||||
order.ActualPureCoinDiscountCent = settlement.PureCoinDiscountCent
|
order.ActualPureCoinDiscountCent = settlement.PureCoinDiscountCent
|
||||||
|
order.ActualExtraItemAmountCent = settlement.ExtraItemAmountCent
|
||||||
if err := appendCheckoutCompletedNotifications(tx, order, renterContent); err != nil {
|
if err := appendCheckoutCompletedNotifications(tx, order, renterContent); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ type OrderDTO struct {
|
|||||||
ActualCoinConsumedM float64 `json:"actual_coin_consumed_m,omitempty"`
|
ActualCoinConsumedM float64 `json:"actual_coin_consumed_m,omitempty"`
|
||||||
ActualPureCoinAmountCent int64 `json:"actual_pure_coin_amount_cent,omitempty"`
|
ActualPureCoinAmountCent int64 `json:"actual_pure_coin_amount_cent,omitempty"`
|
||||||
ActualPureCoinDiscountCent int64 `json:"actual_pure_coin_discount_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"`
|
RenterGrowthLevel string `json:"renter_growth_level,omitempty"`
|
||||||
RenterGrowthLevelName string `json:"renter_growth_level_name,omitempty"`
|
RenterGrowthLevelName string `json:"renter_growth_level_name,omitempty"`
|
||||||
RenterDiscountBps int `json:"renter_discount_bps,omitempty"`
|
RenterDiscountBps int `json:"renter_discount_bps,omitempty"`
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
|||||||
ActualCoinConsumedM: row.ActualCoinConsumedM,
|
ActualCoinConsumedM: row.ActualCoinConsumedM,
|
||||||
ActualPureCoinAmountCent: row.ActualPureCoinAmountCent,
|
ActualPureCoinAmountCent: row.ActualPureCoinAmountCent,
|
||||||
ActualPureCoinDiscountCent: row.ActualPureCoinDiscountCent,
|
ActualPureCoinDiscountCent: row.ActualPureCoinDiscountCent,
|
||||||
|
ActualExtraItemAmountCent: row.ActualExtraItemAmountCent,
|
||||||
RenterGrowthLevel: effectiveRenterGrowthLevel(row.RentalOrder),
|
RenterGrowthLevel: effectiveRenterGrowthLevel(row.RentalOrder),
|
||||||
RenterGrowthLevelName: effectiveRenterGrowthLevelName(row.RentalOrder),
|
RenterGrowthLevelName: effectiveRenterGrowthLevelName(row.RentalOrder),
|
||||||
RenterDiscountBps: effectiveRenterDiscountBps(row.RentalOrder),
|
RenterDiscountBps: effectiveRenterDiscountBps(row.RentalOrder),
|
||||||
@@ -330,6 +331,7 @@ func applyOrderPriceView(dto *OrderDTO, order model.RentalOrder, userID uint64)
|
|||||||
dto.ActualCoinConsumedM = 0
|
dto.ActualCoinConsumedM = 0
|
||||||
dto.ActualPureCoinAmountCent = 0
|
dto.ActualPureCoinAmountCent = 0
|
||||||
dto.ActualPureCoinDiscountCent = 0
|
dto.ActualPureCoinDiscountCent = 0
|
||||||
|
dto.ActualExtraItemAmountCent = 0
|
||||||
dto.RenterGrowthLevel = ""
|
dto.RenterGrowthLevel = ""
|
||||||
dto.RenterGrowthLevelName = ""
|
dto.RenterGrowthLevelName = ""
|
||||||
dto.RenterDiscountBps = 0
|
dto.RenterDiscountBps = 0
|
||||||
@@ -362,6 +364,7 @@ func applyOrderPriceView(dto *OrderDTO, order model.RentalOrder, userID uint64)
|
|||||||
dto.ActualCoinConsumedM = 0
|
dto.ActualCoinConsumedM = 0
|
||||||
dto.ActualPureCoinAmountCent = 0
|
dto.ActualPureCoinAmountCent = 0
|
||||||
dto.ActualPureCoinDiscountCent = 0
|
dto.ActualPureCoinDiscountCent = 0
|
||||||
|
dto.ActualExtraItemAmountCent = 0
|
||||||
dto.RenterGrowthLevel = ""
|
dto.RenterGrowthLevel = ""
|
||||||
dto.RenterGrowthLevelName = ""
|
dto.RenterGrowthLevelName = ""
|
||||||
dto.RenterDiscountBps = 0
|
dto.RenterDiscountBps = 0
|
||||||
|
|||||||
@@ -50,9 +50,28 @@ type ActualPureCoinAmounts struct {
|
|||||||
ConsumedM float64
|
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 计算纯币原价及等级优惠。
|
// CalculateActualPureCoinAmounts 按订单价格快照和实际消耗 M 计算纯币原价及等级优惠。
|
||||||
func CalculateActualPureCoinAmounts(order model.RentalOrder, coinConsumedM float64) ActualPureCoinAmounts {
|
func CalculateActualPureCoinAmounts(order model.RentalOrder, coinConsumedM float64) ActualPureCoinAmounts {
|
||||||
settlement := calculateCheckoutSettlement(order, 0, coinConsumedM, 0)
|
settlement := CalculateActualCheckoutSettlement(order, 0, coinConsumedM, 0)
|
||||||
return ActualPureCoinAmounts{
|
return ActualPureCoinAmounts{
|
||||||
AmountCent: settlement.PureCoinAmountCent,
|
AmountCent: settlement.PureCoinAmountCent,
|
||||||
DiscountCent: settlement.PureCoinDiscountCent,
|
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 {
|
func orderDurationHours(order model.RentalOrder) int {
|
||||||
if order.EstimatedDurationHours > 0 {
|
if order.EstimatedDurationHours > 0 {
|
||||||
return order.EstimatedDurationHours
|
return order.EstimatedDurationHours
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- +goose Up
|
||||||
|
|
||||||
|
ALTER TABLE rental_orders
|
||||||
|
ADD COLUMN actual_extra_item_amount_cent BIGINT NOT NULL DEFAULT 0 COMMENT '最终实际额外物品金额(分)' AFTER actual_pure_coin_discount_cent;
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
|
||||||
|
ALTER TABLE rental_orders
|
||||||
|
DROP COLUMN actual_extra_item_amount_cent;
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
} from '@/shared/utils/statusLabels'
|
} from '@/shared/utils/statusLabels'
|
||||||
import { formatDateTime } from '@/shared/utils/time'
|
import { formatDateTime } from '@/shared/utils/time'
|
||||||
import { formatListingNo } from '@/shared/utils/listingDisplay'
|
import { formatListingNo } from '@/shared/utils/listingDisplay'
|
||||||
import { yuanToCent } from '@/shared/utils/money'
|
import { centToYuan, yuanToCent } from '@/shared/utils/money'
|
||||||
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
import AdminTablePagination from '../components/AdminTablePagination.vue'
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -27,6 +27,7 @@ const result = ref('release_deposit')
|
|||||||
const remark = ref('')
|
const remark = ref('')
|
||||||
const amount = ref<number | undefined>()
|
const amount = ref<number | undefined>()
|
||||||
const actualCoinConsumedM = ref<number | undefined>()
|
const actualCoinConsumedM = ref<number | undefined>()
|
||||||
|
const actualConsumableAmountYuan = ref<number | undefined>()
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const currentPageSize = ref(20)
|
const currentPageSize = ref(20)
|
||||||
const total = ref(0)
|
const total = ref(0)
|
||||||
@@ -43,6 +44,12 @@ const hasValidActualCoinConsumedM = computed(
|
|||||||
Number.isFinite(actualCoinConsumedM.value) &&
|
Number.isFinite(actualCoinConsumedM.value) &&
|
||||||
actualCoinConsumedM.value >= 0
|
actualCoinConsumedM.value >= 0
|
||||||
)
|
)
|
||||||
|
const hasValidActualConsumableAmount = computed(
|
||||||
|
() =>
|
||||||
|
typeof actualConsumableAmountYuan.value === 'number' &&
|
||||||
|
Number.isFinite(actualConsumableAmountYuan.value) &&
|
||||||
|
actualConsumableAmountYuan.value >= 0
|
||||||
|
)
|
||||||
const pendingCount = computed(
|
const pendingCount = computed(
|
||||||
() => disputes.value.filter(item => ['open', 'processing'].includes(item.status)).length
|
() => disputes.value.filter(item => ['open', 'processing'].includes(item.status)).length
|
||||||
)
|
)
|
||||||
@@ -62,6 +69,7 @@ const canSubmitArbitration = computed(() => {
|
|||||||
!hasValidActualCoinConsumedM.value
|
!hasValidActualCoinConsumedM.value
|
||||||
)
|
)
|
||||||
return false
|
return false
|
||||||
|
if (isCompletedResult.value && !hasValidActualConsumableAmount.value) return false
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
function canArbitrate(row: Dispute) {
|
function canArbitrate(row: Dispute) {
|
||||||
@@ -163,6 +171,9 @@ function openArbitration(row: Dispute) {
|
|||||||
remark.value = row.arbitration_remark || ''
|
remark.value = row.arbitration_remark || ''
|
||||||
amount.value = undefined
|
amount.value = undefined
|
||||||
actualCoinConsumedM.value = row.checkout_coin_consumed_m ?? undefined
|
actualCoinConsumedM.value = row.checkout_coin_consumed_m ?? undefined
|
||||||
|
actualConsumableAmountYuan.value = centToYuan(
|
||||||
|
row.checkout_consumable_amount_cent ?? row.extra_item_original_amount_cent ?? 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetFilters() {
|
function resetFilters() {
|
||||||
@@ -301,6 +312,10 @@ async function handleArbitrate() {
|
|||||||
ElMessage.warning('请填写最终实际哈夫币消耗 M')
|
ElMessage.warning('请填写最终实际哈夫币消耗 M')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (isCompletedResult.value && !hasValidActualConsumableAmount.value) {
|
||||||
|
ElMessage.warning('请填写最终实际额外物品消耗金额')
|
||||||
|
return
|
||||||
|
}
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
await arbitrateDispute(activeDispute.value.id, {
|
await arbitrateDispute(activeDispute.value.id, {
|
||||||
@@ -308,11 +323,15 @@ async function handleArbitrate() {
|
|||||||
remark: remark.value.trim(),
|
remark: remark.value.trim(),
|
||||||
amount_cent: amount.value ? yuanToCent(amount.value) : undefined,
|
amount_cent: amount.value ? yuanToCent(amount.value) : undefined,
|
||||||
actual_coin_consumed_m: isCompletedResult.value ? actualCoinConsumedM.value : undefined,
|
actual_coin_consumed_m: isCompletedResult.value ? actualCoinConsumedM.value : undefined,
|
||||||
|
actual_consumable_amount_cent: isCompletedResult.value
|
||||||
|
? yuanToCent(actualConsumableAmountYuan.value)
|
||||||
|
: undefined,
|
||||||
})
|
})
|
||||||
ElMessage.success('仲裁结果已保存,双方已收到通知')
|
ElMessage.success('仲裁结果已保存,双方已收到通知')
|
||||||
activeDispute.value = null
|
activeDispute.value = null
|
||||||
remark.value = ''
|
remark.value = ''
|
||||||
actualCoinConsumedM.value = undefined
|
actualCoinConsumedM.value = undefined
|
||||||
|
actualConsumableAmountYuan.value = undefined
|
||||||
await loadDisputes()
|
await loadDisputes()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(readError(error, '仲裁失败'))
|
ElMessage.error(readError(error, '仲裁失败'))
|
||||||
@@ -527,6 +546,18 @@ async function handleArbitrate() {
|
|||||||
placeholder="实际消耗 M"
|
placeholder="实际消耗 M"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="isCompletedResult" class="arbitration-field">
|
||||||
|
<span>最终实际额外消耗品(元)</span>
|
||||||
|
<el-input-number
|
||||||
|
v-model="actualConsumableAmountYuan"
|
||||||
|
class="full-control"
|
||||||
|
:min="0"
|
||||||
|
:max="1000000000"
|
||||||
|
:precision="2"
|
||||||
|
:step="1"
|
||||||
|
placeholder="实际额外消耗品金额"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<el-input
|
<el-input
|
||||||
v-model="remark"
|
v-model="remark"
|
||||||
class="panel-action"
|
class="panel-action"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface Dispute {
|
|||||||
order_status: string
|
order_status: string
|
||||||
handoff_status: string
|
handoff_status: string
|
||||||
settlement_status: string
|
settlement_status: string
|
||||||
|
extra_item_original_amount_cent?: number
|
||||||
listing_no: string
|
listing_no: string
|
||||||
title: string
|
title: string
|
||||||
owner_id: number
|
owner_id: number
|
||||||
@@ -29,6 +30,7 @@ export interface Dispute {
|
|||||||
previous_settlement_status?: string
|
previous_settlement_status?: string
|
||||||
checkout_id?: number
|
checkout_id?: number
|
||||||
checkout_coin_consumed_m?: number
|
checkout_coin_consumed_m?: number
|
||||||
|
checkout_consumable_amount_cent?: number
|
||||||
previous_checkout_status?: string
|
previous_checkout_status?: string
|
||||||
arbitration_result: string
|
arbitration_result: string
|
||||||
arbitration_remark: string
|
arbitration_remark: string
|
||||||
@@ -99,6 +101,7 @@ export async function arbitrateDispute(
|
|||||||
remark: string
|
remark: string
|
||||||
amount_cent?: number
|
amount_cent?: number
|
||||||
actual_coin_consumed_m?: number
|
actual_coin_consumed_m?: number
|
||||||
|
actual_consumable_amount_cent?: number
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { data } = await apiClient.post<ApiResponse<Dispute>>(
|
const { data } = await apiClient.post<ApiResponse<Dispute>>(
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ export interface Order {
|
|||||||
actual_coin_consumed_m?: number
|
actual_coin_consumed_m?: number
|
||||||
actual_pure_coin_amount_cent?: number
|
actual_pure_coin_amount_cent?: number
|
||||||
actual_pure_coin_discount_cent?: number
|
actual_pure_coin_discount_cent?: number
|
||||||
|
actual_extra_item_amount_cent?: number
|
||||||
renter_growth_level?: string
|
renter_growth_level?: string
|
||||||
renter_growth_level_name?: string
|
renter_growth_level_name?: string
|
||||||
renter_discount_bps?: number
|
renter_discount_bps?: number
|
||||||
|
|||||||
Reference in New Issue
Block a user