Files
hfb_sys/backend/internal/modules/dispute/mutation.go
T

346 lines
10 KiB
Go

package dispute
import (
"context"
"time"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/notification"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
const (
disputeInitiatorUser = "user"
disputeInitiatorAdmin = "admin"
disputeTargetOwner = "owner"
disputeTargetRenter = "renter"
)
func (r *Repository) Create(ctx context.Context, userID uint64, orderID uint64, req CreateRequest) (*DisputeDTO, error) {
var createdID 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 order.RenterID != userID && order.OwnerID != userID {
return ErrPermissionDenied
}
if order.Status == "completed" || order.Status == "cancelled" || order.Status == "closed" {
return ErrInvalidDispute
}
isCheckoutDispute := order.Status == "pending_checkout_confirm" || order.Status == "pending_checkout_accept"
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
}
targetID := order.OwnerID
if userID == order.OwnerID {
targetID = order.RenterID
}
evidence, err := marshalEvidence(req.EvidenceURLS)
if err != nil {
return err
}
row := model.Dispute{
OrderID: order.ID,
InitiatorID: userID,
InitiatorType: disputeInitiatorUser,
TargetUserID: targetID,
Type: disputeType(req.Type, isCheckoutDispute),
Status: "open",
Description: req.Description,
EvidenceURLS: evidence,
PreviousOrderStatus: order.Status,
PreviousHandoffStatus: order.HandoffStatus,
PreviousSettlementStatus: order.SettlementStatus,
}
var checkout model.OrderCheckout
if isCheckoutDispute {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_id = ? AND status IN ?", order.ID, []string{"submitted", "countered"}).
Order("id DESC").
First(&checkout).Error; err != nil {
return err
}
row.CheckoutID = &checkout.ID
row.PreviousCheckoutStatus = checkout.Status
}
if err := tx.Create(&row).Error; err != nil {
return err
}
if isCheckoutDispute {
now := time.Now()
order.Status = "checkout_disputing"
order.HandoffStatus = "checkout_disputed"
order.SettlementStatus = "disputed"
updates := map[string]any{
"status": "disputed",
"updated_at": now,
}
if userID == order.RenterID {
updates["renter_rejected_at"] = now
}
if err := tx.Model(&model.OrderCheckout{}).
Where("id = ?", checkout.ID).
Updates(updates).Error; err != nil {
return err
}
} else {
order.Status = "disputing"
}
recordType := "dispute_opened"
recordContent := "用户发起订单申诉:" + req.Description
if isCheckoutDispute {
recordType = "checkout_dispute_opened"
recordContent = "用户发起结账争议:" + req.Description
}
record := model.HandoffRecord{
OrderID: order.ID,
FromUserID: userID,
ToUserID: targetID,
Type: recordType,
Content: recordContent,
}
if err := tx.Create(&record).Error; err != nil {
return err
}
if err := tx.Save(&order).Error; err != nil {
return err
}
disputeID := row.ID
title := "订单进入申诉"
content := "对方已发起申诉,请等待客服仲裁或补充沟通记录。"
if isCheckoutDispute {
title = "订单进入结账争议"
content = "对方已发起结账争议,请等待客服仲裁或补充结账证据。"
}
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
})
if err != nil {
return nil, err
}
return r.FindForUser(ctx, userID, createdID)
}
func (r *Repository) AdminCreateByOrder(ctx context.Context, adminID uint64, orderID uint64, req AdminCreateRequest, meta AuditMeta) (*DisputeDTO, error) {
var createdID 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 order.Status == "completed" || order.Status == "cancelled" || order.Status == "closed" {
return ErrInvalidDispute
}
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
}
targetID, err := adminDisputeTargetUserID(order, req.TargetRole)
if err != nil {
return err
}
evidence, err := marshalEvidence(req.EvidenceURLS)
if err != nil {
return err
}
isCheckoutDispute := order.Status == "pending_checkout_confirm" || order.Status == "pending_checkout_accept"
adminIDCopy := adminID
row := model.Dispute{
OrderID: order.ID,
InitiatorID: 0,
InitiatorType: disputeInitiatorAdmin,
InitiatorAdminID: &adminIDCopy,
TargetUserID: targetID,
Type: disputeType(req.Type, isCheckoutDispute),
Status: "open",
Description: req.Description,
EvidenceURLS: evidence,
PreviousOrderStatus: order.Status,
PreviousHandoffStatus: order.HandoffStatus,
PreviousSettlementStatus: order.SettlementStatus,
}
var checkout model.OrderCheckout
if isCheckoutDispute {
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("order_id = ? AND status IN ?", order.ID, []string{"submitted", "countered"}).
Order("id DESC").
First(&checkout).Error; err != nil {
return err
}
row.CheckoutID = &checkout.ID
row.PreviousCheckoutStatus = checkout.Status
}
if err := tx.Create(&row).Error; err != nil {
return err
}
if isCheckoutDispute {
now := time.Now()
order.Status = "checkout_disputing"
order.HandoffStatus = "checkout_disputed"
order.SettlementStatus = "disputed"
if err := tx.Model(&model.OrderCheckout{}).
Where("id = ?", checkout.ID).
Updates(map[string]any{"status": "disputed", "updated_at": now}).Error; err != nil {
return err
}
} else {
order.Status = "disputing"
}
recordType := "admin_dispute_opened"
recordContent := "客服发起订单申诉:" + req.Description
if isCheckoutDispute {
recordType = "admin_checkout_dispute_opened"
recordContent = "客服发起结账争议:" + req.Description
}
record := model.HandoffRecord{
OrderID: order.ID,
FromUserID: adminID,
ToUserID: targetID,
Type: recordType,
Content: recordContent,
}
if err := tx.Create(&record).Error; err != nil {
return err
}
if err := tx.Save(&order).Error; err != nil {
return err
}
disputeID := row.ID
title := "客服已发起订单申诉"
content := "客服已将订单提交仲裁流程,请等待处理或补充沟通记录。"
if isCheckoutDispute {
title = "客服已发起结账争议"
content = "客服已将结账问题提交仲裁流程,请等待处理或补充结账证据。"
}
if err := appendAdminCreatedDisputeNotifications(tx, order, disputeID, title, content); err != nil {
return err
}
if err := appendAuditLog(tx, adminID, "dispute.admin_create", "dispute", row.ID, meta, map[string]any{
"dispute_id": row.ID,
"order_id": order.ID,
"order_no": order.OrderNo,
"type": row.Type,
"target_role": req.TargetRole,
"target_user_id": targetID,
"description": req.Description,
"previous_order_status": row.PreviousOrderStatus,
"previous_handoff_status": row.PreviousHandoffStatus,
"previous_settlement_status": row.PreviousSettlementStatus,
"after_order_status": order.Status,
"after_handoff_status": order.HandoffStatus,
"after_settlement_status": order.SettlementStatus,
}); err != nil {
return err
}
createdID = row.ID
return nil
})
if err != nil {
return nil, err
}
return r.FindAdminByID(ctx, createdID)
}
func disputeType(input string, isCheckoutDispute bool) string {
if isCheckoutDispute {
return "checkout_dispute"
}
return input
}
func effectiveInitiatorType(row model.Dispute) string {
if row.InitiatorType == disputeInitiatorAdmin {
return disputeInitiatorAdmin
}
return disputeInitiatorUser
}
func adminDisputeTargetUserID(order model.RentalOrder, role string) (uint64, error) {
switch role {
case disputeTargetOwner:
return order.OwnerID, nil
case disputeTargetRenter:
return order.RenterID, nil
default:
return 0, ErrInvalidDispute
}
}
func appendAdminCreatedDisputeNotifications(tx *gorm.DB, order model.RentalOrder, disputeID uint64, title string, content string) error {
if isPlatformManagedOrder(order) {
return notification.Append(tx, notification.Entry{
UserID: order.RenterID,
Type: "dispute",
Title: title,
Content: content,
BizType: "dispute",
BizID: &disputeID,
})
}
return notification.Append(tx,
notification.Entry{
UserID: order.RenterID,
Type: "dispute",
Title: title,
Content: content,
BizType: "dispute",
BizID: &disputeID,
},
notification.Entry{
UserID: order.OwnerID,
Type: "dispute",
Title: title,
Content: content,
BizType: "dispute",
BizID: &disputeID,
},
)
}