346 lines
10 KiB
Go
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,
|
|
},
|
|
)
|
|
}
|