431 lines
14 KiB
TypeScript
431 lines
14 KiB
TypeScript
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,
|
||
createFeifeiDevMock,
|
||
createLewanDevMock,
|
||
fetchDevMockStatus,
|
||
type DevMockCreateResult,
|
||
type DevMockOpen91QueryResult,
|
||
} from '@/services/admin'
|
||
|
||
export default function AdminDevMockPage() {
|
||
const { message } = App.useApp()
|
||
const [lastCreate, setLastCreate] = useState<DevMockCreateResult | 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 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 默认 10001,feifei 默认 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}>
|
||
<Card title="3. 模拟 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_SECRET(32 位)。
|
||
</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}
|
||
|
||
{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>
|
||
)
|
||
}
|