新增客服一键交接功能
This commit is contained in:
@@ -80,6 +80,7 @@ type OrderDTO struct {
|
||||
type AdminActionsDTO struct {
|
||||
ResetHandoff *AdminActionDTO `json:"reset_handoff,omitempty"`
|
||||
PlatformHandoff *AdminActionDTO `json:"platform_handoff,omitempty"`
|
||||
ForceHandoff *AdminActionDTO `json:"force_handoff,omitempty"`
|
||||
PlatformCheckoutConfirm *AdminActionDTO `json:"platform_checkout_confirm,omitempty"`
|
||||
PlatformCheckoutCounter *AdminActionDTO `json:"platform_checkout_counter,omitempty"`
|
||||
PlatformCheckoutDispute *AdminActionDTO `json:"platform_checkout_dispute,omitempty"`
|
||||
@@ -138,6 +139,12 @@ type PlatformHandoffRequest struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
|
||||
// ForceHandoffRequest 客服确认普通号主订单已完成线下交接。
|
||||
type ForceHandoffRequest struct {
|
||||
Content string `json:"content"`
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
|
||||
type OfflineSettlementRequest struct {
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func canAdminForceHandoff(order model.RentalOrder) bool {
|
||||
if isPlatformHandoffOrder(order) || isPlatformSettlementOrder(order) {
|
||||
return false
|
||||
}
|
||||
if order.Status == orderStatusPendingHandoff {
|
||||
return order.HandoffStatus == handoffStatusPendingOwner ||
|
||||
order.HandoffStatus == handoffStatusOwnerTimeout ||
|
||||
order.HandoffStatus == handoffStatusPendingRenterConfirm
|
||||
}
|
||||
return order.Status == orderStatusAbnormal && order.HandoffStatus == handoffStatusRenterConfirmTimeout
|
||||
}
|
||||
|
||||
func forceHandoffNeedsContent(order model.RentalOrder) bool {
|
||||
return order.HandoffStatus == handoffStatusPendingOwner || order.HandoffStatus == handoffStatusOwnerTimeout
|
||||
}
|
||||
|
||||
// AdminForceHandoff 由客服确认普通号主订单已完成线下交接,并立即开始租期。
|
||||
func (r *Repository) AdminForceHandoff(ctx context.Context, adminID uint64, orderID uint64, req ForceHandoffRequest, 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 !canAdminForceHandoff(order) || strings.TrimSpace(req.Reason) == "" {
|
||||
return ErrOrderCannotForceHandoff
|
||||
}
|
||||
if order.RefundStatus == refundStatusPending || order.RefundStatus == refundStatusPendingReview {
|
||||
return ErrOrderCannotForceHandoff
|
||||
}
|
||||
|
||||
var activeDisputeCount int64
|
||||
if err := tx.Model(&model.Dispute{}).
|
||||
Where("order_id = ? AND status IN ?", order.ID, []string{"open", "processing"}).
|
||||
Count(&activeDisputeCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if activeDisputeCount > 0 {
|
||||
return ErrOrderCannotForceHandoff
|
||||
}
|
||||
|
||||
needsContent := forceHandoffNeedsContent(order)
|
||||
content := strings.TrimSpace(req.Content)
|
||||
if needsContent && content == "" {
|
||||
return ErrOrderCannotForceHandoff
|
||||
}
|
||||
if !needsContent {
|
||||
var handoffCount int64
|
||||
if err := tx.Model(&model.HandoffRecord{}).
|
||||
Where("order_id = ? AND type = ?", order.ID, "owner_handoff").
|
||||
Count(&handoffCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if handoffCount == 0 {
|
||||
return ErrOrderCannotForceHandoff
|
||||
}
|
||||
content = "客服已确认双方完成交接,订单已进入使用中。"
|
||||
}
|
||||
|
||||
beforeOrderStatus := order.Status
|
||||
beforeHandoffStatus := order.HandoffStatus
|
||||
now := time.Now()
|
||||
record := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: adminID,
|
||||
ToUserID: order.RenterID,
|
||||
Type: "admin_force_handoff",
|
||||
Content: content,
|
||||
}
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
order.Status = orderStatusRenting
|
||||
order.HandoffStatus = handoffStatusReceived
|
||||
order.EstimatedDurationHours = estimateOrderDurationHours(order.AccountSnapshot)
|
||||
order.RentedAt = &now
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "order_admin",
|
||||
Title: "客服已确认交接",
|
||||
Content: "客服已确认双方完成交接,订单已进入使用中,租期已开始计算。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "order_admin",
|
||||
Title: "客服已确认交接",
|
||||
Content: "客服已确认双方完成交接,订单已进入使用中,租期已开始计算。",
|
||||
BizType: "order",
|
||||
BizID: &orderID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, adminID, "order.force_handoff", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"reason": strings.TrimSpace(req.Reason),
|
||||
"content_length": len(content),
|
||||
"before_order_status": beforeOrderStatus,
|
||||
"after_order_status": order.Status,
|
||||
"before_handoff_status": beforeHandoffStatus,
|
||||
"after_handoff_status": order.HandoffStatus,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(&order).Error
|
||||
})
|
||||
}
|
||||
@@ -99,6 +99,28 @@ func (h *Handler) AdminPlatformHandoff(c *gin.Context) {
|
||||
response.Created(c, record)
|
||||
}
|
||||
|
||||
func (h *Handler) AdminForceHandoff(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req ForceHandoffRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "操作原因不能为空")
|
||||
return
|
||||
}
|
||||
if err := h.service.AdminForceHandoff(c.Request.Context(), adminID, id, req, auditMeta(c)); err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"received": true})
|
||||
}
|
||||
|
||||
func (h *Handler) AdminPlatformCheckoutConfirm(c *gin.Context) {
|
||||
h.adminAction(c, h.service.AdminPlatformCheckoutConfirm, gin.H{"confirmed": true})
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ func writeOrderError(c *gin.Context, err error) {
|
||||
response.Error(c, http.StatusConflict, "order_cannot_cancel", "当前订单不能取消")
|
||||
case errors.Is(err, ErrOrderCannotHandoff):
|
||||
response.Error(c, http.StatusConflict, "order_cannot_handoff", "当前订单不能交接")
|
||||
case errors.Is(err, ErrOrderCannotForceHandoff):
|
||||
response.Error(c, http.StatusConflict, "order_cannot_force_handoff", "当前订单不能客服一键交接")
|
||||
case errors.Is(err, ErrOrderCannotResetHandoff):
|
||||
response.Error(c, http.StatusConflict, "order_cannot_reset_handoff", "仅超时卡住的订单可重置到对应待办阶段")
|
||||
case errors.Is(err, ErrOrderCannotReceive):
|
||||
|
||||
@@ -23,6 +23,12 @@ func adminActionsForOrder(order model.RentalOrder) *AdminActionsDTO {
|
||||
Label: "客服代交接",
|
||||
}
|
||||
}
|
||||
if canAdminForceHandoff(order) {
|
||||
actions.ForceHandoff = &AdminActionDTO{
|
||||
Enabled: true,
|
||||
Label: "客服一键交接",
|
||||
}
|
||||
}
|
||||
if canAdminPlatformCheckoutConfirm(order) {
|
||||
actions.PlatformCheckoutConfirm = &AdminActionDTO{
|
||||
Enabled: true,
|
||||
@@ -43,7 +49,7 @@ func adminActionsForOrder(order model.RentalOrder) *AdminActionsDTO {
|
||||
Label: "确认线下结算",
|
||||
}
|
||||
}
|
||||
if actions.ResetHandoff == nil && actions.PlatformHandoff == nil &&
|
||||
if actions.ResetHandoff == nil && actions.PlatformHandoff == nil && actions.ForceHandoff == nil &&
|
||||
actions.PlatformCheckoutConfirm == nil && actions.PlatformCheckoutCounter == nil &&
|
||||
actions.PlatformCheckoutDispute == nil && actions.PlatformOfflineSettlement == nil {
|
||||
return nil
|
||||
|
||||
@@ -1011,6 +1011,155 @@ func TestAdminResetRenterConfirmTimeoutRefreshesStageTime(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminForceHandoffStartsNormalOwnerOrder(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
if err := db.AutoMigrate(&model.Dispute{}); err != nil {
|
||||
t.Fatalf("migrate disputes failed: %v", err)
|
||||
}
|
||||
repo := NewRepository(db)
|
||||
|
||||
owner := model.User{Phone: "13800003006"}
|
||||
renter := model.User{Phone: "13900003006"}
|
||||
if err := db.Create(&owner).Error; err != nil {
|
||||
t.Fatalf("create owner failed: %v", err)
|
||||
}
|
||||
if err := db.Create(&renter).Error; err != nil {
|
||||
t.Fatalf("create renter failed: %v", err)
|
||||
}
|
||||
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-FORCE-HANDOFF-001",
|
||||
ListingID: 1,
|
||||
AccountID: 1,
|
||||
OwnerID: owner.ID,
|
||||
RenterID: renter.ID,
|
||||
Status: orderStatusPendingHandoff,
|
||||
HandoffStatus: handoffStatusPendingOwner,
|
||||
HandoffMode: handoffModeOwner,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{
|
||||
"haf_coin_amount": 100000000,
|
||||
"daily_loss_m": 50
|
||||
}`)),
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatalf("create order failed: %v", err)
|
||||
}
|
||||
|
||||
err := repo.AdminForceHandoff(t.Context(), 99, order.ID, ForceHandoffRequest{
|
||||
Content: "账号已通过群聊完成交接",
|
||||
Reason: "已核实双方均可正常使用",
|
||||
}, AuditMeta{})
|
||||
if err != nil {
|
||||
t.Fatalf("AdminForceHandoff() error = %v", err)
|
||||
}
|
||||
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("load order failed: %v", err)
|
||||
}
|
||||
if saved.Status != orderStatusRenting || saved.HandoffStatus != handoffStatusReceived {
|
||||
t.Fatalf("status = %s/%s, want renting/received", saved.Status, saved.HandoffStatus)
|
||||
}
|
||||
if saved.RentedAt == nil || saved.EstimatedDurationHours != 48 {
|
||||
t.Fatalf("rented at/duration = %#v/%d, want set/48", saved.RentedAt, saved.EstimatedDurationHours)
|
||||
}
|
||||
var record model.HandoffRecord
|
||||
if err := db.Where("order_id = ? AND type = ?", order.ID, "admin_force_handoff").First(&record).Error; err != nil {
|
||||
t.Fatalf("find force handoff record failed: %v", err)
|
||||
}
|
||||
if record.Content != "账号已通过群聊完成交接" {
|
||||
t.Fatalf("record content = %q", record.Content)
|
||||
}
|
||||
var notificationCount int64
|
||||
if err := db.Model(&model.Notification{}).Where("biz_type = ? AND biz_id = ?", "order", order.ID).Count(¬ificationCount).Error; err != nil {
|
||||
t.Fatalf("count notifications failed: %v", err)
|
||||
}
|
||||
if notificationCount != 2 {
|
||||
t.Fatalf("notification count = %d, want 2", notificationCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminForceHandoffUsesExistingOwnerRecordAndRejectsPlatformOrder(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
if err := db.AutoMigrate(&model.Dispute{}); err != nil {
|
||||
t.Fatalf("migrate disputes failed: %v", err)
|
||||
}
|
||||
repo := NewRepository(db)
|
||||
|
||||
owner := model.User{Phone: "13800003007"}
|
||||
renter := model.User{Phone: "13900003007"}
|
||||
if err := db.Create(&owner).Error; err != nil {
|
||||
t.Fatalf("create owner failed: %v", err)
|
||||
}
|
||||
if err := db.Create(&renter).Error; err != nil {
|
||||
t.Fatalf("create renter failed: %v", err)
|
||||
}
|
||||
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-FORCE-HANDOFF-002",
|
||||
ListingID: 2,
|
||||
AccountID: 2,
|
||||
OwnerID: owner.ID,
|
||||
RenterID: renter.ID,
|
||||
Status: orderStatusAbnormal,
|
||||
HandoffStatus: handoffStatusRenterConfirmTimeout,
|
||||
HandoffMode: handoffModeOwner,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatalf("create order failed: %v", err)
|
||||
}
|
||||
ownerRecord := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: owner.ID,
|
||||
ToUserID: renter.ID,
|
||||
Type: "owner_handoff",
|
||||
Content: "号主已提交交接说明",
|
||||
}
|
||||
if err := db.Create(&ownerRecord).Error; err != nil {
|
||||
t.Fatalf("create owner handoff record failed: %v", err)
|
||||
}
|
||||
|
||||
err := repo.AdminForceHandoff(t.Context(), 99, order.ID, ForceHandoffRequest{
|
||||
Reason: "已联系双方确认完成交接",
|
||||
}, AuditMeta{})
|
||||
if err != nil {
|
||||
t.Fatalf("AdminForceHandoff() error = %v", err)
|
||||
}
|
||||
if err := db.First(&order, order.ID).Error; err != nil {
|
||||
t.Fatalf("load order failed: %v", err)
|
||||
}
|
||||
if order.Status != orderStatusRenting || order.HandoffStatus != handoffStatusReceived {
|
||||
t.Fatalf("status = %s/%s, want renting/received", order.Status, order.HandoffStatus)
|
||||
}
|
||||
var savedOwnerRecord model.HandoffRecord
|
||||
if err := db.First(&savedOwnerRecord, ownerRecord.ID).Error; err != nil {
|
||||
t.Fatalf("load owner handoff record failed: %v", err)
|
||||
}
|
||||
if savedOwnerRecord.ConfirmedByRenterAt != nil {
|
||||
t.Fatal("owner handoff record must not be marked as renter confirmed")
|
||||
}
|
||||
|
||||
platformOrder := model.RentalOrder{
|
||||
OrderNo: "ORD-FORCE-HANDOFF-003",
|
||||
ListingID: 3,
|
||||
AccountID: 3,
|
||||
OwnerID: owner.ID,
|
||||
RenterID: renter.ID,
|
||||
Status: orderStatusPendingHandoff,
|
||||
HandoffStatus: handoffStatusPendingOwner,
|
||||
HandoffMode: handoffModePlatform,
|
||||
}
|
||||
if err := db.Create(&platformOrder).Error; err != nil {
|
||||
t.Fatalf("create platform order failed: %v", err)
|
||||
}
|
||||
if err := repo.AdminForceHandoff(t.Context(), 99, platformOrder.ID, ForceHandoffRequest{
|
||||
Content: "不应允许",
|
||||
Reason: "测试",
|
||||
}, AuditMeta{}); err != ErrOrderCannotForceHandoff {
|
||||
t.Fatalf("platform order error = %v, want %v", err, ErrOrderCannotForceHandoff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitCheckoutRefreshesStageTime(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
|
||||
@@ -15,6 +15,7 @@ var (
|
||||
ErrChannelPaymentRequired = errors.New("channel payment required")
|
||||
ErrOrderCannotCancel = errors.New("order cannot cancel")
|
||||
ErrOrderCannotHandoff = errors.New("order cannot handoff")
|
||||
ErrOrderCannotForceHandoff = errors.New("order cannot force handoff")
|
||||
ErrOrderCannotResetHandoff = errors.New("order cannot reset handoff")
|
||||
ErrOrderCannotReceive = errors.New("order cannot receive")
|
||||
ErrOrderCannotReturn = errors.New("order cannot return")
|
||||
@@ -233,6 +234,16 @@ func (s *Service) AdminPlatformHandoff(ctx context.Context, adminID uint64, orde
|
||||
return s.repo.AdminPlatformHandoff(ctx, adminID, orderID, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) AdminForceHandoff(ctx context.Context, adminID uint64, orderID uint64, req ForceHandoffRequest, meta AuditMeta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
}
|
||||
if orderID == 0 || req.Reason == "" {
|
||||
return ErrOrderCannotForceHandoff
|
||||
}
|
||||
return s.repo.AdminForceHandoff(ctx, adminID, orderID, req, meta)
|
||||
}
|
||||
|
||||
func (s *Service) AdminPlatformCheckoutConfirm(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
|
||||
@@ -578,6 +578,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
||||
adminRoutes.POST("/orders/:id/mark-abnormal", requirePerm("order:mark_abnormal"), orderHandler.AdminMarkAbnormal)
|
||||
adminRoutes.POST("/orders/:id/reset-handoff", requirePerm("order:mark_abnormal"), orderHandler.AdminResetHandoff)
|
||||
adminRoutes.POST("/orders/:id/platform-handoff", requirePerm("order:mark_abnormal"), orderHandler.AdminPlatformHandoff)
|
||||
adminRoutes.POST("/orders/:id/force-handoff", requirePerm("order:force_handoff"), orderHandler.AdminForceHandoff)
|
||||
adminRoutes.POST("/orders/:id/platform-checkout/confirm", requirePerm("order:mark_abnormal"), orderHandler.AdminPlatformCheckoutConfirm)
|
||||
adminRoutes.POST("/orders/:id/platform-checkout/counter", requirePerm("order:mark_abnormal"), orderHandler.AdminPlatformCheckoutCounter)
|
||||
adminRoutes.POST("/orders/:id/platform-checkout/dispute", requirePerm("order:mark_abnormal"), orderHandler.AdminPlatformCheckoutDispute)
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
-- +goose Up
|
||||
|
||||
INSERT INTO permissions (code, name, resource, action)
|
||||
VALUES ('order:force_handoff', '客服一键交接', 'order', 'force_handoff')
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
resource = VALUES(resource),
|
||||
action = VALUES(action);
|
||||
|
||||
INSERT IGNORE INTO role_permissions (role_id, permission_id)
|
||||
SELECT r.id, p.id
|
||||
FROM roles r
|
||||
JOIN permissions p ON p.code = 'order:force_handoff'
|
||||
WHERE r.code IN ('cs', 'ops');
|
||||
|
||||
-- +goose Down
|
||||
|
||||
DELETE rp
|
||||
FROM role_permissions rp
|
||||
JOIN permissions p ON p.id = rp.permission_id
|
||||
WHERE p.code = 'order:force_handoff';
|
||||
|
||||
DELETE FROM permissions WHERE code = 'order:force_handoff';
|
||||
@@ -6,6 +6,7 @@ import { useRoute } from 'vue-router'
|
||||
|
||||
import {
|
||||
adminCloseOrder,
|
||||
adminForceHandoff,
|
||||
adminHoldDeposit,
|
||||
adminMarkOfflineSettlement,
|
||||
adminMarkOrderAbnormal,
|
||||
@@ -66,6 +67,9 @@ const refundStatus = ref<RefundStatus | null>(null)
|
||||
const platformHandoffVisible = ref(false)
|
||||
const platformHandoffContent = ref('')
|
||||
const platformHandoffReason = ref('')
|
||||
const forceHandoffVisible = ref(false)
|
||||
const forceHandoffContent = ref('')
|
||||
const forceHandoffReason = ref('')
|
||||
const platformCheckoutCounterVisible = ref(false)
|
||||
const platformCheckoutCounterForm = ref({
|
||||
consumableAmountYuan: 0,
|
||||
@@ -126,6 +130,7 @@ const canOperate = computed(
|
||||
)
|
||||
const resetAction = computed(() => order.value?.admin_actions?.reset_handoff)
|
||||
const platformHandoffAction = computed(() => order.value?.admin_actions?.platform_handoff)
|
||||
const forceHandoffAction = computed(() => order.value?.admin_actions?.force_handoff)
|
||||
const platformCheckoutConfirmAction = computed(
|
||||
() => order.value?.admin_actions?.platform_checkout_confirm
|
||||
)
|
||||
@@ -143,6 +148,12 @@ const resetActionLabel = computed(() => {
|
||||
})
|
||||
const canResetHandoff = computed(() => resetAction.value?.enabled === true)
|
||||
const canPlatformHandoff = computed(() => platformHandoffAction.value?.enabled === true)
|
||||
const canForceHandoff = computed(() => forceHandoffAction.value?.enabled === true)
|
||||
const forceHandoffNeedsContent = computed(
|
||||
() =>
|
||||
order.value?.handoff_status === 'pending_owner' ||
|
||||
order.value?.handoff_status === 'owner_timeout'
|
||||
)
|
||||
const canPlatformCheckoutConfirm = computed(
|
||||
() => platformCheckoutConfirmAction.value?.enabled === true
|
||||
)
|
||||
@@ -498,6 +509,39 @@ async function submitPlatformHandoff() {
|
||||
}
|
||||
}
|
||||
|
||||
function openForceHandoff() {
|
||||
forceHandoffContent.value = ''
|
||||
forceHandoffReason.value = ''
|
||||
forceHandoffVisible.value = true
|
||||
}
|
||||
|
||||
async function submitForceHandoff() {
|
||||
if (!order.value) return
|
||||
if (!forceHandoffReason.value.trim()) {
|
||||
ElMessage.warning('请填写客服操作原因')
|
||||
return
|
||||
}
|
||||
if (forceHandoffNeedsContent.value && !forceHandoffContent.value.trim()) {
|
||||
ElMessage.warning('当前尚无交接说明,请填写交接内容')
|
||||
return
|
||||
}
|
||||
platformSubmitting.value = true
|
||||
try {
|
||||
await adminForceHandoff(
|
||||
order.value.id,
|
||||
forceHandoffContent.value.trim(),
|
||||
forceHandoffReason.value.trim()
|
||||
)
|
||||
ElMessage.success('客服已确认交接,订单进入使用中')
|
||||
forceHandoffVisible.value = false
|
||||
await loadOrder()
|
||||
} catch (error) {
|
||||
ElMessage.error(readError(error, '客服一键交接失败'))
|
||||
} finally {
|
||||
platformSubmitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function openPlatformCheckoutCounter() {
|
||||
const checkout = order.value?.checkout
|
||||
platformCheckoutCounterForm.value = {
|
||||
@@ -880,6 +924,9 @@ function paymentPaidAt(record: AdminPayment) {
|
||||
>
|
||||
{{ platformHandoffAction?.label || '客服代交接' }}
|
||||
</el-button>
|
||||
<el-button v-if="canForceHandoff" type="warning" @click="openForceHandoff">
|
||||
{{ forceHandoffAction?.label || '客服一键交接' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="canPlatformCheckoutConfirm"
|
||||
type="primary"
|
||||
@@ -1433,6 +1480,39 @@ function paymentPaidAt(record: AdminPayment) {
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="forceHandoffVisible" title="客服一键交接" width="560px" destroy-on-close>
|
||||
<div v-if="order" class="dialog-body">
|
||||
<p>
|
||||
<strong>{{ order.order_no }}</strong> · 商品编号 {{ listingCode }} · {{ order.title }}
|
||||
</p>
|
||||
<el-alert
|
||||
title="确认后将跳过号主交接与租客确认步骤,订单会立即进入使用中并开始计算租期。"
|
||||
type="warning"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
<el-input
|
||||
v-if="forceHandoffNeedsContent"
|
||||
v-model="forceHandoffContent"
|
||||
type="textarea"
|
||||
:rows="5"
|
||||
placeholder="填写可供租客查看的交接说明"
|
||||
/>
|
||||
<el-input
|
||||
v-model="forceHandoffReason"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
placeholder="填写客服确认双方已完成交接的原因"
|
||||
/>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="forceHandoffVisible = false">取消</el-button>
|
||||
<el-button type="warning" :loading="platformSubmitting" @click="submitForceHandoff">
|
||||
确认一键交接
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="adminDisputeVisible" title="发起订单申诉" width="620px" destroy-on-close>
|
||||
<div v-if="order" class="dialog-body">
|
||||
<p>
|
||||
|
||||
@@ -78,6 +78,7 @@ export interface Order {
|
||||
export interface AdminActions {
|
||||
reset_handoff?: AdminAction
|
||||
platform_handoff?: AdminAction
|
||||
force_handoff?: AdminAction
|
||||
platform_checkout_confirm?: AdminAction
|
||||
platform_checkout_counter?: AdminAction
|
||||
platform_checkout_dispute?: AdminAction
|
||||
@@ -418,6 +419,14 @@ export async function adminPlatformHandoff(id: number, content: string, reason:
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function adminForceHandoff(id: number, content: string, reason: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<{ received: boolean }>>(
|
||||
`/admin/orders/${id}/force-handoff`,
|
||||
{ content, reason }
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
|
||||
export async function adminPlatformCheckoutConfirm(id: number, reason: string) {
|
||||
const { data } = await apiClient.post<ApiResponse<{ confirmed: boolean }>>(
|
||||
`/admin/orders/${id}/platform-checkout/confirm`,
|
||||
|
||||
@@ -242,6 +242,7 @@ export function formatHandoffRecordType(type: string) {
|
||||
const typeMap: Record<string, string> = {
|
||||
owner_handoff: '卖家交接',
|
||||
platform_handoff: '客服代交接',
|
||||
admin_force_handoff: '客服一键交接',
|
||||
renter_checkout: '买家结账',
|
||||
owner_counter_checkout: '卖家反驳结账',
|
||||
platform_checkout_counter: '客服修改结账',
|
||||
|
||||
Reference in New Issue
Block a user