Files

438 lines
15 KiB
Go

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: 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
})
}