feat(claim): 支持固定H5订单号领取与订单兑换有效性后台控制
- 数据库新增 099_order_claim_status.sql 迁移,扩展 claim_status、claim_status_reason 等字段 - 后端新增 /claims/h5/:orderNo、UID提交、分销提交接口,保留原有 token 接口双轨兼容 - 增加兑换有效性强拦截:暂停/关闭/退款/取消订单精准阻断并返回友好提示 - 后台新增 POST /admin/orders/:orderId/claim-status 接口与审计日志记录 - 前端管理后台(订单列表、详情页)支持 H5 兑换状态展示、一键复制链接及暂停/关闭/启用弹窗 - 前端领取页面适配 /h5/:orderNo 路由及暂停/关闭等状态的异常展示 - 验证方式:运行前后端类型检查、全量单元测试(后端 674、前端 132)及全量构建编译均通过
This commit is contained in:
@@ -1,19 +1,44 @@
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons'
|
||||
import { Alert, Button, Card, Descriptions, Skeleton, Space, Table, Tag, Typography } from 'antd'
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
CopyOutlined,
|
||||
PauseCircleOutlined,
|
||||
PlayCircleOutlined,
|
||||
StopOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Card,
|
||||
Descriptions,
|
||||
Input,
|
||||
Modal,
|
||||
Skeleton,
|
||||
Space,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router'
|
||||
|
||||
import JsonPreview from '@/components/admin/JsonPreview'
|
||||
import PageHeader from '@/components/admin/PageHeader'
|
||||
import StatusTag from '@/components/admin/StatusTag'
|
||||
import { fetchAdminOrderDetail } from '@/services/admin'
|
||||
import { fetchAdminOrderDetail, updateAdminOrderClaimStatus } from '@/services/admin'
|
||||
import type { AdminOrderDetail, AdminTaskListItem } from '@/types/admin'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
|
||||
export default function AdminOrderDetailPage() {
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
const { orderId = '' } = useParams()
|
||||
const [statusModalVisible, setStatusModalVisible] = useState(false)
|
||||
const [targetStatus, setTargetStatus] = useState<'active' | 'paused' | 'closed'>('active')
|
||||
const [statusReason, setStatusReason] = useState('')
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ['admin-order-detail', orderId],
|
||||
queryFn: () => fetchAdminOrderDetail(orderId),
|
||||
@@ -21,6 +46,43 @@ export default function AdminOrderDetailPage() {
|
||||
})
|
||||
const detail = query.data?.data
|
||||
|
||||
const statusMutation = useMutation({
|
||||
mutationFn: (data: { status: 'active' | 'paused' | 'closed'; reason?: string }) =>
|
||||
updateAdminOrderClaimStatus(orderId, data),
|
||||
onSuccess: () => {
|
||||
message.success('订单兑换状态已更新')
|
||||
setStatusModalVisible(false)
|
||||
setStatusReason('')
|
||||
void queryClient.invalidateQueries({ queryKey: ['admin-order-detail', orderId] })
|
||||
},
|
||||
onError: (err: unknown) => {
|
||||
message.error(err instanceof Error ? err.message : '更新状态失败')
|
||||
},
|
||||
})
|
||||
|
||||
const handleCopyH5Link = () => {
|
||||
if (!detail?.order.platformOrderId) return
|
||||
const h5Url = `${window.location.origin}/h5/${encodeURIComponent(detail.order.platformOrderId)}`
|
||||
navigator.clipboard
|
||||
.writeText(h5Url)
|
||||
.then(() => message.success('已复制 H5 领取链接: ' + h5Url))
|
||||
.catch(() => message.error('复制失败,请手动复制'))
|
||||
}
|
||||
|
||||
const openStatusModal = (status: 'active' | 'paused' | 'closed') => {
|
||||
if (status === 'active') {
|
||||
Modal.confirm({
|
||||
title: '恢复启用订单兑换',
|
||||
content: '恢复后,买家可正常进入 H5 页面提交 UID 领取,确认恢复吗?',
|
||||
onOk: () => statusMutation.mutateAsync({ status: 'active', reason: '管理员恢复启用' }),
|
||||
})
|
||||
return
|
||||
}
|
||||
setTargetStatus(status)
|
||||
setStatusReason('')
|
||||
setStatusModalVisible(true)
|
||||
}
|
||||
|
||||
if (query.isLoading) {
|
||||
return (
|
||||
<section className="page-stack">
|
||||
@@ -93,7 +155,45 @@ export default function AdminOrderDetailPage() {
|
||||
extra={<BackButton />}
|
||||
/>
|
||||
|
||||
<Card title="基础信息">
|
||||
<Card
|
||||
title="基础信息"
|
||||
extra={
|
||||
<Space>
|
||||
<Button icon={<CopyOutlined />} onClick={handleCopyH5Link}>
|
||||
复制 H5 领取链接
|
||||
</Button>
|
||||
{detail.order.claimStatus === 'paused' || detail.order.claimStatus === 'closed' ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
onClick={() => openStatusModal('active')}
|
||||
loading={statusMutation.isPending}
|
||||
>
|
||||
恢复启用
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
danger
|
||||
icon={<PauseCircleOutlined />}
|
||||
onClick={() => openStatusModal('paused')}
|
||||
loading={statusMutation.isPending}
|
||||
>
|
||||
暂停兑换
|
||||
</Button>
|
||||
)}
|
||||
{detail.order.claimStatus !== 'closed' ? (
|
||||
<Button
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
onClick={() => openStatusModal('closed')}
|
||||
loading={statusMutation.isPending}
|
||||
>
|
||||
关闭兑换
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Descriptions column={{ xs: 1, md: 2, xl: 3 }} bordered size="small">
|
||||
<Descriptions.Item label="订单 ID">{detail.order.orderId}</Descriptions.Item>
|
||||
<Descriptions.Item label="店铺">
|
||||
@@ -106,6 +206,28 @@ export default function AdminOrderDetailPage() {
|
||||
<Descriptions.Item label="支付状态">
|
||||
<StatusTag value={detail.order.payStatus} kind="pay" />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="H5 兑换状态">
|
||||
<Space wrap>
|
||||
{detail.order.claimStatus === 'paused' ? (
|
||||
<Tag color="warning" icon={<PauseCircleOutlined />}>
|
||||
已暂停兑换
|
||||
</Tag>
|
||||
) : detail.order.claimStatus === 'closed' ? (
|
||||
<Tag color="default" icon={<StopOutlined />}>
|
||||
已关闭兑换
|
||||
</Tag>
|
||||
) : (
|
||||
<Tag color="success" icon={<PlayCircleOutlined />}>
|
||||
正常有效
|
||||
</Tag>
|
||||
)}
|
||||
{detail.order.claimStatusReason ? (
|
||||
<span className="muted" style={{ fontSize: 12 }}>
|
||||
({detail.order.claimStatusReason})
|
||||
</span>
|
||||
) : null}
|
||||
</Space>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="金额">
|
||||
{detail.order.totalAmount} {detail.order.currency}
|
||||
</Descriptions.Item>
|
||||
@@ -177,6 +299,34 @@ export default function AdminOrderDetailPage() {
|
||||
<Card title="原始载荷">
|
||||
<JsonPreview value={detail.order.rawPayload} />
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={targetStatus === 'paused' ? '暂停订单兑换' : '关闭订单兑换'}
|
||||
open={statusModalVisible}
|
||||
onCancel={() => setStatusModalVisible(false)}
|
||||
confirmLoading={statusMutation.isPending}
|
||||
onOk={() => {
|
||||
if (!statusReason.trim()) {
|
||||
message.warning('请填写原因或备注(如:买家申请退款)')
|
||||
return
|
||||
}
|
||||
statusMutation.mutate({ status: targetStatus, reason: statusReason.trim() })
|
||||
}}
|
||||
>
|
||||
<div style={{ padding: '12px 0' }}>
|
||||
<p style={{ color: '#64748b', marginBottom: 8 }}>
|
||||
{targetStatus === 'paused'
|
||||
? '暂停后,买家访问 H5 链接将被拦截并提示“兑换已暂停,请联系客服”,后续可随时恢复启用。'
|
||||
: '关闭后,买家将无法继续进入 H5 页面兑换,适用于已退款或订单作废。'}
|
||||
</p>
|
||||
<Input.TextArea
|
||||
rows={3}
|
||||
placeholder="请输入暂停/关闭原因(必填,如:买家退款、售后争议排查)"
|
||||
value={statusReason}
|
||||
onChange={(e) => setStatusReason(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SearchOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
import { Button, Card, Form, Input, Select, Space, Table, Tag, Typography } from 'antd'
|
||||
import { SearchOutlined, ReloadOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import { Button, Card, Form, Input, Select, Space, Table, Tag, Typography, message } from 'antd'
|
||||
import type { TableColumnsType } from 'antd'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
@@ -132,9 +132,25 @@ export default function AdminOrdersPage() {
|
||||
minWidth: 220,
|
||||
render: (_, row) => (
|
||||
<div className="cell-stack">
|
||||
<Typography.Link onClick={() => navigate(`/admin/orders/${row.orderId}`)}>
|
||||
{row.platformOrderId}
|
||||
</Typography.Link>
|
||||
<Space size={4}>
|
||||
<Typography.Link onClick={() => navigate(`/admin/orders/${row.orderId}`)}>
|
||||
{row.platformOrderId}
|
||||
</Typography.Link>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<CopyOutlined />}
|
||||
title="复制 H5 领取链接"
|
||||
style={{ padding: 0, height: 'auto', color: '#64748b' }}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
const url = `${window.location.origin}/h5/${encodeURIComponent(row.platformOrderId)}`
|
||||
navigator.clipboard.writeText(url).then(() => {
|
||||
message.success('已复制 H5 链接')
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Space>
|
||||
<span className="muted">{row.shopName || row.shopId || row.provider}</span>
|
||||
</div>
|
||||
),
|
||||
@@ -214,12 +230,17 @@ export default function AdminOrdersPage() {
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
minWidth: 220,
|
||||
minWidth: 240,
|
||||
render: (_, row) => (
|
||||
<Space size={[0, 6]} wrap>
|
||||
<StatusTag value={row.payStatus} kind="pay" />
|
||||
<StatusTag value={row.resourceStatus} kind="resource" />
|
||||
<StatusTag value={row.customerStatus} kind="customer" />
|
||||
{row.claimStatus === 'paused' ? (
|
||||
<Tag color="warning">H5已暂停</Tag>
|
||||
) : row.claimStatus === 'closed' ? (
|
||||
<Tag color="default">H5已关闭</Tag>
|
||||
) : null}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user