Files
order_site/apps/frontend/src/pages/admin/AdminDevMockPage.tsx
T

579 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { BugOutlined, CopyOutlined, LinkOutlined } from '@ant-design/icons'
import {
Alert,
App,
Button,
Card,
Col,
Divider,
Form,
Input,
Result,
Row,
Select,
Space,
Typography,
} from 'antd'
import { useMutation, useQuery } from '@tanstack/react-query'
import { useMemo, useState } from 'react'
import { Link } from 'react-router'
import PageHeader from '@/components/admin/PageHeader'
import {
buildOpen91QueryDevMock,
createAffiliateDashDevMock,
createFeifeiDevMock,
createKuaishouIndustryVoucherDevMock,
createLewanDevMock,
fetchDevMockStatus,
type DevMockCreateResult,
type DevMockKuaishouIndustryVoucherResult,
type DevMockOpen91QueryResult,
} from '@/services/admin'
export default function AdminDevMockPage() {
const { message } = App.useApp()
const [lastCreate, setLastCreate] = useState<DevMockCreateResult | null>(null)
const [lastVoucherCreate, setLastVoucherCreate] =
useState<DevMockKuaishouIndustryVoucherResult | null>(null)
const [lastQuery, setLastQuery] = useState<DevMockOpen91QueryResult | null>(null)
const statusQuery = useQuery({
queryKey: ['admin-dev-mock-status'],
queryFn: () => fetchDevMockStatus(),
retry: false,
})
const status = statusQuery.data?.data
const enabled = Boolean(status?.enabled)
const lewanMutation = useMutation({
mutationFn: createLewanDevMock,
onSuccess: (response) => {
setLastCreate(response.data)
message.success('lewan mock 已生成')
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '生成失败')
},
})
const feifeiMutation = useMutation({
mutationFn: createFeifeiDevMock,
onSuccess: (response) => {
setLastCreate(response.data)
message.success('feifei mock 已生成')
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '生成失败')
},
})
const affiliateDashMutation = useMutation({
mutationFn: createAffiliateDashDevMock,
onSuccess: (response) => {
setLastCreate(response.data)
message.success('affiliate-dash mock 已生成')
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '生成失败')
},
})
const voucherMutation = useMutation({
mutationFn: createKuaishouIndustryVoucherDevMock,
onSuccess: (response) => {
setLastVoucherCreate(response.data)
message.success(`已生成 ${response.data.createdCount} 条电子凭证 Mock`)
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '生成失败')
},
})
const open91Mutation = useMutation({
mutationFn: buildOpen91QueryDevMock,
onSuccess: (response) => {
setLastQuery(response.data)
message.success('91 查单请求已生成')
},
onError: (error) => {
message.error(error instanceof Error ? error.message : '生成失败')
},
})
const defaultOrderNo = useMemo(() => lastCreate?.orderNo || '', [lastCreate?.orderNo])
if (statusQuery.isLoading) {
return (
<section className="page-stack">
<PageHeader title="开发 Mock" description="加载中…" />
</section>
)
}
if (statusQuery.error || !enabled) {
return (
<section className="page-stack">
<PageHeader title="开发 Mock" description="仅开发环境可用。" />
<Result
status="403"
title="当前环境未开启开发 Mock"
subTitle={
statusQuery.error instanceof Error
? statusQuery.error.message
: '生产环境默认关闭。本地开发请使用 docker-compose.dev 或设置 ENABLE_DEV_MOCK=1。'
}
/>
</section>
)
}
return (
<section className="page-stack">
<PageHeader
title="开发 Mock"
description="一键造单、测领取页与 91 查单,无需真实 91 / 快手 / feifei 环境。"
extra={
<Typography.Text type="secondary">NODE_ENV={status?.nodeEnv || '-'}</Typography.Text>
}
/>
<Alert
type="info"
showIcon
icon={<BugOutlined />}
message="Mock 会写入开发库真实订单/任务,但 lewan 兑换走 mock 成功路径,feifei 不调用真实平台下单。"
description="建议 UID 与默认角色一致:lewan 默认 10001feifei 默认 166909256。"
/>
<Row gutter={[16, 16]}>
<Col xs={24} xl={12}>
<Card title="1. lewan 领取(强制 UID" bordered={false}>
<LewanMockForm
defaultUid={status?.defaultUid || '10001'}
loading={lewanMutation.isPending}
onSubmit={(values) => lewanMutation.mutate(values)}
/>
</Card>
</Col>
<Col xs={24} xl={12}>
<Card title="2. feifei 领取(拼 UID 到 H5" bordered={false}>
<FeifeiMockForm
loading={feifeiMutation.isPending}
onSubmit={(values) => feifeiMutation.mutate(values)}
/>
</Card>
</Col>
<Col xs={24} xl={12}>
<Card title="3. affiliate-dash 领取(mock 二维码/提交发货)" bordered={false}>
<AffiliateDashMockForm
loading={affiliateDashMutation.isPending}
onSubmit={(values) => affiliateDashMutation.mutate(values)}
/>
</Card>
</Col>
<Col xs={24} xl={12}>
<Card title="4. 快手电子凭证测试数据" bordered={false}>
<KuaishouIndustryVoucherMockForm
loading={voucherMutation.isPending}
onSubmit={(values) => voucherMutation.mutate(values)}
/>
</Card>
</Col>
<Col xs={24}>
<Card title="5. 模拟 91 查单(open-91/query" bordered={false}>
<Form
layout="inline"
initialValues={{ orderNo: defaultOrderNo, execute: '1' }}
key={defaultOrderNo}
onFinish={(values) =>
open91Mutation.mutate({
orderNo: values.orderNo,
execute: values.execute === '1',
})
}
>
<Form.Item
label="orderNo"
name="orderNo"
rules={[{ required: true, message: '填写上方生成的订单号' }]}
>
<Input style={{ width: 280 }} placeholder="MOCK91…" />
</Form.Item>
<Form.Item label="执行" name="execute">
<Select
style={{ width: 180 }}
options={[
{ value: '1', label: '生成并请求本机' },
{ value: '0', label: '仅生成请求体' },
]}
/>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={open91Mutation.isPending}>
模拟 91 查单
</Button>
</Form.Item>
</Form>
<Typography.Paragraph type="secondary" style={{ marginTop: 12, marginBottom: 0 }}>
对应日志:POST /api/v1/open/91/orders/query。需配置 KAQUAN91_SECRET32 位)。
</Typography.Paragraph>
</Card>
</Col>
</Row>
{lastCreate ? (
<Card
title="最近生成结果"
extra={
<Space>
<Link to={`/admin/orders/${lastCreate.orderId}`}>订单 #{lastCreate.orderId}</Link>
<Link to={`/admin/tasks/${lastCreate.taskId}`}>任务 #{lastCreate.taskId}</Link>
</Space>
}
>
<Row gutter={[16, 12]}>
<Col xs={24} md={12}>
<Typography.Text type="secondary">平台</Typography.Text>
<div>
<Typography.Text strong>{lastCreate.platform}</Typography.Text>
</div>
</Col>
<Col xs={24} md={12}>
<Typography.Text type="secondary">步骤 / UID</Typography.Text>
<div>
<Typography.Text strong>
{lastCreate.step} / {lastCreate.expectedUid}
</Typography.Text>
</div>
</Col>
<Col xs={24} md={12}>
<Typography.Text type="secondary">91 订单号</Typography.Text>
<div>
<Typography.Text copyable>{lastCreate.orderNo}</Typography.Text>
</div>
</Col>
<Col xs={24} md={12}>
<Typography.Text type="secondary">商品</Typography.Text>
<div>{lastCreate.productName}</div>
</Col>
<Col span={24}>
<Typography.Text type="secondary">领取链接</Typography.Text>
<div>
<Typography.Link href={lastCreate.claimUrl} target="_blank" rel="noreferrer">
<LinkOutlined /> {lastCreate.claimUrl}
</Typography.Link>
</div>
<Space style={{ marginTop: 8 }} wrap>
<Button
icon={<CopyOutlined />}
onClick={async () => {
await navigator.clipboard.writeText(lastCreate.claimUrl)
message.success('已复制领取链接')
}}
>
复制链接
</Button>
<Button type="primary" href={lastCreate.claimUrl} target="_blank">
打开领取页
</Button>
</Space>
</Col>
<Col span={24}>
<Alert
type="success"
showIcon
message="操作提示"
description={
<ul style={{ margin: 0, paddingLeft: 18 }}>
{lastCreate.tips.map((tip) => (
<li key={tip}>{tip}</li>
))}
<li>{lastCreate.open91QueryHint}</li>
</ul>
}
/>
</Col>
</Row>
</Card>
) : null}
{lastVoucherCreate ? (
<Card
title="最近生成的电子凭证 Mock"
extra={<Link to="/admin/kuaishou-industry">查看电子凭证</Link>}
>
<Typography.Paragraph type="secondary">
已为店铺 {lastVoucherCreate.sellerId} 写入 {lastVoucherCreate.createdCount} 条测试券码。
</Typography.Paragraph>
<Space direction="vertical" size={4}>
{lastVoucherCreate.vouchers.map((voucher) => (
<Typography.Text key={voucher.voucherCode} copyable>
{voucher.status} · {voucher.voucherCode} · {voucher.oid}
</Typography.Text>
))}
</Space>
</Card>
) : null}
{lastQuery ? (
<Card title="91 查单结果">
<Typography.Paragraph>
<Typography.Text type="secondary">Endpoint</Typography.Text>
<Typography.Text code copyable>
{lastQuery.endpoint}
</Typography.Text>
</Typography.Paragraph>
<Typography.Paragraph>
<Typography.Text type="secondary">Request</Typography.Text>
</Typography.Paragraph>
<pre className="dev-mock-pre">{JSON.stringify(lastQuery.requestBody, null, 2)}</pre>
<Typography.Paragraph>
<Typography.Text type="secondary">curl</Typography.Text>
<Typography.Text code copyable>
{lastQuery.curl}
</Typography.Text>
</Typography.Paragraph>
{lastQuery.response ? (
<>
<Divider />
<Typography.Paragraph>
<Typography.Text type="secondary">Response</Typography.Text>
</Typography.Paragraph>
<pre className="dev-mock-pre">{JSON.stringify(lastQuery.response, null, 2)}</pre>
</>
) : null}
</Card>
) : null}
</section>
)
}
function LewanMockForm({
defaultUid,
loading,
onSubmit,
}: {
defaultUid: string
loading: boolean
onSubmit: (values: Record<string, unknown>) => void
}) {
const [form] = Form.useForm()
const step = Form.useWatch('step', form)
const uidRequired = step !== 'uid'
return (
<Form
form={form}
layout="vertical"
initialValues={{
step: 'uid',
uid: defaultUid || '10001',
productNo: '套餐_1----3676797936',
}}
onFinish={onSubmit}
>
<Form.Item
label="页面步骤"
name="step"
tooltip="uid=只到填编号第一步;binding=已预填编号且角色匹配;confirm/result=后续步骤"
>
<Select
options={[
{ value: 'uid', label: 'uid · 待填 UID(第一步)' },
{ value: 'binding', label: 'binding · 绑定且 UID 已匹配(推荐)' },
{ value: 'confirm', label: 'confirm · 待确认兑换' },
{ value: 'result', label: 'result · 已完成' },
]}
/>
</Form.Item>
<Form.Item
label="游戏 UID"
name="uid"
required={uidRequired}
rules={
uidRequired
? [{ required: true, whitespace: true, message: '后续步骤请填写游戏 UID' }]
: []
}
extra={
step === 'uid'
? '「待填 UID」步骤可不填,打开领取页会停留在第一步让用户填写'
: '后续步骤用于模拟已提交的编号,建议与默认角色一致(如 10001)'
}
>
<Input allowClear placeholder={defaultUid || '10001'} />
</Form.Item>
<Form.Item label="91 商品 productNo" name="productNo">
<Input placeholder="套餐_1----3676797936" />
</Form.Item>
<Form.Item label="订单号 orderNo(可空自动生成)" name="orderNo">
<Input placeholder="留空自动 MOCK91…" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
生成 lewan Mock
</Button>
</Form>
)
}
function FeifeiMockForm({
loading,
onSubmit,
}: {
loading: boolean
onSubmit: (values: Record<string, unknown>) => void
}) {
const [form] = Form.useForm()
const step = Form.useWatch('step', form)
const uidRequired = step !== 'uid'
return (
<Form
form={form}
layout="vertical"
initialValues={{
step: 'uid',
uid: '166909256',
productName: '套装-浪漫天命',
}}
onFinish={onSubmit}
>
<Form.Item label="页面步骤" name="step">
<Select
options={[
{ value: 'uid', label: 'uid · 待填 UID(第一步)' },
{ value: 'ready', label: 'ready · 已填 UID,可打开 H5(推荐)' },
{ value: 'completed', label: 'completed · 模拟成功' },
{ value: 'failed', label: 'failed · 模拟失败' },
]}
/>
</Form.Item>
<Form.Item
label="游戏 UID"
name="uid"
required={uidRequired}
rules={
uidRequired
? [{ required: true, whitespace: true, message: '后续步骤请填写游戏 UID' }]
: []
}
extra={
step === 'uid'
? '「待填 UID」步骤可不填,打开领取页会停留在第一步让用户填写'
: '后续步骤用于模拟已提交的编号'
}
>
<Input allowClear placeholder="166909256" />
</Form.Item>
<Form.Item label="商品名" name="productName">
<Input />
</Form.Item>
<Form.Item label="H5 链接(可空用 mock 地址)" name="h5Url">
<Input placeholder="http://skin-exchange.../h5/bind?code=..." />
</Form.Item>
<Form.Item label="订单号 orderNo(可空)" name="orderNo">
<Input placeholder="留空自动 MOCKFF…" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
生成 feifei Mock
</Button>
</Form>
)
}
function AffiliateDashMockForm({
loading,
onSubmit,
}: {
loading: boolean
onSubmit: (values: Record<string, unknown>) => void
}) {
const [form] = Form.useForm()
const step = Form.useWatch('step', form)
const uidRequired = step !== 'uid'
return (
<Form
form={form}
layout="vertical"
initialValues={{
step: 'bind',
uid: '166909256',
productName: '幸运币90个',
productSku: 'lucky_coin_x90',
}}
onFinish={onSubmit}
>
<Form.Item label="页面步骤" name="step">
<Select
options={[
{ value: 'uid', label: 'uid · 待填 UID(第一步)' },
{ value: 'bind', label: 'bind · 已填 UID + mock 绑定二维码(第二步,推荐)' },
{ value: 'submitted', label: 'submitted · 已绑定待提交发货(第三步)' },
{ value: 'completed', label: 'completed · 模拟发货成功(第四步)' },
{ value: 'failed', label: 'failed · 模拟发货失败' },
]}
/>
</Form.Item>
<Form.Item
label="游戏 UID"
name="uid"
required={uidRequired}
rules={
uidRequired
? [{ required: true, whitespace: true, message: '后续步骤请填写游戏 UID' }]
: []
}
extra={
step === 'uid'
? '「待填 UID」步骤可不填,打开领取页会停留在第一步让用户填写'
: '后续步骤用于模拟已提交的编号'
}
>
<Input allowClear placeholder="166909256" />
</Form.Item>
<Form.Item label="商品名" name="productName">
<Input />
</Form.Item>
<Form.Item label="affiliate-dash sku(可空)" name="productSku">
<Input placeholder="lucky_coin_x90" />
</Form.Item>
<Form.Item label="订单号 orderNo(可空)" name="orderNo">
<Input placeholder="留空自动 MOCKAD…" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
生成 affiliate-dash Mock
</Button>
</Form>
)
}
function KuaishouIndustryVoucherMockForm({
loading,
onSubmit,
}: {
loading: boolean
onSubmit: (values: { sellerId?: string }) => void
}) {
return (
<Form layout="vertical" onFinish={onSubmit}>
<Form.Item
label="店铺 ID(可空)"
name="sellerId"
extra="留空时使用快手行业配置中的首个启用店铺;会生成 6 条不同状态的券码。"
>
<Input allowClear placeholder="留空自动选择测试店铺" />
</Form.Item>
<Button type="primary" htmlType="submit" loading={loading} block>
生成电子凭证 Mock
</Button>
</Form>
)
}