修复平台代管订单流程
This commit is contained in:
@@ -53,8 +53,11 @@ func MigrateRentalTransactionTestSchema(db *gorm.DB) error {
|
||||
&model.RentalOrder{},
|
||||
&model.PaymentOrder{},
|
||||
&model.Notification{},
|
||||
&model.AdminNotification{},
|
||||
&model.HandoffRecord{},
|
||||
&model.OrderCheckout{},
|
||||
&model.WalletAccount{},
|
||||
&model.WalletLedger{},
|
||||
&model.AuditLog{},
|
||||
&model.ChatConversation{},
|
||||
&model.ChatParticipant{},
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/adminnotification"
|
||||
"hfb_sys/backend/internal/modules/chat"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
|
||||
@@ -55,6 +56,25 @@ func New(db *gorm.DB, redisClient *redis.Client, logger *zap.Logger) *Job {
|
||||
}
|
||||
}
|
||||
|
||||
func isPlatformManagedOrder(order *model.RentalOrder) bool {
|
||||
if order == nil {
|
||||
return false
|
||||
}
|
||||
return order.HandoffMode == "platform" || order.SettlementMode == "platform_managed"
|
||||
}
|
||||
|
||||
func appendManagedTimeoutNotification(tx *gorm.DB, order *model.RentalOrder, title string, content string) error {
|
||||
if order == nil || order.ManagedAdminID == nil || *order.ManagedAdminID == 0 {
|
||||
return nil
|
||||
}
|
||||
return adminnotification.Append(tx, adminnotification.Entry{
|
||||
AdminUserID: *order.ManagedAdminID,
|
||||
Type: "timeout",
|
||||
Title: title,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
// acquireLock 通过 Redis 分布式锁确保同一时刻只有一个实例执行超时扫描。
|
||||
// 未配置 Redis 时直接执行;Redis 出错时降级执行(事务内行锁与状态二次校验可兜底,不会写坏数据)。
|
||||
func (j *Job) acquireLock(ctx context.Context) (func(), bool) {
|
||||
@@ -314,6 +334,22 @@ func (j *Job) handleOwnerSubmitTimeout(ctx context.Context, now time.Time, cfg t
|
||||
before := snapshot(order)
|
||||
order.HandoffStatus = "owner_timeout"
|
||||
orderID := order.ID
|
||||
if isPlatformManagedOrder(order) {
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "timeout",
|
||||
Title: "客服代交接超时",
|
||||
Content: "客服未在规定时间内提交交接说明,你可以取消订单或发起申诉。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := appendManagedTimeoutNotification(tx, order, "代管订单交接超时", "订单已超过交接时限,请尽快进入订单详情处理。"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return before, nil
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
@@ -506,6 +542,22 @@ func (j *Job) handleOwnerReturnConfirmTimeout(ctx context.Context, now time.Time
|
||||
order.Status = "abnormal"
|
||||
order.HandoffStatus = "owner_checkout_confirm_timeout"
|
||||
orderID := order.ID
|
||||
if isPlatformManagedOrder(order) {
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "timeout",
|
||||
Title: "客服确认结账超时",
|
||||
Content: "客服未在规定时间内确认结账,订单已进入客服复核状态。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := appendManagedTimeoutNotification(tx, order, "代管订单确认结账超时", "订单已超过确认结账时限,请尽快进入订单详情处理。"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return before, nil
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
|
||||
@@ -25,6 +25,7 @@ func setupOrderTimeoutTestDB(t *testing.T) *gorm.DB {
|
||||
&model.RentalOrder{},
|
||||
&model.HandoffRecord{},
|
||||
&model.Notification{},
|
||||
&model.AdminNotification{},
|
||||
&model.ChatConversation{},
|
||||
&model.ChatParticipant{},
|
||||
&model.ChatMessage{},
|
||||
|
||||
@@ -38,6 +38,9 @@ type RentalListing struct {
|
||||
InTransaction bool `gorm:"not null;default:false" json:"in_transaction"`
|
||||
Status string `gorm:"size:32;not null;default:'draft'" json:"status"`
|
||||
ReviewStatus string `gorm:"size:32;not null;default:'none'" json:"review_status"`
|
||||
HandoffMode string `gorm:"size:16;not null;default:'owner';index" json:"handoff_mode"`
|
||||
SettlementMode string `gorm:"size:32;not null;default:'owner_wallet'" json:"settlement_mode"`
|
||||
ManagedAdminID *uint64 `gorm:"index" json:"managed_admin_id"`
|
||||
ReviewReason string `gorm:"size:255;not null;default:''" json:"review_reason"`
|
||||
PublishedAt *time.Time `json:"published_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
@@ -7,38 +7,46 @@ import (
|
||||
)
|
||||
|
||||
type RentalOrder struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
OrderNo string `gorm:"size:64;not null;uniqueIndex" json:"order_no"`
|
||||
ListingID uint64 `gorm:"not null;index" json:"listing_id"`
|
||||
AccountID uint64 `gorm:"not null;index" json:"account_id"`
|
||||
OwnerID uint64 `gorm:"not null;index" json:"owner_id"`
|
||||
RenterID uint64 `gorm:"not null;index" json:"renter_id"`
|
||||
RentedAt *time.Time `json:"rented_at"`
|
||||
HandoffStartedAt *time.Time `json:"handoff_started_at"`
|
||||
EstimatedDurationHours int `gorm:"not null;default:24" json:"estimated_duration_hours"`
|
||||
RentAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
OwnerRentAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
DepositAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
DepositOriginalAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
DepositWaivedAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
PlatformFeeCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
|
||||
Status string `gorm:"size:32;not null;default:'pending_payment'" json:"status"`
|
||||
HandoffStatus string `gorm:"size:32;not null;default:'none'" json:"handoff_status"`
|
||||
SettlementStatus string `gorm:"size:32;not null;default:'unsettled'" json:"settlement_status"`
|
||||
RefundStatus string `gorm:"size:32;not null;default:'none';index" json:"refund_status"`
|
||||
RefundAmountCent int64 `gorm:"not null;default:0" json:"refund_amount_cent"`
|
||||
RefundedAt *time.Time `json:"refunded_at"`
|
||||
DepositHoldStatus string `gorm:"size:16;not null;default:'none';index" json:"deposit_hold_status"`
|
||||
DepositHoldAmountCent int64 `gorm:"not null;default:0" json:"deposit_hold_amount_cent"`
|
||||
DepositHoldReason string `gorm:"size:255;not null;default:''" json:"deposit_hold_reason"`
|
||||
DepositHeldBy *uint64 `json:"deposit_held_by"`
|
||||
DepositHeldAt *time.Time `json:"deposit_held_at"`
|
||||
DepositHoldReleasedAt *time.Time `json:"deposit_hold_released_at"`
|
||||
OwnerSettledAt *time.Time `json:"owner_settled_at"`
|
||||
SettledAt *time.Time `json:"settled_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
OrderNo string `gorm:"size:64;not null;uniqueIndex" json:"order_no"`
|
||||
ListingID uint64 `gorm:"not null;index" json:"listing_id"`
|
||||
AccountID uint64 `gorm:"not null;index" json:"account_id"`
|
||||
OwnerID uint64 `gorm:"not null;index" json:"owner_id"`
|
||||
RenterID uint64 `gorm:"not null;index" json:"renter_id"`
|
||||
RentedAt *time.Time `json:"rented_at"`
|
||||
HandoffStartedAt *time.Time `json:"handoff_started_at"`
|
||||
EstimatedDurationHours int `gorm:"not null;default:24" json:"estimated_duration_hours"`
|
||||
RentAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
OwnerRentAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
DepositAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
DepositOriginalAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
DepositWaivedAmountCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
PlatformFeeCent int64 `gorm:"not null;default:0" json:"-"`
|
||||
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
|
||||
Status string `gorm:"size:32;not null;default:'pending_payment'" json:"status"`
|
||||
HandoffStatus string `gorm:"size:32;not null;default:'none'" json:"handoff_status"`
|
||||
HandoffMode string `gorm:"size:16;not null;default:'owner';index" json:"handoff_mode"`
|
||||
SettlementMode string `gorm:"size:32;not null;default:'owner_wallet'" json:"settlement_mode"`
|
||||
ManagedAdminID *uint64 `gorm:"index" json:"managed_admin_id"`
|
||||
SettlementStatus string `gorm:"size:32;not null;default:'unsettled'" json:"settlement_status"`
|
||||
OfflineSettlementStatus string `gorm:"size:16;not null;default:'none';index" json:"offline_settlement_status"`
|
||||
OfflineSettlementAmountCent int64 `gorm:"not null;default:0" json:"offline_settlement_amount_cent"`
|
||||
OfflineSettlementRemark string `gorm:"size:255;not null;default:''" json:"offline_settlement_remark"`
|
||||
OfflineSettledBy *uint64 `json:"offline_settled_by"`
|
||||
OfflineSettledAt *time.Time `json:"offline_settled_at"`
|
||||
RefundStatus string `gorm:"size:32;not null;default:'none';index" json:"refund_status"`
|
||||
RefundAmountCent int64 `gorm:"not null;default:0" json:"refund_amount_cent"`
|
||||
RefundedAt *time.Time `json:"refunded_at"`
|
||||
DepositHoldStatus string `gorm:"size:16;not null;default:'none';index" json:"deposit_hold_status"`
|
||||
DepositHoldAmountCent int64 `gorm:"not null;default:0" json:"deposit_hold_amount_cent"`
|
||||
DepositHoldReason string `gorm:"size:255;not null;default:''" json:"deposit_hold_reason"`
|
||||
DepositHeldBy *uint64 `json:"deposit_held_by"`
|
||||
DepositHeldAt *time.Time `json:"deposit_held_at"`
|
||||
DepositHoldReleasedAt *time.Time `json:"deposit_hold_released_at"`
|
||||
OwnerSettledAt *time.Time `json:"owner_settled_at"`
|
||||
SettledAt *time.Time `json:"settled_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (RentalOrder) TableName() string {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"hfb_sys/backend/internal/listingstatus"
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/adminnotification"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
"hfb_sys/backend/pkg/money"
|
||||
@@ -62,7 +63,12 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
||||
order.Status = arbitrateOrderStatus(req.Result)
|
||||
order.SettlementStatus = "arbitrated"
|
||||
order.SettledAt = &now
|
||||
order.OwnerSettledAt = &now
|
||||
if isPlatformManagedOrder(order) {
|
||||
order.OwnerSettledAt = nil
|
||||
applyPlatformManagedSettlement(&order, &settlement)
|
||||
} else {
|
||||
order.OwnerSettledAt = &now
|
||||
}
|
||||
if order.HandoffStatus != "cancelled" && order.HandoffStatus != "returned" {
|
||||
order.HandoffStatus = "arbitrated"
|
||||
}
|
||||
@@ -147,25 +153,32 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "arbitration",
|
||||
Title: "申诉仲裁已完成",
|
||||
Content: "客服已给出仲裁结果,请在订单和申诉记录中查看处理说明。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "arbitration",
|
||||
Title: "申诉仲裁已完成",
|
||||
Content: "客服已给出仲裁结果,请在订单和申诉记录中查看处理说明。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
); 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
|
||||
})
|
||||
@@ -181,6 +194,42 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -115,25 +115,35 @@ func (r *Repository) Create(ctx context.Context, userID uint64, orderID uint64,
|
||||
title = "订单进入结账争议"
|
||||
content = "对方已发起结账争议,请等待客服仲裁或补充结账证据。"
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: targetID,
|
||||
Type: "dispute",
|
||||
Title: title,
|
||||
Content: content,
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: userID,
|
||||
Type: "dispute",
|
||||
Title: "申诉已提交",
|
||||
Content: "申诉已进入待处理状态,客服仲裁后会通知双方。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
initiatorNotification := notification.Entry{
|
||||
UserID: userID,
|
||||
Type: "dispute",
|
||||
Title: "申诉已提交",
|
||||
Content: "申诉已进入待处理状态,客服仲裁后会通知双方。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
}
|
||||
if isPlatformManagedOrder(order) {
|
||||
if err := appendPlatformManagedAdminNotification(tx, order, "dispute", title, content); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := notification.Append(tx, initiatorNotification); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: targetID,
|
||||
Type: "dispute",
|
||||
Title: title,
|
||||
Content: content,
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
initiatorNotification,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
createdID = row.ID
|
||||
return nil
|
||||
|
||||
@@ -169,6 +169,76 @@ func TestRepositoryCancelRestoresCheckoutDisputeStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformManagedArbitrationUsesOfflineSettlement(t *testing.T) {
|
||||
db := setupDisputeTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
adminID := uint64(77)
|
||||
|
||||
owner, renter, order := createDisputeOrderFixture(t, db, model.RentalOrder{
|
||||
Status: "renting",
|
||||
HandoffStatus: "received",
|
||||
SettlementStatus: "unsettled",
|
||||
HandoffMode: "platform",
|
||||
SettlementMode: "platform_managed",
|
||||
ManagedAdminID: &adminID,
|
||||
RentAmountCent: 10000,
|
||||
OwnerRentAmountCent: 8000,
|
||||
PlatformFeeCent: 2000,
|
||||
OfflineSettlementStatus: "none",
|
||||
OfflineSettlementAmountCent: 0,
|
||||
})
|
||||
|
||||
created, err := repo.Create(t.Context(), renter.ID, order.ID, CreateRequest{
|
||||
Type: "cannot_login",
|
||||
Description: "外部上传账号需要客服处理",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("创建申诉失败: %v", err)
|
||||
}
|
||||
var ownerNotificationCount int64
|
||||
if err := db.Model(&model.Notification{}).Where("user_id = ?", owner.ID).Count(&ownerNotificationCount).Error; err != nil {
|
||||
t.Fatalf("统计号主通知失败: %v", err)
|
||||
}
|
||||
if ownerNotificationCount != 0 {
|
||||
t.Fatalf("owner notification count = %d, want 0", ownerNotificationCount)
|
||||
}
|
||||
var adminNotificationCount int64
|
||||
if err := db.Model(&model.AdminNotification{}).Where("admin_user_id = ?", adminID).Count(&adminNotificationCount).Error; err != nil {
|
||||
t.Fatalf("统计客服通知失败: %v", err)
|
||||
}
|
||||
if adminNotificationCount == 0 {
|
||||
t.Fatal("platform managed dispute should notify managed admin")
|
||||
}
|
||||
|
||||
if _, err := repo.Arbitrate(t.Context(), adminID, created.ID, ArbitrateRequest{
|
||||
Result: "release_deposit",
|
||||
Remark: "确认租金给卖家,线下结算",
|
||||
}, AuditMeta{}); err != nil {
|
||||
t.Fatalf("仲裁失败: %v", err)
|
||||
}
|
||||
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("读取订单失败: %v", err)
|
||||
}
|
||||
if saved.OfflineSettlementStatus != "pending" {
|
||||
t.Fatalf("offline settlement status = %q, want pending", saved.OfflineSettlementStatus)
|
||||
}
|
||||
if saved.OfflineSettlementAmountCent != 8000 {
|
||||
t.Fatalf("offline settlement amount = %d, want 8000", saved.OfflineSettlementAmountCent)
|
||||
}
|
||||
if saved.OwnerSettledAt != nil {
|
||||
t.Fatalf("owner settled at = %#v, want nil before offline settlement", saved.OwnerSettledAt)
|
||||
}
|
||||
var ownerLedgerCount int64
|
||||
if err := db.Model(&model.WalletLedger{}).Where("user_id = ?", owner.ID).Count(&ownerLedgerCount).Error; err != nil {
|
||||
t.Fatalf("统计钱包流水失败: %v", err)
|
||||
}
|
||||
if ownerLedgerCount != 0 {
|
||||
t.Fatalf("owner wallet ledger count = %d, want 0", ownerLedgerCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildArbitrationSettlementSkipsFrozenReleaseWhenNoFrozenBalance(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
ID: 11,
|
||||
|
||||
@@ -32,6 +32,9 @@ type ListingDTO struct {
|
||||
InTransaction bool `json:"in_transaction"`
|
||||
Status string `json:"status"`
|
||||
ReviewStatus string `json:"review_status"`
|
||||
HandoffMode string `json:"handoff_mode"`
|
||||
SettlementMode string `json:"settlement_mode"`
|
||||
ManagedAdminID *uint64 `json:"managed_admin_id,omitempty"`
|
||||
ReviewReason string `json:"review_reason"`
|
||||
PublishedAt *time.Time `json:"published_at"`
|
||||
ListingGroupConversationID uint64 `json:"listing_group_conversation_id,omitempty"`
|
||||
|
||||
@@ -170,6 +170,9 @@ func (r *Repository) CreateFromExternalUpload(ctx context.Context, upload extern
|
||||
DepositAmountCent: req.DepositAmountCent,
|
||||
Status: "draft",
|
||||
ReviewStatus: "pending",
|
||||
HandoffMode: "platform",
|
||||
SettlementMode: "platform_managed",
|
||||
ManagedAdminID: &admin.ID,
|
||||
}
|
||||
if err := tx.Create(&listing).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -127,6 +127,9 @@ func (row listingRow) toDTO() ListingDTO {
|
||||
InTransaction: row.InTransaction,
|
||||
Status: row.Status,
|
||||
ReviewStatus: reviewStatus,
|
||||
HandoffMode: row.HandoffMode,
|
||||
SettlementMode: row.SettlementMode,
|
||||
ManagedAdminID: row.ManagedAdminID,
|
||||
ReviewReason: reviewReason,
|
||||
PublishedAt: row.PublishedAt,
|
||||
CreatedAt: row.CreatedAt,
|
||||
@@ -159,6 +162,9 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
|
||||
InTransaction: listing.InTransaction,
|
||||
Status: listing.Status,
|
||||
ReviewStatus: reviewStatus,
|
||||
HandoffMode: listing.HandoffMode,
|
||||
SettlementMode: listing.SettlementMode,
|
||||
ManagedAdminID: listing.ManagedAdminID,
|
||||
ReviewReason: reviewReason,
|
||||
PublishedAt: listing.PublishedAt,
|
||||
CreatedAt: listing.CreatedAt,
|
||||
|
||||
@@ -63,6 +63,9 @@ func (r *Repository) TransferOwner(ctx context.Context, adminID uint64, listingI
|
||||
|
||||
beforeOwnerID := listing.OwnerID
|
||||
listing.OwnerID = target.ID
|
||||
listing.HandoffMode = "owner"
|
||||
listing.SettlementMode = "owner_wallet"
|
||||
listing.ManagedAdminID = nil
|
||||
account.OwnerID = target.ID
|
||||
if err := tx.Save(account).Error; err != nil {
|
||||
return err
|
||||
|
||||
@@ -31,10 +31,14 @@ func (r *Repository) SubmitCheckout(ctx context.Context, userID uint64, orderID
|
||||
if hasOpen {
|
||||
return ErrCheckoutCannotSubmit
|
||||
}
|
||||
checkoutToUserID := order.OwnerID
|
||||
if isPlatformSettlementOrder(order) && platformManagedAdminID(order) > 0 {
|
||||
checkoutToUserID = platformManagedAdminID(order)
|
||||
}
|
||||
record := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: order.RenterID,
|
||||
ToUserID: order.OwnerID,
|
||||
ToUserID: checkoutToUserID,
|
||||
Type: "renter_checkout",
|
||||
Content: req.Content,
|
||||
}
|
||||
@@ -58,15 +62,21 @@ func (r *Repository) SubmitCheckout(ctx context.Context, userID uint64, orderID
|
||||
now := time.Now()
|
||||
order.HandoffStartedAt = &now
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "checkout",
|
||||
Title: "租客已发起结账",
|
||||
Content: "请检查账号状态和消耗明细,确认无误后完成结算;也可修改后交由租客确认。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
if isPlatformSettlementOrder(order) {
|
||||
if err := appendManagedAdminNotification(tx, order, "checkout", "代管订单待确认结账", "租客已发起结账,请检查账号状态和消耗明细后确认。"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "checkout",
|
||||
Title: "租客已发起结账",
|
||||
Content: "请检查账号状态和消耗明细,确认无误后完成结算;也可修改后交由租客确认。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
@@ -163,37 +173,18 @@ func (r *Repository) CounterCheckout(ctx context.Context, userID uint64, orderID
|
||||
return ErrCheckoutMaxRounds
|
||||
}
|
||||
|
||||
depositDeductCent := req.DepositDeductAmountCent
|
||||
if depositDeductCent <= 0 && req.OtherAmountCent > 0 {
|
||||
depositDeductCent = req.OtherAmountCent
|
||||
}
|
||||
depositDeductCent := checkoutDepositDeductCent(req)
|
||||
next, err := buildCheckout(order, checkout.InitiatedBy, checkoutStatusCountered, checkout.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, depositDeductCent, depositDeductCent, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
checkout.Status = checkoutStatusCountered
|
||||
checkout.RoundCount = round + 1
|
||||
checkout.ProposedBy = userID
|
||||
checkout.RentAmountCent = next.RentAmountCent
|
||||
checkout.OwnerRentAmountCent = next.OwnerRentAmountCent
|
||||
checkout.PlatformFeeCent = next.PlatformFeeCent
|
||||
checkout.DepositAmountCent = next.DepositAmountCent
|
||||
checkout.ConsumableAmountCent = next.ConsumableAmountCent
|
||||
checkout.CoinConsumedM = next.CoinConsumedM
|
||||
checkout.OtherAmountCent = next.OtherAmountCent
|
||||
checkout.DepositDeductAmountCent = next.DepositDeductAmountCent
|
||||
checkout.RenterRefundAmountCent = next.RenterRefundAmountCent
|
||||
checkout.OwnerIncomeAmountCent = next.OwnerIncomeAmountCent
|
||||
checkout.ShortfallCent = next.ShortfallCent
|
||||
checkout.OvershootAmountCent = next.OvershootAmountCent
|
||||
checkout.OwnerAdjustmentReason = req.Reason
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
checkout.EvidenceURLS = next.EvidenceURLS
|
||||
applyCounterCheckoutUpdate(checkout, next, req.Reason, userID, round+1, now)
|
||||
|
||||
notifyUserID := order.RenterID
|
||||
notifyTitle := "号主已修改结账金额"
|
||||
notifyContent := "请核对对方修正的消耗和结算金额。可同意完结、继续还价(最多 6 轮),或发起争议。"
|
||||
notifyManagedAdmin := false
|
||||
if isOwner {
|
||||
checkout.Turn = checkoutTurnRenter
|
||||
order.Status = orderStatusPendingCheckoutAccept
|
||||
@@ -202,20 +193,30 @@ func (r *Repository) CounterCheckout(ctx context.Context, userID uint64, orderID
|
||||
checkout.Turn = checkoutTurnOwner
|
||||
order.Status = orderStatusPendingCheckoutConfirm
|
||||
order.HandoffStatus = handoffStatusPendingOwnerCheckout
|
||||
notifyUserID = order.OwnerID
|
||||
notifyTitle = "租客已修改结账金额"
|
||||
if isPlatformSettlementOrder(order) {
|
||||
notifyManagedAdmin = true
|
||||
} else {
|
||||
notifyUserID = order.OwnerID
|
||||
}
|
||||
}
|
||||
order.SettlementStatus = settlementStatusPending
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: notifyUserID,
|
||||
Type: "checkout",
|
||||
Title: notifyTitle,
|
||||
Content: notifyContent,
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
if notifyManagedAdmin {
|
||||
if err := appendManagedAdminNotification(tx, order, "checkout", notifyTitle, notifyContent); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: notifyUserID,
|
||||
Type: "checkout",
|
||||
Title: notifyTitle,
|
||||
Content: notifyContent,
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
@@ -238,6 +239,35 @@ func (r *Repository) CounterCheckout(ctx context.Context, userID uint64, orderID
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func checkoutDepositDeductCent(req CounterCheckoutRequest) int64 {
|
||||
depositDeductCent := req.DepositDeductAmountCent
|
||||
if depositDeductCent <= 0 && req.OtherAmountCent > 0 {
|
||||
depositDeductCent = req.OtherAmountCent
|
||||
}
|
||||
return depositDeductCent
|
||||
}
|
||||
|
||||
func applyCounterCheckoutUpdate(checkout *model.OrderCheckout, next model.OrderCheckout, reason string, proposedBy uint64, roundCount int, now time.Time) {
|
||||
checkout.Status = checkoutStatusCountered
|
||||
checkout.RoundCount = roundCount
|
||||
checkout.ProposedBy = proposedBy
|
||||
checkout.RentAmountCent = next.RentAmountCent
|
||||
checkout.OwnerRentAmountCent = next.OwnerRentAmountCent
|
||||
checkout.PlatformFeeCent = next.PlatformFeeCent
|
||||
checkout.DepositAmountCent = next.DepositAmountCent
|
||||
checkout.ConsumableAmountCent = next.ConsumableAmountCent
|
||||
checkout.CoinConsumedM = next.CoinConsumedM
|
||||
checkout.OtherAmountCent = next.OtherAmountCent
|
||||
checkout.DepositDeductAmountCent = next.DepositDeductAmountCent
|
||||
checkout.RenterRefundAmountCent = next.RenterRefundAmountCent
|
||||
checkout.OwnerIncomeAmountCent = next.OwnerIncomeAmountCent
|
||||
checkout.ShortfallCent = next.ShortfallCent
|
||||
checkout.OvershootAmountCent = next.OvershootAmountCent
|
||||
checkout.OwnerAdjustmentReason = reason
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
checkout.EvidenceURLS = next.EvidenceURLS
|
||||
}
|
||||
|
||||
// AcceptCheckout 租客同意当前提案并完结(轮到租客时)
|
||||
func (r *Repository) AcceptCheckout(ctx context.Context, userID uint64, orderID uint64) error {
|
||||
var refund *refundAction
|
||||
|
||||
@@ -20,7 +20,9 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
||||
order.HandoffStatus = handoffStatusReturned
|
||||
order.SettlementStatus = settlementStatusSettled
|
||||
order.SettledAt = &now
|
||||
order.OwnerSettledAt = &now
|
||||
if !isPlatformSettlementOrder(*order) {
|
||||
order.OwnerSettledAt = &now
|
||||
}
|
||||
if err := completeAssets(tx, listing, account); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -48,6 +50,16 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
|
||||
}
|
||||
|
||||
func appendCheckoutOwnerIncome(tx *gorm.DB, order *model.RentalOrder, settlement checkoutSettlement) error {
|
||||
if isPlatformSettlementOrder(*order) {
|
||||
amountCent := settlement.OwnerRentIncomeCent + settlement.DepositCompensationCent
|
||||
order.OfflineSettlementAmountCent = amountCent
|
||||
if amountCent > 0 {
|
||||
order.OfflineSettlementStatus = offlineSettlementStatusPending
|
||||
} else {
|
||||
order.OfflineSettlementStatus = offlineSettlementStatusNone
|
||||
}
|
||||
return nil
|
||||
}
|
||||
orderID := order.ID
|
||||
var ownerEntries []wallet.Entry
|
||||
if settlement.OwnerRentIncomeCent > 0 {
|
||||
@@ -102,6 +114,22 @@ func applyCheckoutSettlement(checkout *model.OrderCheckout, settlement checkoutS
|
||||
|
||||
func appendCheckoutCompletedNotifications(tx *gorm.DB, order *model.RentalOrder, renterContent string) error {
|
||||
orderID := order.ID
|
||||
if isPlatformSettlementOrder(*order) {
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "settlement",
|
||||
Title: "订单已完成",
|
||||
Content: renterContent,
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if order.OfflineSettlementAmountCent > 0 {
|
||||
return appendManagedAdminNotification(tx, *order, "settlement", "代管订单待线下结算", "订单已完成,请按线下流程向卖家转出结算金额并回填状态。")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
handoffStatusReturnOverdue = "return_overdue"
|
||||
handoffStatusPendingOwnerCheckout = "pending_owner_checkout"
|
||||
handoffStatusPendingRenterCheckout = "pending_renter_checkout"
|
||||
handoffStatusCheckoutDisputed = "checkout_disputed"
|
||||
handoffStatusReturned = "returned"
|
||||
handoffStatusOwnerTimeout = "owner_timeout"
|
||||
handoffStatusRenterConfirmTimeout = "renter_confirm_timeout"
|
||||
@@ -35,6 +36,17 @@ const (
|
||||
settlementStatusPending = "pending"
|
||||
settlementStatusSettled = "settled"
|
||||
settlementStatusClosed = "closed"
|
||||
settlementStatusDisputed = "disputed"
|
||||
|
||||
handoffModeOwner = "owner"
|
||||
handoffModePlatform = "platform"
|
||||
|
||||
settlementModeOwnerWallet = "owner_wallet"
|
||||
settlementModePlatformManaged = "platform_managed"
|
||||
|
||||
offlineSettlementStatusNone = "none"
|
||||
offlineSettlementStatusPending = "pending"
|
||||
offlineSettlementStatusSettled = "settled"
|
||||
|
||||
checkoutStatusSubmitted = "submitted"
|
||||
checkoutStatusCountered = "countered"
|
||||
|
||||
@@ -9,50 +9,63 @@ import (
|
||||
)
|
||||
|
||||
type OrderDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
ListingID uint64 `json:"listing_id"`
|
||||
ListingNo string `json:"listing_no"`
|
||||
AccountID uint64 `json:"account_id"`
|
||||
OwnerID uint64 `json:"owner_id"`
|
||||
RenterID uint64 `json:"renter_id"`
|
||||
OwnerPhone string `json:"owner_phone,omitempty"`
|
||||
RenterPhone string `json:"renter_phone,omitempty"`
|
||||
Title string `json:"title"`
|
||||
ServerRegion string `json:"server_region"`
|
||||
LoginPlatform string `json:"login_platform"`
|
||||
RentedAt *time.Time `json:"rented_at"`
|
||||
EstimatedDurationHours int `json:"estimated_duration_hours"`
|
||||
EstimatedEndAt *time.Time `json:"estimated_end_at,omitempty"`
|
||||
PriceRole string `json:"price_role,omitempty"`
|
||||
DisplayAmountCent int64 `json:"display_amount_cent"`
|
||||
RentAmountCent *int64 `json:"rent_amount_cent,omitempty"`
|
||||
OwnerRentAmountCent *int64 `json:"owner_rent_amount_cent,omitempty"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
||||
DepositOriginalAmountCent int64 `json:"deposit_original_amount_cent"`
|
||||
DepositWaivedAmountCent int64 `json:"deposit_waived_amount_cent"`
|
||||
PlatformFeeCent *int64 `json:"platform_fee_cent,omitempty"`
|
||||
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
|
||||
Status string `json:"status"`
|
||||
HandoffStatus string `json:"handoff_status"`
|
||||
SettlementStatus string `json:"settlement_status"`
|
||||
RefundStatus string `json:"refund_status,omitempty"`
|
||||
RefundAmountCent int64 `json:"refund_amount_cent"`
|
||||
DepositHoldStatus string `json:"deposit_hold_status,omitempty"`
|
||||
DepositHoldAmountCent int64 `json:"deposit_hold_amount_cent"`
|
||||
DepositHoldReason string `json:"deposit_hold_reason,omitempty"`
|
||||
DepositHeldAt *time.Time `json:"deposit_held_at,omitempty"`
|
||||
DepositHoldReleasedAt *time.Time `json:"deposit_hold_released_at,omitempty"`
|
||||
ActiveDispute *ActiveDisputeDTO `json:"active_dispute,omitempty"`
|
||||
Checkout *CheckoutDTO `json:"checkout,omitempty"`
|
||||
AdminActions *AdminActionsDTO `json:"admin_actions,omitempty"`
|
||||
PaymentDeadlineAt *time.Time `json:"payment_deadline_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint64 `json:"id"`
|
||||
OrderNo string `json:"order_no"`
|
||||
ListingID uint64 `json:"listing_id"`
|
||||
ListingNo string `json:"listing_no"`
|
||||
AccountID uint64 `json:"account_id"`
|
||||
OwnerID uint64 `json:"owner_id"`
|
||||
RenterID uint64 `json:"renter_id"`
|
||||
OwnerPhone string `json:"owner_phone,omitempty"`
|
||||
RenterPhone string `json:"renter_phone,omitempty"`
|
||||
Title string `json:"title"`
|
||||
ServerRegion string `json:"server_region"`
|
||||
LoginPlatform string `json:"login_platform"`
|
||||
RentedAt *time.Time `json:"rented_at"`
|
||||
EstimatedDurationHours int `json:"estimated_duration_hours"`
|
||||
EstimatedEndAt *time.Time `json:"estimated_end_at,omitempty"`
|
||||
PriceRole string `json:"price_role,omitempty"`
|
||||
DisplayAmountCent int64 `json:"display_amount_cent"`
|
||||
RentAmountCent *int64 `json:"rent_amount_cent,omitempty"`
|
||||
OwnerRentAmountCent *int64 `json:"owner_rent_amount_cent,omitempty"`
|
||||
DepositAmountCent int64 `json:"deposit_amount_cent"`
|
||||
DepositOriginalAmountCent int64 `json:"deposit_original_amount_cent"`
|
||||
DepositWaivedAmountCent int64 `json:"deposit_waived_amount_cent"`
|
||||
PlatformFeeCent *int64 `json:"platform_fee_cent,omitempty"`
|
||||
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
|
||||
Status string `json:"status"`
|
||||
HandoffStatus string `json:"handoff_status"`
|
||||
HandoffMode string `json:"handoff_mode"`
|
||||
SettlementMode string `json:"settlement_mode"`
|
||||
ManagedAdminID *uint64 `json:"managed_admin_id,omitempty"`
|
||||
SettlementStatus string `json:"settlement_status"`
|
||||
OfflineSettlementStatus string `json:"offline_settlement_status,omitempty"`
|
||||
OfflineSettlementAmountCent int64 `json:"offline_settlement_amount_cent"`
|
||||
OfflineSettlementRemark string `json:"offline_settlement_remark,omitempty"`
|
||||
OfflineSettledBy *uint64 `json:"offline_settled_by,omitempty"`
|
||||
OfflineSettledAt *time.Time `json:"offline_settled_at,omitempty"`
|
||||
RefundStatus string `json:"refund_status,omitempty"`
|
||||
RefundAmountCent int64 `json:"refund_amount_cent"`
|
||||
DepositHoldStatus string `json:"deposit_hold_status,omitempty"`
|
||||
DepositHoldAmountCent int64 `json:"deposit_hold_amount_cent"`
|
||||
DepositHoldReason string `json:"deposit_hold_reason,omitempty"`
|
||||
DepositHeldAt *time.Time `json:"deposit_held_at,omitempty"`
|
||||
DepositHoldReleasedAt *time.Time `json:"deposit_hold_released_at,omitempty"`
|
||||
ActiveDispute *ActiveDisputeDTO `json:"active_dispute,omitempty"`
|
||||
Checkout *CheckoutDTO `json:"checkout,omitempty"`
|
||||
AdminActions *AdminActionsDTO `json:"admin_actions,omitempty"`
|
||||
PaymentDeadlineAt *time.Time `json:"payment_deadline_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AdminActionsDTO struct {
|
||||
ResetHandoff *AdminActionDTO `json:"reset_handoff,omitempty"`
|
||||
ResetHandoff *AdminActionDTO `json:"reset_handoff,omitempty"`
|
||||
PlatformHandoff *AdminActionDTO `json:"platform_handoff,omitempty"`
|
||||
PlatformCheckoutConfirm *AdminActionDTO `json:"platform_checkout_confirm,omitempty"`
|
||||
PlatformCheckoutCounter *AdminActionDTO `json:"platform_checkout_counter,omitempty"`
|
||||
PlatformCheckoutDispute *AdminActionDTO `json:"platform_checkout_dispute,omitempty"`
|
||||
PlatformOfflineSettlement *AdminActionDTO `json:"platform_offline_settlement,omitempty"`
|
||||
}
|
||||
|
||||
type AdminActionDTO struct {
|
||||
@@ -100,6 +113,15 @@ type AdminActionRequest struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
|
||||
type PlatformHandoffRequest struct {
|
||||
Content string `json:"content" binding:"required"`
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
|
||||
type OfflineSettlementRequest struct {
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type AdminOrderQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
|
||||
@@ -2,6 +2,8 @@ package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
"hfb_sys/backend/internal/middleware"
|
||||
"hfb_sys/backend/pkg/response"
|
||||
@@ -74,6 +76,82 @@ func (h *Handler) AdminResetHandoff(c *gin.Context) {
|
||||
h.adminAction(c, h.service.AdminResetHandoff, gin.H{"reset": true})
|
||||
}
|
||||
|
||||
func (h *Handler) AdminPlatformHandoff(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req PlatformHandoffRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "交接说明和操作原因不能为空")
|
||||
return
|
||||
}
|
||||
record, err := h.service.AdminPlatformHandoff(c.Request.Context(), adminID, id, req, auditMeta(c))
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.Created(c, record)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminPlatformCheckoutConfirm(c *gin.Context) {
|
||||
h.adminAction(c, h.service.AdminPlatformCheckoutConfirm, gin.H{"confirmed": true})
|
||||
}
|
||||
|
||||
func (h *Handler) AdminPlatformCheckoutCounter(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req CounterCheckoutRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "结账修正原因不能为空")
|
||||
return
|
||||
}
|
||||
checkout, err := h.service.AdminPlatformCheckoutCounter(c.Request.Context(), adminID, id, req, auditMeta(c))
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, checkout)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminPlatformCheckoutDispute(c *gin.Context) {
|
||||
h.adminAction(c, h.service.AdminPlatformCheckoutDispute, gin.H{"disputed": true})
|
||||
}
|
||||
|
||||
func (h *Handler) AdminMarkOfflineSettlement(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req OfflineSettlementRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil && !errors.Is(err, io.EOF) {
|
||||
response.BadRequest(c, "线下结算备注格式不正确")
|
||||
return
|
||||
}
|
||||
if err := h.service.AdminMarkOfflineSettlement(c.Request.Context(), adminID, id, req, auditMeta(c)); err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"settled": true})
|
||||
}
|
||||
|
||||
func (h *Handler) AdminHoldDeposit(c *gin.Context) {
|
||||
h.adminAction(c, h.service.AdminHoldDeposit, gin.H{"held": true})
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@ func writeOrderError(c *gin.Context, err error) {
|
||||
response.Error(c, http.StatusConflict, "checkout_deposit_shortfall", "押金不足以覆盖打超/赔付差额,无法自动完结,请发起争议由人工处理")
|
||||
case errors.Is(err, ErrInvalidCheckoutAmount):
|
||||
response.BadRequest(c, "结账金额不符合规则")
|
||||
case errors.Is(err, ErrDisputeExists):
|
||||
response.Error(c, http.StatusConflict, "dispute_exists", "当前订单已有处理中争议")
|
||||
case errors.Is(err, ErrPermissionDenied):
|
||||
response.Error(c, http.StatusForbidden, "permission_denied", "无权操作该订单")
|
||||
case errors.Is(err, ErrDepositCannotHold):
|
||||
@@ -57,6 +59,8 @@ func writeOrderError(c *gin.Context, err error) {
|
||||
response.Error(c, http.StatusConflict, "deposit_not_held", "该订单押金未处于暂扣状态")
|
||||
case errors.Is(err, ErrDepositHoldAmountEmpty):
|
||||
response.Error(c, http.StatusConflict, "deposit_hold_amount_empty", "暂扣押金尚未形成可归还金额")
|
||||
case errors.Is(err, ErrOfflineSettlementCannotMark):
|
||||
response.Error(c, http.StatusConflict, "offline_settlement_cannot_mark", "当前订单不可确认线下结算")
|
||||
case IsNotFound(err):
|
||||
response.Error(c, http.StatusNotFound, "not_found", "订单不存在")
|
||||
default:
|
||||
|
||||
@@ -63,7 +63,11 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
||||
AccountSnapshot: snapshot,
|
||||
Status: orderStatusPendingPayment,
|
||||
HandoffStatus: handoffStatusNone,
|
||||
HandoffMode: listingHandoffMode(listing),
|
||||
SettlementMode: listingSettlementMode(listing),
|
||||
ManagedAdminID: listing.ManagedAdminID,
|
||||
SettlementStatus: settlementStatusUnsettled,
|
||||
OfflineSettlementStatus: offlineSettlementStatusNone,
|
||||
}
|
||||
if err := tx.Create(&order).Error; err != nil {
|
||||
return err
|
||||
@@ -94,6 +98,20 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
||||
return r.FindForUser(ctx, renterID, createdID)
|
||||
}
|
||||
|
||||
func listingHandoffMode(listing model.RentalListing) string {
|
||||
if listing.HandoffMode != "" {
|
||||
return listing.HandoffMode
|
||||
}
|
||||
return handoffModeOwner
|
||||
}
|
||||
|
||||
func listingSettlementMode(listing model.RentalListing) string {
|
||||
if listing.SettlementMode != "" {
|
||||
return listing.SettlementMode
|
||||
}
|
||||
return settlementModeOwnerWallet
|
||||
}
|
||||
|
||||
func (r *Repository) depositAmountsForOrder(tx *gorm.DB, renterID uint64, originalDepositCent int64) (int64, int64, error) {
|
||||
if originalDepositCent <= 0 {
|
||||
return 0, 0, nil
|
||||
@@ -196,25 +214,41 @@ func (r *Repository) ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint
|
||||
conversationID = listingConv.ID
|
||||
}
|
||||
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order",
|
||||
Title: "收到新的租号订单",
|
||||
Content: "租客已完成支付,请尽快提交交接说明。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
if isPlatformHandoffOrder(*order) {
|
||||
if err := appendManagedAdminNotification(tx, *order, "order", "代管订单待交接", "租客已完成支付,请尽快在订单详情中提交交接说明。"); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单支付成功",
|
||||
Content: "支付已完成,等待号主提交交接说明。",
|
||||
Content: "支付已完成,等待客服提交交接说明。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return 0, err
|
||||
}); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
} else {
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order",
|
||||
Title: "收到新的租号订单",
|
||||
Content: "租客已完成支付,请尽快提交交接说明。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单支付成功",
|
||||
Content: "支付已完成,等待号主提交交接说明。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if err := tx.Save(order).Error; err != nil {
|
||||
return 0, err
|
||||
@@ -257,25 +291,41 @@ func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64)
|
||||
} else {
|
||||
order.HandoffStatus = handoffStatusCancelled
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order",
|
||||
Title: "订单已取消",
|
||||
Content: "租客已取消订单,退款待客服审核。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
if isPlatformHandoffOrder(order) {
|
||||
if err := appendManagedAdminNotification(tx, order, "order", "代管订单已取消", "租客已取消订单,退款待客服审核。"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单取消成功",
|
||||
Content: "订单已取消,退款将由客服审核后原路退回。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order",
|
||||
Title: "订单已取消",
|
||||
Content: "租客已取消订单,退款待客服审核。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order",
|
||||
Title: "订单取消成功",
|
||||
Content: "订单已取消,退款将由客服审核后原路退回。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := releaseAssetsForRental(tx, listing, account); err != nil {
|
||||
return err
|
||||
@@ -329,7 +379,7 @@ func (r *Repository) ConfirmReceive(ctx context.Context, userID uint64, orderID
|
||||
}
|
||||
now := time.Now()
|
||||
if err := tx.Model(&model.HandoffRecord{}).
|
||||
Where("order_id = ? AND type = ?", order.ID, "owner_handoff").
|
||||
Where("order_id = ? AND type IN ?", order.ID, []string{"owner_handoff", "platform_handoff"}).
|
||||
Update("confirmed_by_renter_at", now).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -338,15 +388,21 @@ func (r *Repository) ConfirmReceive(ctx context.Context, userID uint64, orderID
|
||||
order.EstimatedDurationHours = estimateOrderDurationHours(order.AccountSnapshot)
|
||||
order.RentedAt = &now
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "handoff",
|
||||
Title: "租客已确认收号",
|
||||
Content: "订单已进入使用中。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
if isPlatformHandoffOrder(order) {
|
||||
if err := appendManagedAdminNotification(tx, order, "handoff", "租客已确认收号", "代管订单已进入使用中。"); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "handoff",
|
||||
Title: "租客已确认收号",
|
||||
Content: "订单已进入使用中。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Save(&order).Error
|
||||
})
|
||||
|
||||
@@ -0,0 +1,435 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/adminnotification"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func isPlatformHandoffOrder(order model.RentalOrder) bool {
|
||||
return effectiveHandoffMode(order) == handoffModePlatform
|
||||
}
|
||||
|
||||
func isPlatformSettlementOrder(order model.RentalOrder) bool {
|
||||
return effectiveSettlementMode(order) == settlementModePlatformManaged
|
||||
}
|
||||
|
||||
func platformManagedAdminID(order model.RentalOrder) uint64 {
|
||||
if order.ManagedAdminID == nil {
|
||||
return 0
|
||||
}
|
||||
return *order.ManagedAdminID
|
||||
}
|
||||
|
||||
func appendManagedAdminNotification(tx *gorm.DB, order model.RentalOrder, typ string, title string, content string) error {
|
||||
adminID := platformManagedAdminID(order)
|
||||
if adminID == 0 || title == "" {
|
||||
return nil
|
||||
}
|
||||
return adminnotification.Append(tx, adminnotification.Entry{
|
||||
AdminUserID: adminID,
|
||||
Type: typ,
|
||||
Title: title,
|
||||
Content: content,
|
||||
})
|
||||
}
|
||||
|
||||
func canAdminPlatformHandoff(order model.RentalOrder) bool {
|
||||
if !isPlatformHandoffOrder(order) {
|
||||
return false
|
||||
}
|
||||
if order.Status != orderStatusPendingHandoff {
|
||||
return false
|
||||
}
|
||||
return order.HandoffStatus == handoffStatusPendingOwner ||
|
||||
order.HandoffStatus == handoffStatusOwnerTimeout
|
||||
}
|
||||
|
||||
func canAdminPlatformCheckoutConfirm(order model.RentalOrder) bool {
|
||||
if !isPlatformSettlementOrder(order) {
|
||||
return false
|
||||
}
|
||||
if order.Status == orderStatusPendingCheckoutConfirm &&
|
||||
order.HandoffStatus == handoffStatusPendingOwnerCheckout {
|
||||
return true
|
||||
}
|
||||
return order.Status == orderStatusAbnormal &&
|
||||
order.HandoffStatus == handoffStatusOwnerCheckoutConfirmTimeout
|
||||
}
|
||||
|
||||
func canAdminMarkOfflineSettlement(order model.RentalOrder) bool {
|
||||
return isPlatformSettlementOrder(order) &&
|
||||
order.Status == orderStatusCompleted &&
|
||||
order.SettlementStatus == settlementStatusSettled &&
|
||||
effectiveOfflineSettlementStatus(order) == offlineSettlementStatusPending &&
|
||||
order.OfflineSettlementAmountCent > 0
|
||||
}
|
||||
|
||||
func (r *Repository) AdminPlatformHandoff(ctx context.Context, adminID uint64, orderID uint64, req PlatformHandoffRequest, meta AuditMeta) (*HandoffRecordDTO, error) {
|
||||
var recordID uint64
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !canAdminPlatformHandoff(order) {
|
||||
return ErrOrderCannotHandoff
|
||||
}
|
||||
beforeHandoffStatus := order.HandoffStatus
|
||||
if order.ManagedAdminID == nil {
|
||||
order.ManagedAdminID = &adminID
|
||||
}
|
||||
record := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: adminID,
|
||||
ToUserID: order.RenterID,
|
||||
Type: "platform_handoff",
|
||||
Content: req.Content,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
order.HandoffStatus = handoffStatusPendingRenterConfirm
|
||||
order.HandoffStartedAt = &now
|
||||
orderID := order.ID
|
||||
content := "客服已提交交接说明,请查看交接记录,确认账号可正常登录后点击确认收号。"
|
||||
if beforeHandoffStatus == handoffStatusOwnerTimeout {
|
||||
content = "客服已补交交接说明,请查看交接记录,确认账号可正常登录后点击确认收号。"
|
||||
}
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "handoff",
|
||||
Title: "客服已提交交接说明",
|
||||
Content: content,
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, adminID, "order.platform_handoff", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"reason": req.Reason,
|
||||
"before_handoff_status": beforeHandoffStatus,
|
||||
"after_handoff_status": order.HandoffStatus,
|
||||
"managed_admin_id": platformManagedAdminID(order),
|
||||
"handoff_content_length": len(req.Content),
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
recordID = record.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.findHandoffRecord(ctx, recordID)
|
||||
}
|
||||
|
||||
func (r *Repository) AdminPlatformCheckoutConfirm(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||
var refund *refundAction
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !canAdminPlatformCheckoutConfirm(order) {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
checkout, err := lockOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if normalizeCheckoutTurn(checkout) != checkoutTurnOwner {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
if err := ensureCheckoutCompletable(order, checkout); err != nil {
|
||||
return err
|
||||
}
|
||||
beforeOrderStatus := order.Status
|
||||
beforeHandoffStatus := order.HandoffStatus
|
||||
beforeSettlementStatus := order.SettlementStatus
|
||||
if order.ManagedAdminID == nil {
|
||||
order.ManagedAdminID = &adminID
|
||||
}
|
||||
now := time.Now()
|
||||
if err := tx.Model(&model.HandoffRecord{}).
|
||||
Where("order_id = ? AND type = ?", order.ID, "renter_checkout").
|
||||
Update("confirmed_by_owner_at", now).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
checkout.Status = checkoutStatusAccepted
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
action, err := r.finalizeCheckout(tx, &order, checkout, "客服已确认结账,订单完成。")
|
||||
refund = action
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return appendAuditLog(tx, adminID, "order.platform_checkout_confirm", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"reason": req.Reason,
|
||||
"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,
|
||||
"offline_settlement_status": order.OfflineSettlementStatus,
|
||||
"offline_settlement_amount_cent": order.OfflineSettlementAmountCent,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.startRefundBestEffort(ctx, refund)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Repository) AdminPlatformCheckoutCounter(ctx context.Context, adminID uint64, orderID uint64, req CounterCheckoutRequest, meta AuditMeta) (*CheckoutDTO, error) {
|
||||
var checkoutID uint64
|
||||
var orderSnapshot model.RentalOrder
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !canAdminPlatformCheckoutConfirm(order) {
|
||||
return ErrCheckoutCannotCounter
|
||||
}
|
||||
if order.ManagedAdminID == nil {
|
||||
order.ManagedAdminID = &adminID
|
||||
}
|
||||
checkout, err := lockOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if normalizeCheckoutTurn(checkout) != checkoutTurnOwner {
|
||||
return ErrCheckoutCannotCounter
|
||||
}
|
||||
round := checkout.RoundCount
|
||||
if round <= 0 {
|
||||
round = 1
|
||||
}
|
||||
if round >= checkoutMaxRounds {
|
||||
return ErrCheckoutMaxRounds
|
||||
}
|
||||
|
||||
depositDeductCent := checkoutDepositDeductCent(req)
|
||||
next, err := buildCheckout(order, checkout.InitiatedBy, checkoutStatusCountered, checkout.Content, req.EvidenceURLS, req.ConsumableAmountCent, req.CoinConsumedM, depositDeductCent, depositDeductCent, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
beforeOrderStatus := order.Status
|
||||
beforeHandoffStatus := order.HandoffStatus
|
||||
applyCounterCheckoutUpdate(checkout, next, req.Reason, adminID, round+1, now)
|
||||
checkout.Turn = checkoutTurnRenter
|
||||
order.Status = orderStatusPendingCheckoutAccept
|
||||
order.HandoffStatus = handoffStatusPendingRenterCheckout
|
||||
order.SettlementStatus = settlementStatusPending
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "checkout",
|
||||
Title: "客服已修改结账金额",
|
||||
Content: "请核对客服修正的消耗和结算金额。可同意完结、继续协商,或发起争议。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
record := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: adminID,
|
||||
ToUserID: order.RenterID,
|
||||
Type: "platform_checkout_counter",
|
||||
Content: "客服修改结账方案:" + req.Reason,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, adminID, "order.platform_checkout_counter", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"reason": req.Reason,
|
||||
"before_order_status": beforeOrderStatus,
|
||||
"after_order_status": order.Status,
|
||||
"before_handoff_status": beforeHandoffStatus,
|
||||
"after_handoff_status": order.HandoffStatus,
|
||||
"round_count": checkout.RoundCount,
|
||||
"consumable_amount_cent": checkout.ConsumableAmountCent,
|
||||
"coin_consumed_m": checkout.CoinConsumedM,
|
||||
"deposit_deduct_amount_cent": checkout.DepositDeductAmountCent,
|
||||
"owner_income_amount_cent": checkout.OwnerIncomeAmountCent,
|
||||
"renter_refund_amount_cent": checkout.RenterRefundAmountCent,
|
||||
"offline_settlement_amount_cent": order.OfflineSettlementAmountCent,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
checkoutID = checkout.ID
|
||||
orderSnapshot = order
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
checkout, err := r.findCheckout(ctx, checkoutID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := toCheckoutAdminDTO(*checkout)
|
||||
refreshCheckoutSettlementDTO(&dto, orderSnapshot, *checkout)
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) AdminPlatformCheckoutDispute(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !canAdminPlatformCheckoutConfirm(order) {
|
||||
return ErrCheckoutCannotCounter
|
||||
}
|
||||
if order.ManagedAdminID == nil {
|
||||
order.ManagedAdminID = &adminID
|
||||
}
|
||||
var count int64
|
||||
if err := tx.Model(&model.Dispute{}).
|
||||
Where("order_id = ? AND status IN ?", order.ID, []string{"open", "processing"}).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return ErrDisputeExists
|
||||
}
|
||||
checkout, err := lockOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if normalizeCheckoutTurn(checkout) != checkoutTurnOwner {
|
||||
return ErrCheckoutCannotCounter
|
||||
}
|
||||
evidence, err := marshalStringList([]string{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
beforeOrderStatus := order.Status
|
||||
beforeHandoffStatus := order.HandoffStatus
|
||||
beforeSettlementStatus := order.SettlementStatus
|
||||
row := model.Dispute{
|
||||
OrderID: order.ID,
|
||||
InitiatorID: adminID,
|
||||
TargetUserID: order.RenterID,
|
||||
Type: "checkout_dispute",
|
||||
Status: "open",
|
||||
Description: req.Reason,
|
||||
EvidenceURLS: evidence,
|
||||
PreviousOrderStatus: beforeOrderStatus,
|
||||
PreviousHandoffStatus: beforeHandoffStatus,
|
||||
PreviousSettlementStatus: beforeSettlementStatus,
|
||||
CheckoutID: &checkout.ID,
|
||||
PreviousCheckoutStatus: checkout.Status,
|
||||
}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
order.Status = orderStatusCheckoutDisputing
|
||||
order.HandoffStatus = handoffStatusCheckoutDisputed
|
||||
order.SettlementStatus = settlementStatusDisputed
|
||||
checkout.Status = checkoutStatusDisputed
|
||||
checkout.UpdatedAt = now
|
||||
record := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: adminID,
|
||||
ToUserID: order.RenterID,
|
||||
Type: "platform_checkout_dispute_opened",
|
||||
Content: "客服发起结账争议:" + req.Reason,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
disputeID := row.ID
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "dispute",
|
||||
Title: "订单进入结账争议",
|
||||
Content: "客服已发起结账争议,请等待仲裁处理或补充结账证据。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendManagedAdminNotification(tx, order, "dispute", "代管订单进入结账争议", "客服已发起结账争议,请在争议列表继续仲裁处理。"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, adminID, "order.platform_checkout_dispute", "order", order.ID, meta, map[string]any{
|
||||
"order_id": orderID,
|
||||
"order_no": order.OrderNo,
|
||||
"dispute_id": row.ID,
|
||||
"reason": req.Reason,
|
||||
"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,
|
||||
"checkout_id": checkout.ID,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(checkout).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) AdminMarkOfflineSettlement(ctx context.Context, adminID uint64, orderID uint64, req OfflineSettlementRequest, meta AuditMeta) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if !canAdminMarkOfflineSettlement(order) {
|
||||
return ErrOfflineSettlementCannotMark
|
||||
}
|
||||
beforeStatus := order.OfflineSettlementStatus
|
||||
now := time.Now()
|
||||
order.OfflineSettlementStatus = offlineSettlementStatusSettled
|
||||
order.OfflineSettlementRemark = req.Remark
|
||||
order.OfflineSettledBy = &adminID
|
||||
order.OfflineSettledAt = &now
|
||||
order.OwnerSettledAt = &now
|
||||
if err := appendAuditLog(tx, adminID, "order.offline_settlement", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"before_offline_settlement_status": beforeStatus,
|
||||
"after_offline_settlement_status": order.OfflineSettlementStatus,
|
||||
"offline_settlement_amount_cent": order.OfflineSettlementAmountCent,
|
||||
"remark": req.Remark,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(&order).Error
|
||||
})
|
||||
}
|
||||
@@ -9,16 +9,46 @@ import (
|
||||
)
|
||||
|
||||
func adminActionsForOrder(order model.RentalOrder) *AdminActionsDTO {
|
||||
actions := &AdminActionsDTO{}
|
||||
target, ok := resetTargetForOrder(order)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return &AdminActionsDTO{
|
||||
ResetHandoff: &AdminActionDTO{
|
||||
if ok {
|
||||
actions.ResetHandoff = &AdminActionDTO{
|
||||
Enabled: true,
|
||||
Label: target.Label,
|
||||
},
|
||||
}
|
||||
}
|
||||
if canAdminPlatformHandoff(order) {
|
||||
actions.PlatformHandoff = &AdminActionDTO{
|
||||
Enabled: true,
|
||||
Label: "客服代交接",
|
||||
}
|
||||
}
|
||||
if canAdminPlatformCheckoutConfirm(order) {
|
||||
actions.PlatformCheckoutConfirm = &AdminActionDTO{
|
||||
Enabled: true,
|
||||
Label: "客服确认结账",
|
||||
}
|
||||
actions.PlatformCheckoutCounter = &AdminActionDTO{
|
||||
Enabled: true,
|
||||
Label: "客服修改结账方案",
|
||||
}
|
||||
actions.PlatformCheckoutDispute = &AdminActionDTO{
|
||||
Enabled: true,
|
||||
Label: "发起结账争议",
|
||||
}
|
||||
}
|
||||
if canAdminMarkOfflineSettlement(order) {
|
||||
actions.PlatformOfflineSettlement = &AdminActionDTO{
|
||||
Enabled: true,
|
||||
Label: "确认线下结算",
|
||||
}
|
||||
}
|
||||
if actions.ResetHandoff == nil && actions.PlatformHandoff == nil &&
|
||||
actions.PlatformCheckoutConfirm == nil && actions.PlatformCheckoutCounter == nil &&
|
||||
actions.PlatformCheckoutDispute == nil && actions.PlatformOfflineSettlement == nil {
|
||||
return nil
|
||||
}
|
||||
return actions
|
||||
}
|
||||
|
||||
func (row orderRow) toAdminDTO() OrderDTO {
|
||||
@@ -29,43 +59,51 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
||||
ownerRentAmountCent := row.OwnerRentAmountCent
|
||||
platformFeeCent := row.PlatformFeeCent
|
||||
return OrderDTO{
|
||||
ID: row.ID,
|
||||
OrderNo: row.OrderNo,
|
||||
ListingID: row.ListingID,
|
||||
ListingNo: row.ListingNo,
|
||||
AccountID: row.AccountID,
|
||||
OwnerID: row.OwnerID,
|
||||
RenterID: row.RenterID,
|
||||
OwnerPhone: row.OwnerPhone,
|
||||
RenterPhone: row.RenterPhone,
|
||||
Title: row.Title,
|
||||
ServerRegion: row.ServerRegion,
|
||||
LoginPlatform: row.LoginPlatform,
|
||||
RentedAt: rentedAt,
|
||||
EstimatedDurationHours: durationHours,
|
||||
EstimatedEndAt: estimatedEndAt,
|
||||
PriceRole: "admin",
|
||||
DisplayAmountCent: row.RentAmountCent,
|
||||
RentAmountCent: &rentAmountCent,
|
||||
OwnerRentAmountCent: &ownerRentAmountCent,
|
||||
DepositAmountCent: row.DepositAmountCent,
|
||||
DepositOriginalAmountCent: effectiveDepositOriginalAmountCent(row.RentalOrder),
|
||||
DepositWaivedAmountCent: row.DepositWaivedAmountCent,
|
||||
PlatformFeeCent: &platformFeeCent,
|
||||
AccountSnapshot: row.AccountSnapshot,
|
||||
Status: row.Status,
|
||||
HandoffStatus: row.HandoffStatus,
|
||||
SettlementStatus: row.SettlementStatus,
|
||||
RefundStatus: row.RefundStatus,
|
||||
RefundAmountCent: row.RefundAmountCent,
|
||||
DepositHoldStatus: row.DepositHoldStatus,
|
||||
DepositHoldAmountCent: row.DepositHoldAmountCent,
|
||||
DepositHoldReason: row.DepositHoldReason,
|
||||
DepositHeldAt: row.DepositHeldAt,
|
||||
DepositHoldReleasedAt: row.DepositHoldReleasedAt,
|
||||
AdminActions: adminActionsForOrder(row.RentalOrder),
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
ID: row.ID,
|
||||
OrderNo: row.OrderNo,
|
||||
ListingID: row.ListingID,
|
||||
ListingNo: row.ListingNo,
|
||||
AccountID: row.AccountID,
|
||||
OwnerID: row.OwnerID,
|
||||
RenterID: row.RenterID,
|
||||
OwnerPhone: row.OwnerPhone,
|
||||
RenterPhone: row.RenterPhone,
|
||||
Title: row.Title,
|
||||
ServerRegion: row.ServerRegion,
|
||||
LoginPlatform: row.LoginPlatform,
|
||||
RentedAt: rentedAt,
|
||||
EstimatedDurationHours: durationHours,
|
||||
EstimatedEndAt: estimatedEndAt,
|
||||
PriceRole: "admin",
|
||||
DisplayAmountCent: row.RentAmountCent,
|
||||
RentAmountCent: &rentAmountCent,
|
||||
OwnerRentAmountCent: &ownerRentAmountCent,
|
||||
DepositAmountCent: row.DepositAmountCent,
|
||||
DepositOriginalAmountCent: effectiveDepositOriginalAmountCent(row.RentalOrder),
|
||||
DepositWaivedAmountCent: row.DepositWaivedAmountCent,
|
||||
PlatformFeeCent: &platformFeeCent,
|
||||
AccountSnapshot: row.AccountSnapshot,
|
||||
Status: row.Status,
|
||||
HandoffStatus: row.HandoffStatus,
|
||||
HandoffMode: effectiveHandoffMode(row.RentalOrder),
|
||||
SettlementMode: effectiveSettlementMode(row.RentalOrder),
|
||||
ManagedAdminID: row.ManagedAdminID,
|
||||
SettlementStatus: row.SettlementStatus,
|
||||
OfflineSettlementStatus: effectiveOfflineSettlementStatus(row.RentalOrder),
|
||||
OfflineSettlementAmountCent: row.OfflineSettlementAmountCent,
|
||||
OfflineSettlementRemark: row.OfflineSettlementRemark,
|
||||
OfflineSettledBy: row.OfflineSettledBy,
|
||||
OfflineSettledAt: row.OfflineSettledAt,
|
||||
RefundStatus: row.RefundStatus,
|
||||
RefundAmountCent: row.RefundAmountCent,
|
||||
DepositHoldStatus: row.DepositHoldStatus,
|
||||
DepositHoldAmountCent: row.DepositHoldAmountCent,
|
||||
DepositHoldReason: row.DepositHoldReason,
|
||||
DepositHeldAt: row.DepositHeldAt,
|
||||
DepositHoldReleasedAt: row.DepositHoldReleasedAt,
|
||||
AdminActions: adminActionsForOrder(row.RentalOrder),
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,10 +116,37 @@ func (row orderRow) toDTOForUser(userID uint64) OrderDTO {
|
||||
dto.DepositHoldReason = ""
|
||||
dto.DepositHeldAt = nil
|
||||
dto.DepositHoldReleasedAt = nil
|
||||
dto.ManagedAdminID = nil
|
||||
dto.OfflineSettlementStatus = ""
|
||||
dto.OfflineSettlementAmountCent = 0
|
||||
dto.OfflineSettlementRemark = ""
|
||||
dto.OfflineSettledBy = nil
|
||||
dto.OfflineSettledAt = nil
|
||||
applyOrderPriceView(&dto, row.RentalOrder, userID)
|
||||
return dto
|
||||
}
|
||||
|
||||
func effectiveHandoffMode(order model.RentalOrder) string {
|
||||
if order.HandoffMode != "" {
|
||||
return order.HandoffMode
|
||||
}
|
||||
return handoffModeOwner
|
||||
}
|
||||
|
||||
func effectiveSettlementMode(order model.RentalOrder) string {
|
||||
if order.SettlementMode != "" {
|
||||
return order.SettlementMode
|
||||
}
|
||||
return settlementModeOwnerWallet
|
||||
}
|
||||
|
||||
func effectiveOfflineSettlementStatus(order model.RentalOrder) string {
|
||||
if order.OfflineSettlementStatus != "" {
|
||||
return order.OfflineSettlementStatus
|
||||
}
|
||||
return offlineSettlementStatusNone
|
||||
}
|
||||
|
||||
func effectiveDepositOriginalAmountCent(order model.RentalOrder) int64 {
|
||||
if order.DepositOriginalAmountCent > 0 {
|
||||
return order.DepositOriginalAmountCent
|
||||
|
||||
@@ -155,16 +155,16 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
|
||||
return model.OrderCheckout{}, err
|
||||
}
|
||||
return model.OrderCheckout{
|
||||
OrderID: order.ID,
|
||||
InitiatedBy: initiatedBy,
|
||||
Status: status,
|
||||
RentAmountCent: settlement.ActualRentAmountCent,
|
||||
OwnerRentAmountCent: settlement.OwnerRentIncomeCent,
|
||||
PlatformFeeCent: settlement.PlatformFeeCent,
|
||||
DepositAmountCent: order.DepositAmountCent,
|
||||
ConsumableAmountCent: consumableAmountCent,
|
||||
CoinConsumedM: roundQuantity(coinConsumedM),
|
||||
OtherAmountCent: otherAmountCent,
|
||||
OrderID: order.ID,
|
||||
InitiatedBy: initiatedBy,
|
||||
Status: status,
|
||||
RentAmountCent: settlement.ActualRentAmountCent,
|
||||
OwnerRentAmountCent: settlement.OwnerRentIncomeCent,
|
||||
PlatformFeeCent: settlement.PlatformFeeCent,
|
||||
DepositAmountCent: order.DepositAmountCent,
|
||||
ConsumableAmountCent: consumableAmountCent,
|
||||
CoinConsumedM: roundQuantity(coinConsumedM),
|
||||
OtherAmountCent: otherAmountCent,
|
||||
// 存用户申报的押金赔付(损坏等),打超由结算自动计入 shortfall/overshoot
|
||||
DepositDeductAmountCent: deductAmountCent,
|
||||
RenterRefundAmountCent: settlement.RenterRefundCent,
|
||||
@@ -219,6 +219,17 @@ func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent i
|
||||
} else {
|
||||
usedOwnerCoinPriceCent = int64(math.Round(float64(sellerCoinBasePriceCent) * coinUseRatio))
|
||||
}
|
||||
coinOvershoot := totalCoinM > 0 && consumedM > totalCoinM
|
||||
if !coinOvershoot {
|
||||
// 未打超时,比例反推只能用于折算未用完部分,不能因四舍五入超过订单快照。
|
||||
if totalCoinM > 0 && consumedM >= totalCoinM {
|
||||
usedBuyerCoinPriceCent = buyerCoinBasePriceCent
|
||||
usedOwnerCoinPriceCent = sellerCoinBasePriceCent
|
||||
} else {
|
||||
usedBuyerCoinPriceCent = minCent(usedBuyerCoinPriceCent, buyerCoinBasePriceCent)
|
||||
usedOwnerCoinPriceCent = minCent(usedOwnerCoinPriceCent, sellerCoinBasePriceCent)
|
||||
}
|
||||
}
|
||||
|
||||
// 消耗品按申报金额计价,允许超过预收;号主侧按预收消耗品占比线性外推
|
||||
usedBuyerConsumablePriceCent := maxCent(consumableAmountCent, 0)
|
||||
@@ -227,6 +238,10 @@ func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent i
|
||||
consumableUseRatio = maxRatio(float64(usedBuyerConsumablePriceCent)/float64(prepaidConsumablePriceCent), 0)
|
||||
}
|
||||
usedOwnerConsumablePriceCent := int64(math.Round(float64(prepaidOwnerConsumablePriceCent) * consumableUseRatio))
|
||||
if usedBuyerConsumablePriceCent <= prepaidConsumablePriceCent {
|
||||
usedBuyerConsumablePriceCent = minCent(usedBuyerConsumablePriceCent, prepaidConsumablePriceCent)
|
||||
usedOwnerConsumablePriceCent = minCent(usedOwnerConsumablePriceCent, prepaidOwnerConsumablePriceCent)
|
||||
}
|
||||
|
||||
actualBuyerRentCent := usedBuyerCoinPriceCent + usedBuyerConsumablePriceCent
|
||||
actualOwnerRentCent := usedOwnerCoinPriceCent + usedOwnerConsumablePriceCent
|
||||
|
||||
@@ -831,3 +831,332 @@ func TestSubmitCheckoutRefreshesStageTime(t *testing.T) {
|
||||
t.Fatalf("handoff started at = %#v, should be refreshed after old stage time", saved.HandoffStartedAt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformManagedOrderOfflineSettlementFlow(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
|
||||
adminID := uint64(77)
|
||||
owner := model.User{Phone: "admin:77"}
|
||||
renter := model.User{Phone: "13900004001"}
|
||||
if err := db.Create(&owner).Error; err != nil {
|
||||
t.Fatalf("create owner failed: %v", err)
|
||||
}
|
||||
if err := db.Create(&renter).Error; err != nil {
|
||||
t.Fatalf("create renter failed: %v", err)
|
||||
}
|
||||
|
||||
account := model.GameAccount{
|
||||
OwnerID: owner.ID,
|
||||
Status: accountStatusRented,
|
||||
ServerRegion: "国服",
|
||||
LoginPlatform: "steam",
|
||||
Title: "平台代管账号",
|
||||
}
|
||||
if err := db.Create(&account).Error; err != nil {
|
||||
t.Fatalf("create account failed: %v", err)
|
||||
}
|
||||
listing := model.RentalListing{
|
||||
ListingNo: "LST-PM-001",
|
||||
OwnerID: owner.ID,
|
||||
AccountID: account.ID,
|
||||
Status: listingStatusRented,
|
||||
ReviewStatus: listingReviewStatusApproved,
|
||||
InTransaction: true,
|
||||
PriceCent: 10000,
|
||||
HandoffMode: handoffModePlatform,
|
||||
SettlementMode: settlementModePlatformManaged,
|
||||
ManagedAdminID: &adminID,
|
||||
}
|
||||
if err := db.Create(&listing).Error; err != nil {
|
||||
t.Fatalf("create listing failed: %v", err)
|
||||
}
|
||||
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-PM-001",
|
||||
ListingID: listing.ID,
|
||||
AccountID: account.ID,
|
||||
OwnerID: owner.ID,
|
||||
RenterID: renter.ID,
|
||||
Status: orderStatusPendingHandoff,
|
||||
HandoffStatus: handoffStatusPendingOwner,
|
||||
HandoffMode: handoffModePlatform,
|
||||
SettlementMode: settlementModePlatformManaged,
|
||||
ManagedAdminID: &adminID,
|
||||
RentAmountCent: 10000,
|
||||
OwnerRentAmountCent: 8000,
|
||||
PlatformFeeCent: 2000,
|
||||
DepositAmountCent: 0,
|
||||
EstimatedDurationHours: 24,
|
||||
OfflineSettlementStatus: offlineSettlementStatusNone,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{"haf_coin_amount":1000000}`)),
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatalf("create order failed: %v", err)
|
||||
}
|
||||
|
||||
record, err := repo.AdminPlatformHandoff(t.Context(), adminID, order.ID, PlatformHandoffRequest{
|
||||
Content: "账号和登录说明",
|
||||
Reason: "外部上传账号由客服代交接",
|
||||
}, AuditMeta{})
|
||||
if err != nil {
|
||||
t.Fatalf("AdminPlatformHandoff() error = %v", err)
|
||||
}
|
||||
if record.Type != "platform_handoff" || record.FromUserID != adminID || record.ToUserID != renter.ID {
|
||||
t.Fatalf("handoff record = %#v, want platform_handoff from admin to renter", record)
|
||||
}
|
||||
|
||||
if err := repo.ConfirmReceive(t.Context(), renter.ID, order.ID); err != nil {
|
||||
t.Fatalf("ConfirmReceive() error = %v", err)
|
||||
}
|
||||
var handoff model.HandoffRecord
|
||||
if err := db.Where("order_id = ? AND type = ?", order.ID, "platform_handoff").First(&handoff).Error; err != nil {
|
||||
t.Fatalf("load platform handoff failed: %v", err)
|
||||
}
|
||||
if handoff.ConfirmedByRenterAt == nil {
|
||||
t.Fatal("platform handoff should be confirmed by renter")
|
||||
}
|
||||
|
||||
if _, err := repo.SubmitCheckout(t.Context(), renter.ID, order.ID, SubmitCheckoutRequest{
|
||||
Content: "正常结账",
|
||||
CoinConsumedM: 1,
|
||||
}); err != nil {
|
||||
t.Fatalf("SubmitCheckout() error = %v", err)
|
||||
}
|
||||
if err := repo.AdminPlatformCheckoutConfirm(t.Context(), adminID, order.ID, AdminActionRequest{Reason: "客服核对无误"}, AuditMeta{}); err != nil {
|
||||
t.Fatalf("AdminPlatformCheckoutConfirm() error = %v", err)
|
||||
}
|
||||
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("load order failed: %v", err)
|
||||
}
|
||||
if saved.Status != orderStatusCompleted || saved.SettlementStatus != settlementStatusSettled {
|
||||
t.Fatalf("status = %s/%s, want completed/settled", saved.Status, saved.SettlementStatus)
|
||||
}
|
||||
if saved.OfflineSettlementStatus != offlineSettlementStatusPending {
|
||||
t.Fatalf("offline settlement status = %q, want pending", saved.OfflineSettlementStatus)
|
||||
}
|
||||
if saved.OfflineSettlementAmountCent != 8000 {
|
||||
t.Fatalf("offline settlement amount = %d, want 8000", saved.OfflineSettlementAmountCent)
|
||||
}
|
||||
if saved.OwnerSettledAt != nil {
|
||||
t.Fatalf("owner settled at = %#v, want nil before offline settlement", saved.OwnerSettledAt)
|
||||
}
|
||||
var ownerLedgerCount int64
|
||||
if err := db.Model(&model.WalletLedger{}).Where("user_id = ?", owner.ID).Count(&ownerLedgerCount).Error; err != nil {
|
||||
t.Fatalf("count wallet ledger failed: %v", err)
|
||||
}
|
||||
if ownerLedgerCount != 0 {
|
||||
t.Fatalf("owner wallet ledger count = %d, want 0", ownerLedgerCount)
|
||||
}
|
||||
|
||||
dto, err := repo.FindAdmin(t.Context(), order.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("FindAdmin() error = %v", err)
|
||||
}
|
||||
if dto.AdminActions == nil || dto.AdminActions.PlatformOfflineSettlement == nil {
|
||||
t.Fatal("admin dto should expose offline settlement action")
|
||||
}
|
||||
|
||||
if err := repo.AdminMarkOfflineSettlement(t.Context(), adminID, order.ID, OfflineSettlementRequest{Remark: "支付宝线下转账"}, AuditMeta{}); err != nil {
|
||||
t.Fatalf("AdminMarkOfflineSettlement() error = %v", err)
|
||||
}
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("reload order failed: %v", err)
|
||||
}
|
||||
if saved.OfflineSettlementStatus != offlineSettlementStatusSettled {
|
||||
t.Fatalf("offline settlement status = %q, want settled", saved.OfflineSettlementStatus)
|
||||
}
|
||||
if saved.OfflineSettledBy == nil || *saved.OfflineSettledBy != adminID {
|
||||
t.Fatalf("offline settled by = %#v, want %d", saved.OfflineSettledBy, adminID)
|
||||
}
|
||||
if saved.OfflineSettledAt == nil || saved.OwnerSettledAt == nil {
|
||||
t.Fatalf("offline settled at = %#v, owner settled at = %#v, want both set", saved.OfflineSettledAt, saved.OwnerSettledAt)
|
||||
}
|
||||
if saved.OfflineSettlementRemark != "支付宝线下转账" {
|
||||
t.Fatalf("offline settlement remark = %q", saved.OfflineSettlementRemark)
|
||||
}
|
||||
if err := db.Model(&model.WalletLedger{}).Where("user_id = ?", owner.ID).Count(&ownerLedgerCount).Error; err != nil {
|
||||
t.Fatalf("recount wallet ledger failed: %v", err)
|
||||
}
|
||||
if ownerLedgerCount != 0 {
|
||||
t.Fatalf("owner wallet ledger count after offline settlement = %d, want 0", ownerLedgerCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminPlatformCheckoutCounterCanBeAcceptedByRenter(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
adminID := uint64(78)
|
||||
_, renter, order := createPlatformManagedCheckoutOrder(t, db, adminID)
|
||||
|
||||
checkout := createOpenPlatformCheckout(t, db, order, renter.ID)
|
||||
dto, err := repo.AdminPlatformCheckoutCounter(t.Context(), adminID, order.ID, CounterCheckoutRequest{
|
||||
CoinConsumedM: 1,
|
||||
DepositDeductAmountCent: 1000,
|
||||
Reason: "客服核对后增加押金赔付",
|
||||
}, AuditMeta{})
|
||||
if err != nil {
|
||||
t.Fatalf("AdminPlatformCheckoutCounter() error = %v", err)
|
||||
}
|
||||
if dto.ID != checkout.ID || dto.Turn != checkoutTurnRenter {
|
||||
t.Fatalf("checkout dto = %#v, want same checkout and renter turn", dto)
|
||||
}
|
||||
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("load order failed: %v", err)
|
||||
}
|
||||
if saved.Status != orderStatusPendingCheckoutAccept || saved.HandoffStatus != handoffStatusPendingRenterCheckout {
|
||||
t.Fatalf("status = %s/%s, want pending_checkout_accept/pending_renter_checkout", saved.Status, saved.HandoffStatus)
|
||||
}
|
||||
var savedCheckout model.OrderCheckout
|
||||
if err := db.First(&savedCheckout, checkout.ID).Error; err != nil {
|
||||
t.Fatalf("load checkout failed: %v", err)
|
||||
}
|
||||
if savedCheckout.Status != checkoutStatusCountered || savedCheckout.ProposedBy != adminID {
|
||||
t.Fatalf("checkout status/proposed = %s/%d, want countered/%d", savedCheckout.Status, savedCheckout.ProposedBy, adminID)
|
||||
}
|
||||
if savedCheckout.DepositDeductAmountCent != 1000 {
|
||||
t.Fatalf("deposit deduct = %d, want 1000", savedCheckout.DepositDeductAmountCent)
|
||||
}
|
||||
|
||||
if err := repo.AcceptCheckout(t.Context(), renter.ID, order.ID); err != nil {
|
||||
t.Fatalf("AcceptCheckout() error = %v", err)
|
||||
}
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("reload order failed: %v", err)
|
||||
}
|
||||
if saved.Status != orderStatusCompleted || saved.OfflineSettlementStatus != offlineSettlementStatusPending {
|
||||
t.Fatalf("status/offline = %s/%s, want completed/pending", saved.Status, saved.OfflineSettlementStatus)
|
||||
}
|
||||
if saved.OfflineSettlementAmountCent != 9000 {
|
||||
t.Fatalf("offline settlement amount = %d, want 9000", saved.OfflineSettlementAmountCent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminPlatformCheckoutDisputeCreatesArbitrationCase(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
if err := db.AutoMigrate(&model.Dispute{}); err != nil {
|
||||
t.Fatalf("migrate dispute failed: %v", err)
|
||||
}
|
||||
repo := NewRepository(db)
|
||||
adminID := uint64(79)
|
||||
_, renter, order := createPlatformManagedCheckoutOrder(t, db, adminID)
|
||||
checkout := createOpenPlatformCheckout(t, db, order, renter.ID)
|
||||
|
||||
if err := repo.AdminPlatformCheckoutDispute(t.Context(), adminID, order.ID, AdminActionRequest{Reason: "租客与客服对结账金额有异议"}, AuditMeta{}); err != nil {
|
||||
t.Fatalf("AdminPlatformCheckoutDispute() error = %v", err)
|
||||
}
|
||||
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("load order failed: %v", err)
|
||||
}
|
||||
if saved.Status != orderStatusCheckoutDisputing || saved.HandoffStatus != handoffStatusCheckoutDisputed || saved.SettlementStatus != settlementStatusDisputed {
|
||||
t.Fatalf("status = %s/%s/%s, want checkout_disputing/checkout_disputed/disputed", saved.Status, saved.HandoffStatus, saved.SettlementStatus)
|
||||
}
|
||||
var savedCheckout model.OrderCheckout
|
||||
if err := db.First(&savedCheckout, checkout.ID).Error; err != nil {
|
||||
t.Fatalf("load checkout failed: %v", err)
|
||||
}
|
||||
if savedCheckout.Status != checkoutStatusDisputed {
|
||||
t.Fatalf("checkout status = %s, want disputed", savedCheckout.Status)
|
||||
}
|
||||
var dispute model.Dispute
|
||||
if err := db.Where("order_id = ?", order.ID).First(&dispute).Error; err != nil {
|
||||
t.Fatalf("load dispute failed: %v", err)
|
||||
}
|
||||
if dispute.InitiatorID != adminID || dispute.TargetUserID != renter.ID || dispute.Type != "checkout_dispute" {
|
||||
t.Fatalf("dispute = %#v, want admin initiated checkout dispute to renter", dispute)
|
||||
}
|
||||
if dispute.CheckoutID == nil || *dispute.CheckoutID != checkout.ID || dispute.PreviousCheckoutStatus != checkoutStatusSubmitted {
|
||||
t.Fatalf("dispute checkout snapshot = %#v/%s, want checkout %d submitted", dispute.CheckoutID, dispute.PreviousCheckoutStatus, checkout.ID)
|
||||
}
|
||||
dto, err := repo.FindAdmin(t.Context(), order.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("FindAdmin() error = %v", err)
|
||||
}
|
||||
if dto.ActiveDispute == nil || dto.ActiveDispute.ID != dispute.ID {
|
||||
t.Fatalf("active dispute = %#v, want dispute %d", dto.ActiveDispute, dispute.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func createPlatformManagedCheckoutOrder(t *testing.T, db *gorm.DB, adminID uint64) (model.User, model.User, model.RentalOrder) {
|
||||
t.Helper()
|
||||
owner := model.User{Phone: "admin:checkout"}
|
||||
renter := model.User{Phone: "13900004002"}
|
||||
if err := db.Create(&owner).Error; err != nil {
|
||||
t.Fatalf("create owner failed: %v", err)
|
||||
}
|
||||
if err := db.Create(&renter).Error; err != nil {
|
||||
t.Fatalf("create renter failed: %v", err)
|
||||
}
|
||||
account := model.GameAccount{
|
||||
OwnerID: owner.ID,
|
||||
Status: accountStatusRented,
|
||||
ServerRegion: "国服",
|
||||
LoginPlatform: "steam",
|
||||
Title: "平台代管结账账号",
|
||||
}
|
||||
if err := db.Create(&account).Error; err != nil {
|
||||
t.Fatalf("create account failed: %v", err)
|
||||
}
|
||||
listing := model.RentalListing{
|
||||
ListingNo: "LST-PM-CHECKOUT",
|
||||
OwnerID: owner.ID,
|
||||
AccountID: account.ID,
|
||||
Status: listingStatusRented,
|
||||
ReviewStatus: listingReviewStatusApproved,
|
||||
InTransaction: true,
|
||||
PriceCent: 10000,
|
||||
HandoffMode: handoffModePlatform,
|
||||
SettlementMode: settlementModePlatformManaged,
|
||||
ManagedAdminID: &adminID,
|
||||
}
|
||||
if err := db.Create(&listing).Error; err != nil {
|
||||
t.Fatalf("create listing failed: %v", err)
|
||||
}
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-PM-CHECKOUT",
|
||||
ListingID: listing.ID,
|
||||
AccountID: account.ID,
|
||||
OwnerID: owner.ID,
|
||||
RenterID: renter.ID,
|
||||
Status: orderStatusPendingCheckoutConfirm,
|
||||
HandoffStatus: handoffStatusPendingOwnerCheckout,
|
||||
HandoffMode: handoffModePlatform,
|
||||
SettlementMode: settlementModePlatformManaged,
|
||||
ManagedAdminID: &adminID,
|
||||
RentAmountCent: 10000,
|
||||
OwnerRentAmountCent: 8000,
|
||||
PlatformFeeCent: 2000,
|
||||
DepositAmountCent: 1000,
|
||||
EstimatedDurationHours: 24,
|
||||
OfflineSettlementStatus: offlineSettlementStatusNone,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{
|
||||
"haf_coin_amount":1000000,
|
||||
"asset_summary":{"price_breakdown":{"buyer_coin_base_price":100,"seller_coin_base_price":80,"consumable_price":0}}
|
||||
}`)),
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatalf("create order failed: %v", err)
|
||||
}
|
||||
return owner, renter, order
|
||||
}
|
||||
|
||||
func createOpenPlatformCheckout(t *testing.T, db *gorm.DB, order model.RentalOrder, renterID uint64) model.OrderCheckout {
|
||||
t.Helper()
|
||||
checkout, err := buildCheckout(order, renterID, checkoutStatusSubmitted, "租客申请结账", nil, 0, 1, 0, 0, true)
|
||||
if err != nil {
|
||||
t.Fatalf("build checkout failed: %v", err)
|
||||
}
|
||||
checkout.RoundCount = 1
|
||||
checkout.Turn = checkoutTurnOwner
|
||||
checkout.ProposedBy = renterID
|
||||
if err := db.Create(&checkout).Error; err != nil {
|
||||
t.Fatalf("create checkout failed: %v", err)
|
||||
}
|
||||
return checkout
|
||||
}
|
||||
|
||||
@@ -233,6 +233,42 @@ func TestCalculateCheckoutSettlementUsesExplicitRatiosForActualConsume(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateCheckoutSettlementCapsFullUseRatioRounding(t *testing.T) {
|
||||
// 发布量刚好用完时,比例换算的四舍五入不能把线下待打款抬高。
|
||||
order := model.RentalOrder{
|
||||
RentAmountCent: 37210,
|
||||
OwnerRentAmountCent: 34000,
|
||||
PlatformFeeCent: 3210,
|
||||
DepositAmountCent: 50000,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{
|
||||
"haf_coin_amount": 160000000,
|
||||
"asset_summary": {
|
||||
"price_breakdown": {
|
||||
"buyer_ratio": 43,
|
||||
"seller_ratio": 47,
|
||||
"buyer_coin_base_price": 372.1,
|
||||
"seller_coin_base_price": 340,
|
||||
"consumable_price": 0
|
||||
}
|
||||
}
|
||||
}`)),
|
||||
}
|
||||
|
||||
settlement := calculateCheckoutSettlement(order, 0, 160, 0)
|
||||
if settlement.ActualRentAmountCent != order.RentAmountCent {
|
||||
t.Fatalf("ActualRentAmountCent = %d, want %d", settlement.ActualRentAmountCent, order.RentAmountCent)
|
||||
}
|
||||
if settlement.OwnerRentIncomeCent != order.OwnerRentAmountCent {
|
||||
t.Fatalf("OwnerRentIncomeCent = %d, want %d", settlement.OwnerRentIncomeCent, order.OwnerRentAmountCent)
|
||||
}
|
||||
if settlement.PlatformFeeCent != order.PlatformFeeCent {
|
||||
t.Fatalf("PlatformFeeCent = %d, want %d", settlement.PlatformFeeCent, order.PlatformFeeCent)
|
||||
}
|
||||
if settlement.OvershootAmountCent != 0 {
|
||||
t.Fatalf("OvershootAmountCent = %d, want 0", settlement.OvershootAmountCent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCalculateDepositWaiverUsesSharedRemainingQuota(t *testing.T) {
|
||||
paid, waived := calculateDepositWaiver(500, 300, 0)
|
||||
if paid != 200 || waived != 300 {
|
||||
|
||||
@@ -6,29 +6,31 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
ErrInvalidRentHours = errors.New("invalid rent hours")
|
||||
ErrListingUnavailable = errors.New("listing unavailable")
|
||||
ErrCannotRentOwnListing = errors.New("cannot rent own listing")
|
||||
ErrInsufficientBalance = errors.New("insufficient balance")
|
||||
ErrOrderCannotPay = errors.New("order cannot pay")
|
||||
ErrChannelPaymentRequired = errors.New("channel payment required")
|
||||
ErrOrderCannotCancel = errors.New("order cannot cancel")
|
||||
ErrOrderCannotHandoff = errors.New("order cannot handoff")
|
||||
ErrOrderCannotResetHandoff = errors.New("order cannot reset handoff")
|
||||
ErrOrderCannotReceive = errors.New("order cannot receive")
|
||||
ErrOrderCannotReturn = errors.New("order cannot return")
|
||||
ErrOrderCannotComplete = errors.New("order cannot complete")
|
||||
ErrCheckoutCannotSubmit = errors.New("checkout cannot submit")
|
||||
ErrCheckoutCannotConfirm = errors.New("checkout cannot confirm")
|
||||
ErrCheckoutCannotCounter = errors.New("checkout cannot counter")
|
||||
ErrCheckoutMaxRounds = errors.New("checkout max rounds reached")
|
||||
ErrCheckoutDepositShortfall = errors.New("checkout deposit shortfall")
|
||||
ErrInvalidCheckoutAmount = errors.New("invalid checkout amount")
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
ErrDepositCannotHold = errors.New("deposit cannot hold")
|
||||
ErrDepositNotHeld = errors.New("deposit not held")
|
||||
ErrDepositHoldAmountEmpty = errors.New("deposit hold amount empty")
|
||||
ErrDependencyUnavailable = errors.New("dependency unavailable")
|
||||
ErrInvalidRentHours = errors.New("invalid rent hours")
|
||||
ErrListingUnavailable = errors.New("listing unavailable")
|
||||
ErrCannotRentOwnListing = errors.New("cannot rent own listing")
|
||||
ErrInsufficientBalance = errors.New("insufficient balance")
|
||||
ErrOrderCannotPay = errors.New("order cannot pay")
|
||||
ErrChannelPaymentRequired = errors.New("channel payment required")
|
||||
ErrOrderCannotCancel = errors.New("order cannot cancel")
|
||||
ErrOrderCannotHandoff = errors.New("order cannot handoff")
|
||||
ErrOrderCannotResetHandoff = errors.New("order cannot reset handoff")
|
||||
ErrOrderCannotReceive = errors.New("order cannot receive")
|
||||
ErrOrderCannotReturn = errors.New("order cannot return")
|
||||
ErrOrderCannotComplete = errors.New("order cannot complete")
|
||||
ErrCheckoutCannotSubmit = errors.New("checkout cannot submit")
|
||||
ErrCheckoutCannotConfirm = errors.New("checkout cannot confirm")
|
||||
ErrCheckoutCannotCounter = errors.New("checkout cannot counter")
|
||||
ErrCheckoutMaxRounds = errors.New("checkout max rounds reached")
|
||||
ErrCheckoutDepositShortfall = errors.New("checkout deposit shortfall")
|
||||
ErrInvalidCheckoutAmount = errors.New("invalid checkout amount")
|
||||
ErrDisputeExists = errors.New("dispute already exists")
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
ErrDepositCannotHold = errors.New("deposit cannot hold")
|
||||
ErrDepositNotHeld = errors.New("deposit not held")
|
||||
ErrDepositHoldAmountEmpty = errors.New("deposit hold amount empty")
|
||||
ErrOfflineSettlementCannotMark = errors.New("offline settlement cannot mark")
|
||||
)
|
||||
|
||||
const internalOrderHours = 24
|
||||
@@ -221,6 +223,56 @@ func (s *Service) AdminResetHandoff(ctx context.Context, adminID uint64, orderID
|
||||
return s.repo.AdminResetHandoff(ctx, adminID, orderID, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) AdminPlatformHandoff(ctx context.Context, adminID uint64, orderID uint64, req PlatformHandoffRequest, meta AuditMeta) (*HandoffRecordDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 || req.Content == "" || req.Reason == "" {
|
||||
return nil, ErrOrderCannotHandoff
|
||||
}
|
||||
return s.repo.AdminPlatformHandoff(ctx, adminID, orderID, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) AdminPlatformCheckoutConfirm(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 || req.Reason == "" {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
return s.repo.AdminPlatformCheckoutConfirm(ctx, adminID, orderID, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) AdminPlatformCheckoutCounter(ctx context.Context, adminID uint64, orderID uint64, req CounterCheckoutRequest, meta AuditMeta) (*CheckoutDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 || req.Reason == "" {
|
||||
return nil, ErrCheckoutCannotCounter
|
||||
}
|
||||
return s.repo.AdminPlatformCheckoutCounter(ctx, adminID, orderID, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) AdminPlatformCheckoutDispute(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 || req.Reason == "" {
|
||||
return ErrCheckoutCannotCounter
|
||||
}
|
||||
return s.repo.AdminPlatformCheckoutDispute(ctx, adminID, orderID, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) AdminMarkOfflineSettlement(ctx context.Context, adminID uint64, orderID uint64, req OfflineSettlementRequest, meta AuditMeta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 {
|
||||
return ErrOfflineSettlementCannotMark
|
||||
}
|
||||
return s.repo.AdminMarkOfflineSettlement(ctx, adminID, orderID, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) AdminHoldDeposit(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
|
||||
@@ -576,6 +576,11 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.POST("/orders/:id/seal", requirePerm("order:close"), orderHandler.AdminSeal)
|
||||
adminRoutes.POST("/orders/:id/mark-abnormal", requirePerm("order:mark_abnormal"), orderHandler.AdminMarkAbnormal)
|
||||
adminRoutes.POST("/orders/:id/reset-handoff", requirePerm("order:mark_abnormal"), orderHandler.AdminResetHandoff)
|
||||
adminRoutes.POST("/orders/:id/platform-handoff", requirePerm("order:mark_abnormal"), orderHandler.AdminPlatformHandoff)
|
||||
adminRoutes.POST("/orders/:id/platform-checkout/confirm", requirePerm("order:mark_abnormal"), orderHandler.AdminPlatformCheckoutConfirm)
|
||||
adminRoutes.POST("/orders/:id/platform-checkout/counter", requirePerm("order:mark_abnormal"), orderHandler.AdminPlatformCheckoutCounter)
|
||||
adminRoutes.POST("/orders/:id/platform-checkout/dispute", requirePerm("order:mark_abnormal"), orderHandler.AdminPlatformCheckoutDispute)
|
||||
adminRoutes.POST("/orders/:id/offline-settlement", requirePerm("order:mark_abnormal"), orderHandler.AdminMarkOfflineSettlement)
|
||||
adminRoutes.POST("/orders/:id/refund", requirePerm("order:close"), orderHandler.AdminRefund)
|
||||
adminRoutes.GET("/orders/:id/refund-status", requirePerm("order:view"), orderHandler.AdminRefundStatus)
|
||||
adminRoutes.POST("/orders/:id/refund/approve", requirePerm("order:close"), orderHandler.AdminApproveRefund)
|
||||
|
||||
Reference in New Issue
Block a user