feat: 客服封存订单功能(号主失联场景)
新增客服「封存订单」动作,用于号主失联、无法联系的场景:单事务内 完成关闭订单、全量原路退款、永久封存关联商品、解散发布群、双方通知 与审计日志。 - 新增 listing 终态 sealed(封存),区别于可恢复的 offline - listingLockedForOwnerMutation 锁定 sealed,号主无法再编辑/提审/上架 - 解散群聊按 listing_id 查发布群(listing_group)置 archived, 阻断所有成员发言并留系统消息 - 新增 order.AdminSeal 及 /admin/orders/:id/seal 路由 - 前端:卖家页 PC/移动端 sealed 视为终态禁用操作,客服后台新增 「封存订单」按钮与二次确认 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
8a11c2517e
commit
be5d322c10
@@ -22,3 +22,17 @@ func (a *ListingChatCreatorAdapter) EnsureListingConversation(tx *gorm.DB, listi
|
|||||||
}
|
}
|
||||||
return conv.ID, nil
|
return conv.ID, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OrderChatArchiverAdapter 适配器,实现 order.OrderChatArchiver 接口,
|
||||||
|
// 供订单封存时在同一事务内解散关联的发布群。
|
||||||
|
type OrderChatArchiverAdapter struct{}
|
||||||
|
|
||||||
|
func NewOrderChatArchiverAdapter() *OrderChatArchiverAdapter {
|
||||||
|
return &OrderChatArchiverAdapter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ArchiveListingConversation 将商品关联的发布群标记为已归档(解散),
|
||||||
|
// 写入一条系统消息提示群聊已被客服解散。会话不存在时静默跳过。
|
||||||
|
func (a *OrderChatArchiverAdapter) ArchiveListingConversation(tx *gorm.DB, listingID uint64, reason string) error {
|
||||||
|
return ArchiveListingConversation(tx, listingID, reason)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package chat
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"hfb_sys/backend/internal/model"
|
"hfb_sys/backend/internal/model"
|
||||||
@@ -366,3 +367,30 @@ func applyAdminChatStageFilter(db *gorm.DB, stage string, principal Principal) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ArchiveListingConversation 将商品关联的发布群标记为已归档(解散),并写入系统提示。
|
||||||
|
// 用于客服封存订单时同事务解散群聊;发布群不存在时静默跳过,不阻断封存流程。
|
||||||
|
// 归档后 SendMessage 的 status 校验会阻断所有成员继续发言。
|
||||||
|
func ArchiveListingConversation(tx *gorm.DB, listingID uint64, reason string) error {
|
||||||
|
var conversation model.ChatConversation
|
||||||
|
err := tx.Where("listing_id = ?", listingID).First(&conversation).Error
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if conversation.Status == "archived" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := tx.Model(&model.ChatConversation{}).
|
||||||
|
Where("id = ?", conversation.ID).
|
||||||
|
Update("status", "archived").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
content := "群聊已由客服解散,订单已封存。"
|
||||||
|
if trimmed := strings.TrimSpace(reason); trimmed != "" {
|
||||||
|
content += "原因:" + trimmed
|
||||||
|
}
|
||||||
|
return sendSystemMessage(tx, conversation.ID, content)
|
||||||
|
}
|
||||||
|
|||||||
@@ -370,5 +370,6 @@ func listingLockedForOwnerMutation(listing *model.RentalListing) bool {
|
|||||||
if listing == nil {
|
if listing == nil {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return listing.Status == "rented" || listing.Status == "completed" || listing.InTransaction
|
return listing.Status == "rented" || listing.Status == "completed" ||
|
||||||
|
listing.Status == "sealed" || listing.InTransaction
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -440,6 +440,100 @@ func (r *Repository) AdminRejectRefund(ctx context.Context, orderID uint64, acti
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminSeal 客服封存订单:关闭订单并全额原路退款,将关联商品/账号置为终态 sealed
|
||||||
|
// (号主不可再编辑、提审、上架),同时在同一事务内解散关联群聊。
|
||||||
|
// 适用于号主失联、无法继续交接,需要终止并永久封存该商品的场景。
|
||||||
|
func (r *Repository) AdminSeal(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||||
|
var refund *refundAction
|
||||||
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
assets, err := r.lockOrderAssets(tx, orderID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
order := assets.Order
|
||||||
|
listing := assets.Listing
|
||||||
|
account := assets.Account
|
||||||
|
if isTerminalStatus(order.Status) {
|
||||||
|
return ErrOrderCannotComplete
|
||||||
|
}
|
||||||
|
beforeOrderStatus := order.Status
|
||||||
|
beforeHandoffStatus := order.HandoffStatus
|
||||||
|
beforeSettlementStatus := order.SettlementStatus
|
||||||
|
beforeListingStatus := listing.Status
|
||||||
|
beforeAccountStatus := account.Status
|
||||||
|
now := time.Now()
|
||||||
|
order.Status = orderStatusClosed
|
||||||
|
order.HandoffStatus = handoffStatusAdminClosed
|
||||||
|
order.SettlementStatus = settlementStatusClosed
|
||||||
|
order.SettledAt = &now
|
||||||
|
sealAssets(listing, account)
|
||||||
|
if beforeOrderStatus != orderStatusPendingPayment {
|
||||||
|
totalCent := order.RentAmountCent + order.DepositAmountCent
|
||||||
|
action, err := r.prepareRefund(order, totalCent, refundBizAdminSeal, "客服封存订单原路退款")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
refund = action
|
||||||
|
}
|
||||||
|
if r.chatArchiver != nil {
|
||||||
|
if err := r.chatArchiver.ArchiveListingConversation(tx, listing.ID, req.Reason); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := notification.Append(tx,
|
||||||
|
notification.Entry{
|
||||||
|
UserID: order.RenterID,
|
||||||
|
Type: "order_admin",
|
||||||
|
Title: "订单已由客服封存",
|
||||||
|
Content: "客服已封存订单,退款将原路退回您的支付账户。原因:" + req.Reason,
|
||||||
|
BizType: "order",
|
||||||
|
BizID: &order.ID,
|
||||||
|
},
|
||||||
|
notification.Entry{
|
||||||
|
UserID: order.OwnerID,
|
||||||
|
Type: "order_admin",
|
||||||
|
Title: "订单已由客服封存",
|
||||||
|
Content: "客服已封存订单,关联商品已永久封存,不可再次编辑或上架。原因:" + req.Reason,
|
||||||
|
BizType: "order",
|
||||||
|
BizID: &order.ID,
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := appendAuditLog(tx, adminID, "order.admin_seal", "order", order.ID, meta, map[string]any{
|
||||||
|
"order_id": order.ID,
|
||||||
|
"order_no": order.OrderNo,
|
||||||
|
"listing_id": order.ListingID,
|
||||||
|
"account_id": order.AccountID,
|
||||||
|
"reason": req.Reason,
|
||||||
|
"before_order_status": beforeOrderStatus,
|
||||||
|
"after_order_status": order.Status,
|
||||||
|
"before_handoff_status": beforeHandoffStatus,
|
||||||
|
"after_handoff_status": order.HandoffStatus,
|
||||||
|
"before_settlement_status": beforeSettlementStatus,
|
||||||
|
"after_settlement_status": order.SettlementStatus,
|
||||||
|
"before_listing_status": beforeListingStatus,
|
||||||
|
"after_listing_status": listing.Status,
|
||||||
|
"before_account_status": beforeAccountStatus,
|
||||||
|
"after_account_status": account.Status,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Save(order).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.Save(listing).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Save(account).Error
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.startRefundBestEffort(ctx, refund)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func isTerminalStatus(status string) bool {
|
func isTerminalStatus(status string) bool {
|
||||||
return status == orderStatusCompleted || status == orderStatusCancelled || status == orderStatusClosed
|
return status == orderStatusCompleted || status == orderStatusCancelled || status == orderStatusClosed
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,13 @@ func archiveAssets(listing *model.RentalListing, account *model.GameAccount) {
|
|||||||
account.Status = accountStatusOffline
|
account.Status = accountStatusOffline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sealAssets(listing *model.RentalListing, account *model.GameAccount) {
|
||||||
|
listing.Status = listingStatusSealed
|
||||||
|
listing.InTransaction = false
|
||||||
|
listing.PublishedAt = nil
|
||||||
|
account.Status = accountStatusSealed
|
||||||
|
}
|
||||||
|
|
||||||
func completeAssets(listing *model.RentalListing, account *model.GameAccount) {
|
func completeAssets(listing *model.RentalListing, account *model.GameAccount) {
|
||||||
listing.Status = listingStatusCompleted
|
listing.Status = listingStatusCompleted
|
||||||
listing.InTransaction = false
|
listing.InTransaction = false
|
||||||
|
|||||||
@@ -50,17 +50,20 @@ const (
|
|||||||
listingStatusOffline = "offline"
|
listingStatusOffline = "offline"
|
||||||
listingStatusCompleted = "completed"
|
listingStatusCompleted = "completed"
|
||||||
listingStatusAbnormal = "abnormal"
|
listingStatusAbnormal = "abnormal"
|
||||||
|
listingStatusSealed = "sealed"
|
||||||
|
|
||||||
accountStatusPublished = "published"
|
accountStatusPublished = "published"
|
||||||
accountStatusRented = "rented"
|
accountStatusRented = "rented"
|
||||||
accountStatusOffline = "offline"
|
accountStatusOffline = "offline"
|
||||||
accountStatusAbnormal = "abnormal"
|
accountStatusAbnormal = "abnormal"
|
||||||
|
accountStatusSealed = "sealed"
|
||||||
|
|
||||||
walletBizOwnerIncome = "owner_income"
|
walletBizOwnerIncome = "owner_income"
|
||||||
walletBizDepositCompensation = "deposit_compensation"
|
walletBizDepositCompensation = "deposit_compensation"
|
||||||
|
|
||||||
refundBizCancel = "cancel_refund"
|
refundBizCancel = "cancel_refund"
|
||||||
refundBizAdminClose = "admin_close_refund"
|
refundBizAdminClose = "admin_close_refund"
|
||||||
|
refundBizAdminSeal = "admin_seal_refund"
|
||||||
refundBizAdmin = "admin_refund"
|
refundBizAdmin = "admin_refund"
|
||||||
refundBizCheckout = "checkout_refund"
|
refundBizCheckout = "checkout_refund"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ func (h *Handler) AdminClose(c *gin.Context) {
|
|||||||
h.adminAction(c, h.service.AdminClose, gin.H{"closed": true})
|
h.adminAction(c, h.service.AdminClose, gin.H{"closed": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) AdminSeal(c *gin.Context) {
|
||||||
|
h.adminAction(c, h.service.AdminSeal, gin.H{"sealed": true})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) AdminMarkAbnormal(c *gin.Context) {
|
func (h *Handler) AdminMarkAbnormal(c *gin.Context) {
|
||||||
h.adminAction(c, h.service.AdminMarkAbnormal, gin.H{"abnormal": true})
|
h.adminAction(c, h.service.AdminMarkAbnormal, gin.H{"abnormal": true})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,15 @@ type OrderChatNotifier interface {
|
|||||||
NotifyNewConversation(conversationID uint64)
|
NotifyNewConversation(conversationID uint64)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OrderChatArchiver 由 chat 模块适配实现,在订单封存时于同一事务内解散关联群聊。
|
||||||
|
// 实际聊天群按 listing 关联(发布群 listing_group),故传 listingID。
|
||||||
|
type OrderChatArchiver interface {
|
||||||
|
ArchiveListingConversation(tx *gorm.DB, listingID uint64, reason string) error
|
||||||
|
}
|
||||||
|
|
||||||
type Dependencies struct {
|
type Dependencies struct {
|
||||||
ChatNotifier OrderChatNotifier
|
ChatNotifier OrderChatNotifier
|
||||||
|
ChatArchiver OrderChatArchiver
|
||||||
RefundStarter RefundStarter
|
RefundStarter RefundStarter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +43,7 @@ type refundAction struct {
|
|||||||
type Repository struct {
|
type Repository struct {
|
||||||
db *gorm.DB
|
db *gorm.DB
|
||||||
chatNotifier OrderChatNotifier
|
chatNotifier OrderChatNotifier
|
||||||
|
chatArchiver OrderChatArchiver
|
||||||
refundStarter RefundStarter
|
refundStarter RefundStarter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +53,7 @@ func NewRepository(db *gorm.DB, deps ...Dependencies) *Repository {
|
|||||||
repo := &Repository{db: db}
|
repo := &Repository{db: db}
|
||||||
if len(deps) > 0 {
|
if len(deps) > 0 {
|
||||||
repo.chatNotifier = deps[0].ChatNotifier
|
repo.chatNotifier = deps[0].ChatNotifier
|
||||||
|
repo.chatArchiver = deps[0].ChatArchiver
|
||||||
repo.refundStarter = deps[0].RefundStarter
|
repo.refundStarter = deps[0].RefundStarter
|
||||||
}
|
}
|
||||||
return repo
|
return repo
|
||||||
|
|||||||
@@ -196,6 +196,16 @@ func (s *Service) AdminMarkAbnormal(ctx context.Context, adminID uint64, orderID
|
|||||||
return s.repo.AdminMarkAbnormal(ctx, adminID, orderID, req, meta)
|
return s.repo.AdminMarkAbnormal(ctx, adminID, orderID, req, meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) AdminSeal(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||||
|
if s.repo == nil {
|
||||||
|
return ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
if orderID == 0 || req.Reason == "" {
|
||||||
|
return ErrOrderCannotComplete
|
||||||
|
}
|
||||||
|
return s.repo.AdminSeal(ctx, adminID, orderID, req, meta)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) AdminResetHandoff(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
func (s *Service) AdminResetHandoff(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return ErrDependencyUnavailable
|
return ErrDependencyUnavailable
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
if deps.DB != nil {
|
if deps.DB != nil {
|
||||||
orderRepo = order.NewRepository(deps.DB, order.Dependencies{
|
orderRepo = order.NewRepository(deps.DB, order.Dependencies{
|
||||||
ChatNotifier: chatRepo,
|
ChatNotifier: chatRepo,
|
||||||
|
ChatArchiver: chat.NewOrderChatArchiverAdapter(),
|
||||||
RefundStarter: order.RefundStarterFunc(func(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (string, error) {
|
RefundStarter: order.RefundStarterFunc(func(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (string, error) {
|
||||||
if paymentRepo == nil {
|
if paymentRepo == nil {
|
||||||
return "", order.ErrDependencyUnavailable
|
return "", order.ErrDependencyUnavailable
|
||||||
@@ -518,6 +519,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
adminRoutes.GET("/orders/:id", requirePerm("order:view"), orderHandler.AdminDetail)
|
adminRoutes.GET("/orders/:id", requirePerm("order:view"), orderHandler.AdminDetail)
|
||||||
adminRoutes.GET("/orders/:id/handoff-records", requirePerm("order:view"), orderHandler.AdminHandoffRecords)
|
adminRoutes.GET("/orders/:id/handoff-records", requirePerm("order:view"), orderHandler.AdminHandoffRecords)
|
||||||
adminRoutes.POST("/orders/:id/close", requirePerm("order:close"), orderHandler.AdminClose)
|
adminRoutes.POST("/orders/:id/close", requirePerm("order:close"), orderHandler.AdminClose)
|
||||||
|
adminRoutes.POST("/orders/:id/seal", requirePerm("order:close"), orderHandler.AdminSeal)
|
||||||
adminRoutes.POST("/orders/:id/mark-abnormal", requirePerm("order:mark_abnormal"), orderHandler.AdminMarkAbnormal)
|
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/reset-handoff", requirePerm("order:mark_abnormal"), orderHandler.AdminResetHandoff)
|
||||||
adminRoutes.POST("/orders/:id/refund", requirePerm("order:close"), orderHandler.AdminRefund)
|
adminRoutes.POST("/orders/:id/refund", requirePerm("order:close"), orderHandler.AdminRefund)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
adminRefundOrder,
|
adminRefundOrder,
|
||||||
adminRefundStatus,
|
adminRefundStatus,
|
||||||
adminResetHandoff,
|
adminResetHandoff,
|
||||||
|
adminSealOrder,
|
||||||
fetchAdminHandoffRecords,
|
fetchAdminHandoffRecords,
|
||||||
fetchAdminOrder,
|
fetchAdminOrder,
|
||||||
type HandoffRecord,
|
type HandoffRecord,
|
||||||
@@ -40,7 +41,7 @@ const submitting = ref(false)
|
|||||||
const order = ref<Order | null>(null)
|
const order = ref<Order | null>(null)
|
||||||
const handoffRecords = ref<HandoffRecord[]>([])
|
const handoffRecords = ref<HandoffRecord[]>([])
|
||||||
const paymentRecords = ref<AdminPayment[]>([])
|
const paymentRecords = ref<AdminPayment[]>([])
|
||||||
const actionType = ref<'close' | 'abnormal' | 'reset' | ''>('')
|
const actionType = ref<'close' | 'seal' | 'abnormal' | 'reset' | ''>('')
|
||||||
const reason = ref('')
|
const reason = ref('')
|
||||||
const refundStatus = ref<RefundStatus | null>(null)
|
const refundStatus = ref<RefundStatus | null>(null)
|
||||||
|
|
||||||
@@ -89,10 +90,13 @@ const resetActionLabel = computed(() => {
|
|||||||
const canResetHandoff = computed(() => resetAction.value?.enabled === true)
|
const canResetHandoff = computed(() => resetAction.value?.enabled === true)
|
||||||
const actionTitle = computed(() => {
|
const actionTitle = computed(() => {
|
||||||
if (actionType.value === 'close') return '客服关闭订单'
|
if (actionType.value === 'close') return '客服关闭订单'
|
||||||
|
if (actionType.value === 'seal') return '封存订单'
|
||||||
if (actionType.value === 'reset') return `${resetActionLabel.value}(恢复到对应待办)`
|
if (actionType.value === 'reset') return `${resetActionLabel.value}(恢复到对应待办)`
|
||||||
return '标记订单异常'
|
return '标记订单异常'
|
||||||
})
|
})
|
||||||
const closeActionTip = '关闭订单并归档商品/账号;已支付订单会按租金+实付押金全量原路退款。'
|
const closeActionTip = '关闭订单并归档商品/账号;已支付订单会按租金+实付押金全量原路退款。'
|
||||||
|
const sealActionTip =
|
||||||
|
'封存订单:终止订单并全量原路退款,自动解散关联群聊,关联商品永久封存,号主无法再次编辑或上架。适用于号主失联、无法联系的场景。'
|
||||||
const refundActionTip = '仅发起后台人工全量原路退款,不关闭订单或调整商品/账号状态。'
|
const refundActionTip = '仅发起后台人工全量原路退款,不关闭订单或调整商品/账号状态。'
|
||||||
const refundButtonDisabled = computed(() => refundStatus.value?.refund_status === 'refunded')
|
const refundButtonDisabled = computed(() => refundStatus.value?.refund_status === 'refunded')
|
||||||
const orderTotalCent = computed(
|
const orderTotalCent = computed(
|
||||||
@@ -201,7 +205,7 @@ async function loadRefundStatus() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function openAction(type: 'close' | 'abnormal' | 'reset') {
|
function openAction(type: 'close' | 'seal' | 'abnormal' | 'reset') {
|
||||||
actionType.value = type
|
actionType.value = type
|
||||||
reason.value = ''
|
reason.value = ''
|
||||||
}
|
}
|
||||||
@@ -224,6 +228,24 @@ async function confirmCloseAction() {
|
|||||||
openAction('close')
|
openAction('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function confirmSealAction() {
|
||||||
|
if (!order.value || !canOperate.value) return
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'封存会终止订单并全量原路退款,自动解散关联群聊,关联商品将被永久封存,号主无法再次编辑或上架。适用于号主失联场景。确认继续?',
|
||||||
|
'确认封存订单',
|
||||||
|
{
|
||||||
|
confirmButtonText: '继续封存',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
openAction('seal')
|
||||||
|
}
|
||||||
|
|
||||||
async function submitAction() {
|
async function submitAction() {
|
||||||
if (!order.value || !actionType.value) return
|
if (!order.value || !actionType.value) return
|
||||||
submitting.value = true
|
submitting.value = true
|
||||||
@@ -231,6 +253,9 @@ async function submitAction() {
|
|||||||
if (actionType.value === 'close') {
|
if (actionType.value === 'close') {
|
||||||
await adminCloseOrder(order.value.id, reason.value)
|
await adminCloseOrder(order.value.id, reason.value)
|
||||||
ElMessage.success('订单已关闭')
|
ElMessage.success('订单已关闭')
|
||||||
|
} else if (actionType.value === 'seal') {
|
||||||
|
await adminSealOrder(order.value.id, reason.value)
|
||||||
|
ElMessage.success('订单已封存,群聊已解散')
|
||||||
} else if (actionType.value === 'reset') {
|
} else if (actionType.value === 'reset') {
|
||||||
await adminResetHandoff(order.value.id, reason.value)
|
await adminResetHandoff(order.value.id, reason.value)
|
||||||
ElMessage.success(`${resetActionLabel.value}成功`)
|
ElMessage.success(`${resetActionLabel.value}成功`)
|
||||||
@@ -467,6 +492,13 @@ function paymentPaidAt(record: AdminPayment) {
|
|||||||
>
|
>
|
||||||
</span>
|
</span>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
|
<el-tooltip :content="sealActionTip" placement="top">
|
||||||
|
<span>
|
||||||
|
<el-button type="danger" plain :disabled="!canOperate" @click="confirmSealAction"
|
||||||
|
>封存订单</el-button
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</el-tooltip>
|
||||||
<el-tooltip :content="refundActionTip" placement="top">
|
<el-tooltip :content="refundActionTip" placement="top">
|
||||||
<span>
|
<span>
|
||||||
<el-button
|
<el-button
|
||||||
|
|||||||
@@ -341,6 +341,11 @@ export async function adminCloseOrder(id: number, reason: string) {
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function adminSealOrder(id: number, reason: string) {
|
||||||
|
const { data } = await apiClient.post<ApiResponse<Order>>(`/admin/orders/${id}/seal`, { reason })
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
export async function adminMarkOrderAbnormal(id: number, reason: string) {
|
export async function adminMarkOrderAbnormal(id: number, reason: string) {
|
||||||
const { data } = await apiClient.post<ApiResponse<Order>>(`/admin/orders/${id}/mark-abnormal`, {
|
const { data } = await apiClient.post<ApiResponse<Order>>(`/admin/orders/${id}/mark-abnormal`, {
|
||||||
reason,
|
reason,
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ function statusTone(status: string) {
|
|||||||
completed: 'success',
|
completed: 'success',
|
||||||
rented: 'warning',
|
rented: 'warning',
|
||||||
abnormal: 'danger',
|
abnormal: 'danger',
|
||||||
|
sealed: 'muted',
|
||||||
}
|
}
|
||||||
return tones[status] || 'info'
|
return tones[status] || 'info'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ function statusTone(status: string) {
|
|||||||
draft: 'info',
|
draft: 'info',
|
||||||
offline: 'muted',
|
offline: 'muted',
|
||||||
completed: 'success',
|
completed: 'success',
|
||||||
|
sealed: 'danger',
|
||||||
rented: 'warning',
|
rented: 'warning',
|
||||||
abnormal: 'danger',
|
abnormal: 'danger',
|
||||||
}
|
}
|
||||||
@@ -177,7 +178,7 @@ function showReviewStatus(row: Listing) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isTerminalListing(row: Listing) {
|
function isTerminalListing(row: Listing) {
|
||||||
return row.status === 'rented' || row.status === 'completed'
|
return row.status === 'rented' || row.status === 'completed' || row.status === 'sealed'
|
||||||
}
|
}
|
||||||
|
|
||||||
function isPendingReview(row: Listing) {
|
function isPendingReview(row: Listing) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ export const listingStatuses = [
|
|||||||
'offline',
|
'offline',
|
||||||
'completed',
|
'completed',
|
||||||
'abnormal',
|
'abnormal',
|
||||||
|
'sealed',
|
||||||
] as const
|
] as const
|
||||||
export type ListingStatus = (typeof listingStatuses)[number]
|
export type ListingStatus = (typeof listingStatuses)[number]
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const listingStatusMap: Record<ListingStatus, string> = {
|
|||||||
offline: '已下架',
|
offline: '已下架',
|
||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
abnormal: '异常',
|
abnormal: '异常',
|
||||||
|
sealed: '已封存',
|
||||||
}
|
}
|
||||||
|
|
||||||
const listingReviewStatusMap: Record<ListingReviewStatus, string> = {
|
const listingReviewStatusMap: Record<ListingReviewStatus, string> = {
|
||||||
|
|||||||
Reference in New Issue
Block a user