436 lines
15 KiB
Go
436 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: 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
|
|
})
|
|
}
|