支持客服主动发起申诉
This commit is contained in:
@@ -10,6 +10,8 @@ type Dispute struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
OrderID uint64 `gorm:"not null;index" json:"order_id"`
|
||||
InitiatorID uint64 `gorm:"not null" json:"initiator_id"`
|
||||
InitiatorType string `gorm:"size:16;not null;default:'user';index" json:"initiator_type"`
|
||||
InitiatorAdminID *uint64 `gorm:"index" json:"initiator_admin_id"`
|
||||
TargetUserID uint64 `gorm:"not null" json:"target_user_id"`
|
||||
Type string `gorm:"size:32;not null" json:"type"`
|
||||
Status string `gorm:"size:32;not null;default:'open'" json:"status"`
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func (r *Repository) CancelByOrder(ctx context.Context, userID uint64, orderID uint64) (*DisputeDTO, error) {
|
||||
var row model.Dispute
|
||||
if err := r.db.WithContext(ctx).
|
||||
Where("order_id = ? AND initiator_id = ? AND status IN ?", orderID, userID, []string{"open", "processing"}).
|
||||
Where("order_id = ? AND initiator_type = ? AND initiator_id = ? AND status IN ?", orderID, disputeInitiatorUser, userID, []string{"open", "processing"}).
|
||||
Order("id DESC").
|
||||
First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -28,7 +28,7 @@ func (r *Repository) Cancel(ctx context.Context, userID uint64, id uint64) (*Dis
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if row.InitiatorID != userID {
|
||||
if effectiveInitiatorType(row) != disputeInitiatorUser || row.InitiatorID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if row.Status != "open" && row.Status != "processing" {
|
||||
|
||||
@@ -22,6 +22,8 @@ type DisputeDTO struct {
|
||||
OwnerPhone string `json:"owner_phone"`
|
||||
RenterPhone string `json:"renter_phone"`
|
||||
InitiatorID uint64 `json:"initiator_id"`
|
||||
InitiatorType string `json:"initiator_type"`
|
||||
InitiatorAdminID *uint64 `json:"initiator_admin_id,omitempty"`
|
||||
TargetUserID uint64 `json:"target_user_id"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
@@ -46,6 +48,13 @@ type CreateRequest struct {
|
||||
EvidenceURLS []string `json:"evidence_urls"`
|
||||
}
|
||||
|
||||
type AdminCreateRequest struct {
|
||||
Type string `json:"type" binding:"required"`
|
||||
Description string `json:"description" binding:"required"`
|
||||
TargetRole string `json:"target_role" binding:"required"`
|
||||
EvidenceURLS []string `json:"evidence_urls"`
|
||||
}
|
||||
|
||||
type ArbitrateRequest struct {
|
||||
Result string `json:"result" binding:"required"`
|
||||
Remark string `json:"remark" binding:"required"`
|
||||
|
||||
@@ -129,6 +129,29 @@ func (h *Handler) AdminList(c *gin.Context) {
|
||||
response.OK(c, result)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminCreateByOrder(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
orderID, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req AdminCreateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "申诉信息不完整")
|
||||
return
|
||||
}
|
||||
item, err := h.service.AdminCreateByOrder(c.Request.Context(), adminID, orderID, req, auditMeta(c))
|
||||
if err != nil {
|
||||
writeDisputeError(c, err)
|
||||
return
|
||||
}
|
||||
response.Created(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminArbitrate(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -11,6 +11,14 @@ import (
|
||||
"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 {
|
||||
@@ -46,6 +54,7 @@ func (r *Repository) Create(ctx context.Context, userID uint64, orderID uint64,
|
||||
row := model.Dispute{
|
||||
OrderID: order.ID,
|
||||
InitiatorID: userID,
|
||||
InitiatorType: disputeInitiatorUser,
|
||||
TargetUserID: targetID,
|
||||
Type: disputeType(req.Type, isCheckoutDispute),
|
||||
Status: "open",
|
||||
@@ -154,9 +163,183 @@ func (r *Repository) Create(ctx context.Context, userID uint64, orderID uint64,
|
||||
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,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ func (row disputeRow) toDTO() DisputeDTO {
|
||||
OwnerPhone: row.OwnerPhone,
|
||||
RenterPhone: row.RenterPhone,
|
||||
InitiatorID: row.InitiatorID,
|
||||
InitiatorType: effectiveInitiatorType(row.Dispute),
|
||||
InitiatorAdminID: row.InitiatorAdminID,
|
||||
TargetUserID: row.TargetUserID,
|
||||
Type: row.Type,
|
||||
Status: row.Status,
|
||||
|
||||
@@ -5,22 +5,18 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
db := r.db.WithContext(ctx)
|
||||
conditions := db.Model(&model.Dispute{}).Where("initiator_id = ? OR target_user_id = ?", userID, userID)
|
||||
conditions := r.userVisibleQuery(ctx, userID)
|
||||
var total int64
|
||||
if err := conditions.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
var rows []disputeRow
|
||||
err := r.baseQuery(ctx).
|
||||
Where("d.initiator_id = ? OR d.target_user_id = ?", userID, userID).
|
||||
err := applyUserVisibleFilter(r.baseQuery(ctx), userID).
|
||||
Order("d.id DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Scan(&rows).Error
|
||||
@@ -32,8 +28,7 @@ func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageS
|
||||
|
||||
func (r *Repository) FindForUser(ctx context.Context, userID uint64, id uint64) (*DisputeDTO, error) {
|
||||
var row disputeRow
|
||||
if err := r.baseQuery(ctx).
|
||||
Where("d.id = ? AND (d.initiator_id = ? OR d.target_user_id = ?)", id, userID, userID).
|
||||
if err := applyUserVisibleFilter(r.baseQuery(ctx).Where("d.id = ?", id), userID).
|
||||
First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -68,6 +63,31 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminListQuery) (*Pagi
|
||||
return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindAdminByID(ctx context.Context, id uint64) (*DisputeDTO, error) {
|
||||
var row disputeRow
|
||||
if err := r.baseQuery(ctx).Where("d.id = ?", id).First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := row.toDTO()
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) userVisibleQuery(ctx context.Context, userID uint64) *gorm.DB {
|
||||
return applyUserVisibleFilter(r.adminFilterQuery(ctx), userID)
|
||||
}
|
||||
|
||||
func applyUserVisibleFilter(db *gorm.DB, userID uint64) *gorm.DB {
|
||||
return db.Where(
|
||||
`((d.initiator_type = ? AND d.initiator_id = ?) OR d.target_user_id = ? OR (d.initiator_type = ? AND (o.owner_id = ? OR o.renter_id = ?)))`,
|
||||
disputeInitiatorUser,
|
||||
userID,
|
||||
userID,
|
||||
disputeInitiatorAdmin,
|
||||
userID,
|
||||
userID,
|
||||
)
|
||||
}
|
||||
|
||||
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
||||
return r.adminFilterQuery(ctx).
|
||||
Select(`d.*, o.order_no, o.status AS order_status, o.handoff_status, o.settlement_status,
|
||||
|
||||
@@ -169,6 +169,58 @@ func TestRepositoryCancelRestoresCheckoutDisputeStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminCreateByOrderCreatesAdminInitiatedDispute(t *testing.T) {
|
||||
db := setupDisputeTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
adminID := uint64(88)
|
||||
owner, renter, order := createDisputeOrderFixture(t, db, model.RentalOrder{
|
||||
Status: "renting",
|
||||
HandoffStatus: "received",
|
||||
SettlementStatus: "unsettled",
|
||||
})
|
||||
|
||||
created, err := repo.AdminCreateByOrder(t.Context(), adminID, order.ID, AdminCreateRequest{
|
||||
Type: "cannot_login",
|
||||
TargetRole: "owner",
|
||||
Description: "客服核查后主动发起申诉",
|
||||
}, AuditMeta{})
|
||||
if err != nil {
|
||||
t.Fatalf("后台发起申诉失败: %v", err)
|
||||
}
|
||||
if created.InitiatorType != disputeInitiatorAdmin || created.InitiatorAdminID == nil || *created.InitiatorAdminID != adminID {
|
||||
t.Fatalf("发起方 = %s/%v, want admin/%d", created.InitiatorType, created.InitiatorAdminID, adminID)
|
||||
}
|
||||
if created.InitiatorID != 0 || created.TargetUserID != owner.ID {
|
||||
t.Fatalf("initiator/target = %d/%d, want 0/%d", created.InitiatorID, created.TargetUserID, owner.ID)
|
||||
}
|
||||
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("读取订单失败: %v", err)
|
||||
}
|
||||
if saved.Status != "disputing" || saved.HandoffStatus != "received" || saved.SettlementStatus != "unsettled" {
|
||||
t.Fatalf("订单状态 = %s/%s/%s, want disputing/received/unsettled", saved.Status, saved.HandoffStatus, saved.SettlementStatus)
|
||||
}
|
||||
|
||||
if _, err := repo.FindForUser(t.Context(), renter.ID, created.ID); err != nil {
|
||||
t.Fatalf("租客应能看到客服发起的申诉: %v", err)
|
||||
}
|
||||
if _, err := repo.FindForUser(t.Context(), owner.ID, created.ID); err != nil {
|
||||
t.Fatalf("号主应能看到客服发起的申诉: %v", err)
|
||||
}
|
||||
if _, err := repo.CancelByOrder(t.Context(), renter.ID, order.ID); err == nil {
|
||||
t.Fatal("客服发起的申诉不应允许租客取消")
|
||||
}
|
||||
|
||||
var record model.HandoffRecord
|
||||
if err := db.Where("order_id = ? AND type = ?", order.ID, "admin_dispute_opened").First(&record).Error; err != nil {
|
||||
t.Fatalf("读取客服发起申诉记录失败: %v", err)
|
||||
}
|
||||
if record.FromUserID != adminID || record.ToUserID != owner.ID {
|
||||
t.Fatalf("record from/to = %d/%d, want %d/%d", record.FromUserID, record.ToUserID, adminID, owner.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformManagedArbitrationUsesOfflineSettlement(t *testing.T) {
|
||||
db := setupDisputeTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
|
||||
@@ -32,6 +32,16 @@ func (s *Service) Create(ctx context.Context, userID uint64, orderID uint64, req
|
||||
return s.repo.Create(ctx, userID, orderID, req)
|
||||
}
|
||||
|
||||
func (s *Service) AdminCreateByOrder(ctx context.Context, adminID uint64, orderID uint64, req AdminCreateRequest, meta AuditMeta) (*DisputeDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 || req.Type == "" || req.Description == "" || req.TargetRole == "" {
|
||||
return nil, ErrInvalidDispute
|
||||
}
|
||||
return s.repo.AdminCreateByOrder(ctx, adminID, orderID, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
|
||||
@@ -74,10 +74,12 @@ type AdminActionDTO struct {
|
||||
}
|
||||
|
||||
type ActiveDisputeDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
InitiatorID uint64 `json:"initiator_id"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
ID uint64 `json:"id"`
|
||||
InitiatorID uint64 `json:"initiator_id"`
|
||||
InitiatorType string `json:"initiator_type"`
|
||||
InitiatorAdminID *uint64 `json:"initiator_admin_id,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type CreateRequest struct {
|
||||
|
||||
@@ -336,7 +336,9 @@ func (r *Repository) AdminPlatformCheckoutDispute(ctx context.Context, adminID u
|
||||
beforeSettlementStatus := order.SettlementStatus
|
||||
row := model.Dispute{
|
||||
OrderID: order.ID,
|
||||
InitiatorID: adminID,
|
||||
InitiatorID: 0,
|
||||
InitiatorType: "admin",
|
||||
InitiatorAdminID: &adminID,
|
||||
TargetUserID: order.RenterID,
|
||||
Type: "checkout_dispute",
|
||||
Status: "open",
|
||||
|
||||
@@ -203,13 +203,22 @@ func (r *Repository) activeDisputeDTO(ctx context.Context, orderID uint64) *Acti
|
||||
return nil
|
||||
}
|
||||
return &ActiveDisputeDTO{
|
||||
ID: row.ID,
|
||||
InitiatorID: row.InitiatorID,
|
||||
Type: row.Type,
|
||||
Status: row.Status,
|
||||
ID: row.ID,
|
||||
InitiatorID: row.InitiatorID,
|
||||
InitiatorType: effectiveDisputeInitiatorType(row),
|
||||
InitiatorAdminID: row.InitiatorAdminID,
|
||||
Type: row.Type,
|
||||
Status: row.Status,
|
||||
}
|
||||
}
|
||||
|
||||
func effectiveDisputeInitiatorType(row model.Dispute) string {
|
||||
if row.InitiatorType == "admin" {
|
||||
return "admin"
|
||||
}
|
||||
return "user"
|
||||
}
|
||||
|
||||
func shouldAttachCheckout(status string) bool {
|
||||
switch status {
|
||||
case orderStatusPendingCheckoutConfirm, orderStatusPendingCheckoutAccept, orderStatusCheckoutDisputing, orderStatusCompleted:
|
||||
|
||||
@@ -1068,7 +1068,7 @@ func TestAdminPlatformCheckoutDisputeCreatesArbitrationCase(t *testing.T) {
|
||||
if err := db.Where("order_id = ?", order.ID).First(&dispute).Error; err != nil {
|
||||
t.Fatalf("load dispute failed: %v", err)
|
||||
}
|
||||
if dispute.InitiatorID != adminID || dispute.TargetUserID != renter.ID || dispute.Type != "checkout_dispute" {
|
||||
if dispute.InitiatorID != 0 || dispute.InitiatorType != "admin" || dispute.InitiatorAdminID == nil || *dispute.InitiatorAdminID != adminID || dispute.TargetUserID != renter.ID || dispute.Type != "checkout_dispute" {
|
||||
t.Fatalf("dispute = %#v, want admin initiated checkout dispute to renter", dispute)
|
||||
}
|
||||
if dispute.CheckoutID == nil || *dispute.CheckoutID != checkout.ID || dispute.PreviousCheckoutStatus != checkoutStatusSubmitted {
|
||||
|
||||
@@ -588,6 +588,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.POST("/orders/:id/deposit-hold", requirePerm("order:deposit_hold"), orderHandler.AdminHoldDeposit)
|
||||
adminRoutes.POST("/orders/:id/deposit-release", requirePerm("order:deposit_hold"), orderHandler.AdminReleaseDeposit)
|
||||
adminRoutes.GET("/orders/refund-pending", requirePerm("order:close"), orderHandler.ListPendingRefund)
|
||||
adminRoutes.POST("/orders/:id/dispute", requirePerm("dispute:arbitrate"), disputeHandler.AdminCreateByOrder)
|
||||
|
||||
// 管理员线下提号(独立于正常订单流程)
|
||||
adminRoutes.POST("/pickups", requirePerm("order:pickup"), pickupHandler.Create)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- +goose Up
|
||||
|
||||
ALTER TABLE disputes
|
||||
ADD COLUMN initiator_type VARCHAR(16) NOT NULL DEFAULT 'user' COMMENT '发起方类型: user用户/admin客服' AFTER initiator_id,
|
||||
ADD COLUMN initiator_admin_id BIGINT UNSIGNED NULL COMMENT '客服发起时的管理员ID' AFTER initiator_type,
|
||||
ADD KEY idx_disputes_initiator_type (initiator_type, initiator_id),
|
||||
ADD KEY idx_disputes_initiator_admin (initiator_admin_id);
|
||||
|
||||
UPDATE disputes
|
||||
SET initiator_type = 'user'
|
||||
WHERE initiator_type = '';
|
||||
|
||||
-- +goose Down
|
||||
|
||||
ALTER TABLE disputes
|
||||
DROP KEY idx_disputes_initiator_admin,
|
||||
DROP KEY idx_disputes_initiator_type,
|
||||
DROP COLUMN initiator_admin_id,
|
||||
DROP COLUMN initiator_type;
|
||||
Reference in New Issue
Block a user