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 || order.SettlementStatus == settlementStatusArbitrated) && 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: 0, InitiatorType: "admin", InitiatorAdminID: &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 }) }