撞车订单新增「接待中」状态,支持后台接待标记
已支付订单可标记为接待中,避免重复跟进;完成/取消覆盖接待中状态,前后台同步展示与筛选。
This commit is contained in:
@@ -92,6 +92,7 @@ const (
|
|||||||
|
|
||||||
MohongOrderStatusPendingPayment = "pending_payment"
|
MohongOrderStatusPendingPayment = "pending_payment"
|
||||||
MohongOrderStatusPaid = "paid"
|
MohongOrderStatusPaid = "paid"
|
||||||
|
MohongOrderStatusReceiving = "receiving" // 接待中:已有人接待买家
|
||||||
MohongOrderStatusCompleted = "completed"
|
MohongOrderStatusCompleted = "completed"
|
||||||
MohongOrderStatusCancelled = "cancelled"
|
MohongOrderStatusCancelled = "cancelled"
|
||||||
MohongOrderStatusRefunded = "refunded"
|
MohongOrderStatusRefunded = "refunded"
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ type AdminCompleteOrderRequest struct {
|
|||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminReceiveOrderRequest struct {
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
}
|
||||||
|
|
||||||
type AdminCancelOrderRequest struct {
|
type AdminCancelOrderRequest struct {
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ var (
|
|||||||
ErrOrderNotFound = errors.New("order not found")
|
ErrOrderNotFound = errors.New("order not found")
|
||||||
ErrOrderCannotPay = errors.New("order cannot pay")
|
ErrOrderCannotPay = errors.New("order cannot pay")
|
||||||
ErrOrderCannotCancel = errors.New("order cannot cancel")
|
ErrOrderCannotCancel = errors.New("order cannot cancel")
|
||||||
|
ErrOrderCannotReceive = errors.New("order cannot receive")
|
||||||
ErrOrderCannotComplete = errors.New("order cannot complete")
|
ErrOrderCannotComplete = errors.New("order cannot complete")
|
||||||
ErrUnauthorized = errors.New("unauthorized")
|
ErrUnauthorized = errors.New("unauthorized")
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -292,6 +292,21 @@ func (h *Handler) AdminGetOrder(c *gin.Context) {
|
|||||||
response.OK(c, item)
|
response.OK(c, item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) AdminReceiveOrder(c *gin.Context) {
|
||||||
|
id, ok := parseID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var req AdminReceiveOrderRequest
|
||||||
|
_ = c.ShouldBindJSON(&req)
|
||||||
|
item, err := h.service.AdminReceiveOrder(c.Request.Context(), id, req.Remark)
|
||||||
|
if err != nil {
|
||||||
|
writeError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, item)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) AdminCompleteOrder(c *gin.Context) {
|
func (h *Handler) AdminCompleteOrder(c *gin.Context) {
|
||||||
id, ok := parseID(c)
|
id, ok := parseID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -357,6 +372,8 @@ func writeError(c *gin.Context, err error) {
|
|||||||
response.Error(c, 409, "order_cannot_pay", "订单无法支付")
|
response.Error(c, 409, "order_cannot_pay", "订单无法支付")
|
||||||
case errors.Is(err, ErrOrderCannotCancel):
|
case errors.Is(err, ErrOrderCannotCancel):
|
||||||
response.Error(c, 409, "order_cannot_cancel", "订单无法取消")
|
response.Error(c, 409, "order_cannot_cancel", "订单无法取消")
|
||||||
|
case errors.Is(err, ErrOrderCannotReceive):
|
||||||
|
response.Error(c, 409, "order_cannot_receive", "订单无法标记为接待中")
|
||||||
case errors.Is(err, ErrOrderCannotComplete):
|
case errors.Is(err, ErrOrderCannotComplete):
|
||||||
response.Error(c, 409, "order_cannot_complete", "订单无法完成")
|
response.Error(c, 409, "order_cannot_complete", "订单无法完成")
|
||||||
case errors.Is(err, ErrInvalidRequest):
|
case errors.Is(err, ErrInvalidRequest):
|
||||||
|
|||||||
@@ -266,13 +266,15 @@ func (r *Repository) AdminCancelOrder(ctx context.Context, orderID uint64, reaso
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if order.Status != model.MohongOrderStatusPendingPayment && order.Status != model.MohongOrderStatusPaid {
|
if order.Status != model.MohongOrderStatusPendingPayment &&
|
||||||
|
order.Status != model.MohongOrderStatusPaid &&
|
||||||
|
order.Status != model.MohongOrderStatusReceiving {
|
||||||
return ErrOrderCannotCancel
|
return ErrOrderCannotCancel
|
||||||
}
|
}
|
||||||
if order.Status == model.MohongOrderStatusPendingPayment {
|
if order.Status == model.MohongOrderStatusPendingPayment {
|
||||||
return r.cancelPendingOrderTx(tx, &order, firstNonEmpty(reason, "后台取消"))
|
return r.cancelPendingOrderTx(tx, &order, firstNonEmpty(reason, "后台取消"))
|
||||||
}
|
}
|
||||||
// 已支付:仅标记取消(退款后续可接支付退款)。
|
// 已支付/接待中:仅标记取消(退款后续可接支付退款)。
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
order.Status = model.MohongOrderStatusCancelled
|
order.Status = model.MohongOrderStatusCancelled
|
||||||
order.CancelledAt = &now
|
order.CancelledAt = &now
|
||||||
@@ -285,6 +287,31 @@ func (r *Repository) AdminCancelOrder(ctx context.Context, orderID uint64, reaso
|
|||||||
return r.AdminFindOrder(ctx, orderID)
|
return r.AdminFindOrder(ctx, orderID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AdminReceiveOrder 标记订单为接待中(已支付 → 接待中)。
|
||||||
|
func (r *Repository) AdminReceiveOrder(ctx context.Context, orderID uint64, remark string) (*OrderDTO, error) {
|
||||||
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
var order model.MohongOrder
|
||||||
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||||
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
return ErrOrderNotFound
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if order.Status != model.MohongOrderStatusPaid {
|
||||||
|
return ErrOrderCannotReceive
|
||||||
|
}
|
||||||
|
order.Status = model.MohongOrderStatusReceiving
|
||||||
|
if strings.TrimSpace(remark) != "" {
|
||||||
|
order.AdminRemark = strings.TrimSpace(remark)
|
||||||
|
}
|
||||||
|
return tx.Save(&order).Error
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.AdminFindOrder(ctx, orderID)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) AdminCompleteOrder(ctx context.Context, orderID uint64, remark string) (*OrderDTO, error) {
|
func (r *Repository) AdminCompleteOrder(ctx context.Context, orderID uint64, remark string) (*OrderDTO, error) {
|
||||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
var order model.MohongOrder
|
var order model.MohongOrder
|
||||||
@@ -294,7 +321,7 @@ func (r *Repository) AdminCompleteOrder(ctx context.Context, orderID uint64, rem
|
|||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if order.Status != model.MohongOrderStatusPaid {
|
if order.Status != model.MohongOrderStatusPaid && order.Status != model.MohongOrderStatusReceiving {
|
||||||
return ErrOrderCannotComplete
|
return ErrOrderCannotComplete
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -319,6 +346,7 @@ func (r *Repository) ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint
|
|||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
if order.Status == model.MohongOrderStatusPaid ||
|
if order.Status == model.MohongOrderStatusPaid ||
|
||||||
|
order.Status == model.MohongOrderStatusReceiving ||
|
||||||
order.Status == model.MohongOrderStatusCompleted {
|
order.Status == model.MohongOrderStatusCompleted {
|
||||||
return 0, nil
|
return 0, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,6 +136,13 @@ func (s *Service) AdminFindOrder(ctx context.Context, orderID uint64) (*OrderDTO
|
|||||||
return s.repo.AdminFindOrder(ctx, orderID)
|
return s.repo.AdminFindOrder(ctx, orderID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) AdminReceiveOrder(ctx context.Context, orderID uint64, remark string) (*OrderDTO, error) {
|
||||||
|
if s.repo == nil {
|
||||||
|
return nil, ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
return s.repo.AdminReceiveOrder(ctx, orderID, remark)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) AdminCompleteOrder(ctx context.Context, orderID uint64, remark string) (*OrderDTO, error) {
|
func (s *Service) AdminCompleteOrder(ctx context.Context, orderID uint64, remark string) (*OrderDTO, error) {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return nil, ErrDependencyUnavailable
|
return nil, ErrDependencyUnavailable
|
||||||
|
|||||||
@@ -617,6 +617,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
adminRoutes.DELETE(prefix+"/products/:id", requirePerm("mohong:product_manage"), mohongHandler.AdminDeleteProduct)
|
adminRoutes.DELETE(prefix+"/products/:id", requirePerm("mohong:product_manage"), mohongHandler.AdminDeleteProduct)
|
||||||
adminRoutes.GET(prefix+"/orders", requirePerm("mohong:order_view"), mohongHandler.AdminListOrders)
|
adminRoutes.GET(prefix+"/orders", requirePerm("mohong:order_view"), mohongHandler.AdminListOrders)
|
||||||
adminRoutes.GET(prefix+"/orders/:id", requirePerm("mohong:order_view"), mohongHandler.AdminGetOrder)
|
adminRoutes.GET(prefix+"/orders/:id", requirePerm("mohong:order_view"), mohongHandler.AdminGetOrder)
|
||||||
|
adminRoutes.POST(prefix+"/orders/:id/receive", requirePerm("mohong:order_manage"), mohongHandler.AdminReceiveOrder)
|
||||||
adminRoutes.POST(prefix+"/orders/:id/complete", requirePerm("mohong:order_manage"), mohongHandler.AdminCompleteOrder)
|
adminRoutes.POST(prefix+"/orders/:id/complete", requirePerm("mohong:order_manage"), mohongHandler.AdminCompleteOrder)
|
||||||
adminRoutes.POST(prefix+"/orders/:id/cancel", requirePerm("mohong:order_manage"), mohongHandler.AdminCancelOrder)
|
adminRoutes.POST(prefix+"/orders/:id/cancel", requirePerm("mohong:order_manage"), mohongHandler.AdminCancelOrder)
|
||||||
adminRoutes.GET(prefix+"/config", requirePerm("mohong:config"), mohongHandler.AdminGetConfig)
|
adminRoutes.GET(prefix+"/config", requirePerm("mohong:config"), mohongHandler.AdminGetConfig)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
completeAdminMohongOrder,
|
completeAdminMohongOrder,
|
||||||
fetchAdminMohongOrders,
|
fetchAdminMohongOrders,
|
||||||
mohongOrderStatusLabel,
|
mohongOrderStatusLabel,
|
||||||
|
receiveAdminMohongOrder,
|
||||||
type MohongOrder,
|
type MohongOrder,
|
||||||
} from '@/features/mohong/api/mohong'
|
} from '@/features/mohong/api/mohong'
|
||||||
import { formatCent } from '@/shared/utils/money'
|
import { formatCent } from '@/shared/utils/money'
|
||||||
@@ -43,6 +44,19 @@ async function load() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleReceive(row: MohongOrder) {
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(`确认接待订单 ${row.order_no}?标记后状态变为「接待中」。`, '提示', {
|
||||||
|
type: 'info',
|
||||||
|
})
|
||||||
|
await receiveAdminMohongOrder(row.id)
|
||||||
|
ElMessage.success('已标记为接待中')
|
||||||
|
await load()
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') ElMessage.error(readError(error, '操作失败'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleComplete(row: MohongOrder) {
|
async function handleComplete(row: MohongOrder) {
|
||||||
try {
|
try {
|
||||||
await ElMessageBox.confirm(`确认将订单 ${row.order_no} 标记为已完成?`, '提示', {
|
await ElMessageBox.confirm(`确认将订单 ${row.order_no} 标记为已完成?`, '提示', {
|
||||||
@@ -71,6 +85,20 @@ async function handleCancel(row: MohongOrder) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canReceive(row: MohongOrder) {
|
||||||
|
return row.status === 'paid'
|
||||||
|
}
|
||||||
|
|
||||||
|
function canComplete(row: MohongOrder) {
|
||||||
|
return row.status === 'paid' || row.status === 'receiving'
|
||||||
|
}
|
||||||
|
|
||||||
|
function canCancel(row: MohongOrder) {
|
||||||
|
return (
|
||||||
|
row.status === 'pending_payment' || row.status === 'paid' || row.status === 'receiving'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function copyText(text: string) {
|
async function copyText(text: string) {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text)
|
await navigator.clipboard.writeText(text)
|
||||||
@@ -86,7 +114,7 @@ async function copyText(text: string) {
|
|||||||
<div class="page-head">
|
<div class="page-head">
|
||||||
<div>
|
<div>
|
||||||
<h2>撞车订单</h2>
|
<h2>撞车订单</h2>
|
||||||
<p>查看订单、完成履约、复制用户订单信息</p>
|
<p>查看订单、接待买家、完成履约、复制用户订单信息</p>
|
||||||
</div>
|
</div>
|
||||||
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
<el-button :icon="Refresh" @click="load">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,6 +124,7 @@ async function copyText(text: string) {
|
|||||||
<el-select v-model="status" clearable placeholder="状态" style="width: 140px">
|
<el-select v-model="status" clearable placeholder="状态" style="width: 140px">
|
||||||
<el-option label="待支付" value="pending_payment" />
|
<el-option label="待支付" value="pending_payment" />
|
||||||
<el-option label="已支付" value="paid" />
|
<el-option label="已支付" value="paid" />
|
||||||
|
<el-option label="接待中" value="receiving" />
|
||||||
<el-option label="已完成" value="completed" />
|
<el-option label="已完成" value="completed" />
|
||||||
<el-option label="已取消" value="cancelled" />
|
<el-option label="已取消" value="cancelled" />
|
||||||
</el-select>
|
</el-select>
|
||||||
@@ -121,23 +150,16 @@ async function copyText(text: string) {
|
|||||||
<el-table-column label="下单时间" min-width="160">
|
<el-table-column label="下单时间" min-width="160">
|
||||||
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
<template #default="{ row }">{{ formatDateTime(row.created_at) }}</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="220" fixed="right">
|
<el-table-column label="操作" width="260" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-button link type="primary" @click="detail = row">详情</el-button>
|
<el-button link type="primary" @click="detail = row">详情</el-button>
|
||||||
<el-button
|
<el-button v-if="canReceive(row)" link type="warning" @click="handleReceive(row)">
|
||||||
v-if="row.status === 'paid'"
|
接待
|
||||||
link
|
</el-button>
|
||||||
type="success"
|
<el-button v-if="canComplete(row)" link type="success" @click="handleComplete(row)">
|
||||||
@click="handleComplete(row)"
|
|
||||||
>
|
|
||||||
完成
|
完成
|
||||||
</el-button>
|
</el-button>
|
||||||
<el-button
|
<el-button v-if="canCancel(row)" link type="danger" @click="handleCancel(row)">
|
||||||
v-if="row.status === 'pending_payment' || row.status === 'paid'"
|
|
||||||
link
|
|
||||||
type="danger"
|
|
||||||
@click="handleCancel(row)"
|
|
||||||
>
|
|
||||||
取消
|
取消
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -263,6 +263,14 @@ export async function fetchAdminMohongOrder(id: number | string) {
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function receiveAdminMohongOrder(id: number, remark?: string) {
|
||||||
|
const { data } = await apiClient.post<ApiResponse<MohongOrder>>(
|
||||||
|
`/admin/crash/orders/${id}/receive`,
|
||||||
|
{ remark: remark || '' }
|
||||||
|
)
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
export async function completeAdminMohongOrder(id: number, remark?: string) {
|
export async function completeAdminMohongOrder(id: number, remark?: string) {
|
||||||
const { data } = await apiClient.post<ApiResponse<MohongOrder>>(
|
const { data } = await apiClient.post<ApiResponse<MohongOrder>>(
|
||||||
`/admin/crash/orders/${id}/complete`,
|
`/admin/crash/orders/${id}/complete`,
|
||||||
@@ -293,6 +301,7 @@ export function mohongOrderStatusLabel(status: string) {
|
|||||||
const map: Record<string, string> = {
|
const map: Record<string, string> = {
|
||||||
pending_payment: '待支付',
|
pending_payment: '待支付',
|
||||||
paid: '已支付',
|
paid: '已支付',
|
||||||
|
receiving: '接待中',
|
||||||
completed: '已完成',
|
completed: '已完成',
|
||||||
cancelled: '已取消',
|
cancelled: '已取消',
|
||||||
refunded: '已退款',
|
refunded: '已退款',
|
||||||
|
|||||||
@@ -157,6 +157,11 @@ async function loadOrders() {
|
|||||||
color: #059669;
|
color: #059669;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-tag.receiving {
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
.status-tag.completed {
|
.status-tag.completed {
|
||||||
background: #eff6ff;
|
background: #eff6ff;
|
||||||
color: #2563eb;
|
color: #2563eb;
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ const statusTabs = [
|
|||||||
{ key: 'pending_handoff', label: '待交接' },
|
{ key: 'pending_handoff', label: '待交接' },
|
||||||
{ key: 'renting', label: '使用中' },
|
{ key: 'renting', label: '使用中' },
|
||||||
{ key: 'paid', label: '已支付' },
|
{ key: 'paid', label: '已支付' },
|
||||||
|
{ key: 'receiving', label: '接待中' },
|
||||||
{ key: 'pending_checkout_confirm', label: '待结账' },
|
{ key: 'pending_checkout_confirm', label: '待结账' },
|
||||||
{ key: 'completed', label: '已完成' },
|
{ key: 'completed', label: '已完成' },
|
||||||
{ key: 'cancelled', label: '已取消' },
|
{ key: 'cancelled', label: '已取消' },
|
||||||
@@ -638,6 +639,10 @@ async function copyListingCode(order: Order) {
|
|||||||
color: #1477ff;
|
color: #1477ff;
|
||||||
background: rgba(20, 119, 255, 0.08);
|
background: rgba(20, 119, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
.badge-receiving {
|
||||||
|
color: #d97706;
|
||||||
|
background: rgba(217, 119, 6, 0.1);
|
||||||
|
}
|
||||||
.badge-completed {
|
.badge-completed {
|
||||||
color: #10b981;
|
color: #10b981;
|
||||||
background: rgba(16, 185, 129, 0.08);
|
background: rgba(16, 185, 129, 0.08);
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const statusTabs = [
|
|||||||
{ key: 'pending_handoff', label: '待交接' },
|
{ key: 'pending_handoff', label: '待交接' },
|
||||||
{ key: 'renting', label: '使用中' },
|
{ key: 'renting', label: '使用中' },
|
||||||
{ key: 'paid', label: '已支付' },
|
{ key: 'paid', label: '已支付' },
|
||||||
|
{ key: 'receiving', label: '接待中' },
|
||||||
{ key: 'pending_checkout_confirm', label: '待结账' },
|
{ key: 'pending_checkout_confirm', label: '待结账' },
|
||||||
{ key: 'completed', label: '已完成' },
|
{ key: 'completed', label: '已完成' },
|
||||||
{ key: 'cancelled', label: '已取消' },
|
{ key: 'cancelled', label: '已取消' },
|
||||||
@@ -844,6 +845,11 @@ function bizTypeLabel(bizType: BizType) {
|
|||||||
color: #059669;
|
color: #059669;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.crash-status.receiving {
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
.crash-status.completed {
|
.crash-status.completed {
|
||||||
background: #eff6ff;
|
background: #eff6ff;
|
||||||
color: #2563eb;
|
color: #2563eb;
|
||||||
|
|||||||
Reference in New Issue
Block a user