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