修复平台代管订单流程
This commit is contained in:
@@ -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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user