feat(semi-auto): 支持微博半自动履约与手机号国家码登录
新增微博内置 Connector:扫码/短信登录、Cookie 获取与一次性浏览器票据;客户页支持国家码选择、短信/扫码登录及登录即授权;dev-mock 与后台任务页适配微博 Mock 单。
This commit is contained in:
@@ -304,6 +304,7 @@ export default function AdminDevMockPage() {
|
||||
<Col xs={24} md={12}>
|
||||
<Typography.Text type="secondary">内部 SKU / 任务</Typography.Text>
|
||||
<div>
|
||||
{lastSemiAuto.semiAutoPlatform === 'weibo' ? '微博' : '虎牙'} ·{' '}
|
||||
{lastSemiAuto.skuKey} · 任务 #{lastSemiAuto.taskId}
|
||||
</div>
|
||||
</Col>
|
||||
@@ -525,6 +526,7 @@ function SemiAutoMockForm({
|
||||
externalConnectorConfigured?: boolean
|
||||
mockConnectorAvailable?: boolean
|
||||
onSubmit: (values: {
|
||||
platform: 'huya' | 'weibo'
|
||||
orderNo?: string
|
||||
skuName?: string
|
||||
quantity?: number
|
||||
@@ -534,6 +536,8 @@ function SemiAutoMockForm({
|
||||
}) => void
|
||||
}) {
|
||||
const [form] = Form.useForm()
|
||||
const selectedPlatform =
|
||||
(Form.useWatch('platform', form) as 'huya' | 'weibo' | undefined) || 'huya'
|
||||
const defaultMode =
|
||||
defaultConnectorMode === 'mock' && mockConnectorAvailable
|
||||
? 'mock'
|
||||
@@ -549,6 +553,7 @@ function SemiAutoMockForm({
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
platform: 'huya',
|
||||
quantity: 1,
|
||||
pageTitle: '虎牙账号授权测试',
|
||||
browserTargetUrl: 'https://www.huya.com/',
|
||||
@@ -557,37 +562,73 @@ function SemiAutoMockForm({
|
||||
onFinish={onSubmit}
|
||||
>
|
||||
<Form.Item
|
||||
label="Mock 范围"
|
||||
name="connectorMode"
|
||||
extra="只 Mock 订单时,登录、角色绑定和浏览器票据注入可走内置真实 Connector,也可显式使用远程 Connector。"
|
||||
label="履约平台"
|
||||
name="platform"
|
||||
extra="微博 Mock 单登录成功即视为授权完成,不再有角色绑定步骤。"
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{
|
||||
value: 'embedded',
|
||||
label: '仅 Mock 订单(内置真实登录 / 绑定 / 注入)',
|
||||
},
|
||||
{
|
||||
value: 'external',
|
||||
label: '仅 Mock 订单(远程真实 Connector)',
|
||||
},
|
||||
{
|
||||
value: 'mock',
|
||||
label: '订单 + 登录绑定都 Mock(内置 Connector)',
|
||||
disabled: mockConnectorAvailable === false,
|
||||
},
|
||||
{ value: 'huya', label: '虎牙(扫码登录 + 游戏角色绑定)' },
|
||||
{ value: 'weibo', label: '微博(短信 / 扫码登录,登录即授权)' },
|
||||
]}
|
||||
onChange={(value: 'huya' | 'weibo') => {
|
||||
form.setFieldsValue({
|
||||
pageTitle: value === 'weibo' ? '微博发帖授权测试' : '虎牙账号授权测试',
|
||||
browserTargetUrl: value === 'weibo' ? 'https://weibo.com/' : 'https://www.huya.com/',
|
||||
...(value === 'weibo' ? { connectorMode: 'embedded' } : {}),
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
{selectedPlatform === 'huya' ? (
|
||||
<Form.Item
|
||||
label="Mock 范围"
|
||||
name="connectorMode"
|
||||
extra="只 Mock 订单时,登录、角色绑定和浏览器票据注入可走内置真实 Connector,也可显式使用远程 Connector。"
|
||||
>
|
||||
<Select
|
||||
options={[
|
||||
{
|
||||
value: 'embedded',
|
||||
label: '仅 Mock 订单(内置真实登录 / 绑定 / 注入)',
|
||||
},
|
||||
{
|
||||
value: 'external',
|
||||
label: '仅 Mock 订单(远程真实 Connector)',
|
||||
},
|
||||
{
|
||||
value: 'mock',
|
||||
label: '订单 + 登录绑定都 Mock(内置 Connector)',
|
||||
disabled: mockConnectorAvailable === false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
) : (
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message="微博目前只支持内置 Connector,连接模式固定为 embedded。"
|
||||
/>
|
||||
)}
|
||||
<Form.Item
|
||||
label="内部 SKU 名称(可空)"
|
||||
name="skuName"
|
||||
extra="留空自动生成“虎牙半自动测试-时间戳”;必须包含中文。"
|
||||
extra={`留空自动生成“${
|
||||
selectedPlatform === 'weibo' ? '微博半自动' : '虎牙半自动'
|
||||
}测试-时间戳”;必须包含中文。`}
|
||||
>
|
||||
<Input allowClear placeholder="虎牙半自动测试" />
|
||||
<Input
|
||||
allowClear
|
||||
placeholder={selectedPlatform === 'weibo' ? '微博半自动测试' : '虎牙半自动测试'}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Form.Item label="订单号 orderNo(可空)" name="orderNo">
|
||||
<Input allowClear placeholder="留空自动 MOCKSA…" />
|
||||
<Input
|
||||
allowClear
|
||||
placeholder={selectedPlatform === 'weibo' ? '留空自动 MOCKWB…' : '留空自动 MOCKSA…'}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Space size={12} style={{ display: 'flex' }}>
|
||||
<Form.Item label="数量" name="quantity" style={{ flex: 1 }}>
|
||||
@@ -602,26 +643,34 @@ function SemiAutoMockForm({
|
||||
name="browserTargetUrl"
|
||||
extra="客服点击“打开浏览器登录态”时,扩展会注入到此页面。"
|
||||
>
|
||||
<Input placeholder="https://www.huya.com/" />
|
||||
<Input
|
||||
placeholder={
|
||||
selectedPlatform === 'weibo' ? 'https://weibo.com/' : 'https://www.huya.com/'
|
||||
}
|
||||
/>
|
||||
</Form.Item>
|
||||
<Alert
|
||||
type={
|
||||
selectedMode === 'mock'
|
||||
? 'warning'
|
||||
: selectedMode === 'external' && !externalConnectorConfigured
|
||||
? 'error'
|
||||
: 'success'
|
||||
selectedPlatform === 'weibo'
|
||||
? 'info'
|
||||
: selectedMode === 'mock'
|
||||
? 'warning'
|
||||
: selectedMode === 'external' && !externalConnectorConfigured
|
||||
? 'error'
|
||||
: 'success'
|
||||
}
|
||||
showIcon
|
||||
style={{ marginBottom: 12 }}
|
||||
message={
|
||||
selectedMode === 'mock'
|
||||
? '登录和绑定会使用内置 Mock Connector,仅用于联调页面。'
|
||||
: selectedMode === 'external'
|
||||
? externalConnectorConfigured
|
||||
? '远程真实 Connector 已配置;只有订单由 Mock 生成。'
|
||||
: '远程真实 Connector 未配置:请设置 SEMI_AUTO_CONNECTOR_BASE_URL 和 API_KEY。'
|
||||
: '使用 order_site 内置真实 Connector;无需启动 live-hub-py,只有订单由 Mock 生成。'
|
||||
selectedPlatform === 'weibo'
|
||||
? '微博使用内置真实 Connector:短信验证码会真实发送到客户手机号,扫码需在微博 App 确认;登录成功即视为授权完成。'
|
||||
: selectedMode === 'mock'
|
||||
? '登录和绑定会使用内置 Mock Connector,仅用于联调页面。'
|
||||
: selectedMode === 'external'
|
||||
? externalConnectorConfigured
|
||||
? '远程真实 Connector 已配置;只有订单由 Mock 生成。'
|
||||
: '远程真实 Connector 未配置:请设置 SEMI_AUTO_CONNECTOR_BASE_URL 和 API_KEY。'
|
||||
: '使用 order_site 内置真实 Connector;无需启动 live-hub-py,只有订单由 Mock 生成。'
|
||||
}
|
||||
/>
|
||||
<Button type="primary" htmlType="submit" loading={loading} block>
|
||||
|
||||
@@ -290,7 +290,7 @@ export default function AdminSemiAutoTasksPage() {
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '绑定角色',
|
||||
title: '授权 / 绑定角色',
|
||||
key: 'binding',
|
||||
width: 190,
|
||||
render: (_, task) =>
|
||||
@@ -302,7 +302,7 @@ export default function AdminSemiAutoTasksPage() {
|
||||
</Text>
|
||||
</Space>
|
||||
) : (
|
||||
<Text type="secondary">未绑定</Text>
|
||||
<Text type="secondary">{task.platform === 'weibo' ? '未授权' : '未绑定'}</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -481,19 +481,29 @@ export default function AdminSemiAutoTasksPage() {
|
||||
{detail.task.displayStatus.label}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="虎牙 UID">{detail.task.huyaUid || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={detail.task.platform === 'weibo' ? '微博 UID' : '虎牙 UID'}>
|
||||
{detail.task.huyaUid || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="平台昵称">
|
||||
{detail.task.platformNickname || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="游戏账号">
|
||||
<Descriptions.Item label={detail.task.platform === 'weibo' ? '微博昵称' : '游戏账号'}>
|
||||
{detail.task.bindGameAccount || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="游戏角色">
|
||||
{detail.task.bindGameRole || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="区服" span={2}>
|
||||
{detail.task.bindGameChannel || '-'}
|
||||
</Descriptions.Item>
|
||||
{detail.task.platform === 'weibo' ? (
|
||||
<Descriptions.Item label="授权状态">
|
||||
{detail.task.bindingStatus === 'confirmed' ? '登录即授权' : '未授权'}
|
||||
</Descriptions.Item>
|
||||
) : (
|
||||
<Descriptions.Item label="游戏角色">
|
||||
{detail.task.bindGameRole || '-'}
|
||||
</Descriptions.Item>
|
||||
)}
|
||||
{detail.task.platform !== 'weibo' ? (
|
||||
<Descriptions.Item label="区服" span={2}>
|
||||
{detail.task.bindGameChannel || '-'}
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
<Descriptions.Item label="客服">
|
||||
{detail.task.assignedAdminName || '未领取'}
|
||||
</Descriptions.Item>
|
||||
|
||||
@@ -11,11 +11,14 @@ import {
|
||||
Card,
|
||||
Col,
|
||||
Image,
|
||||
Input,
|
||||
Result,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Spin,
|
||||
Steps,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
@@ -26,12 +29,18 @@ import { useParams } from 'react-router'
|
||||
import {
|
||||
confirmCustomerBinding,
|
||||
fetchCustomerFulfillment,
|
||||
fetchCustomerWeiboCountryCodes,
|
||||
loginCustomerWeiboSms,
|
||||
markCustomerFulfillmentOpened,
|
||||
pollCustomerBinding,
|
||||
pollCustomerLogin,
|
||||
pollCustomerWeiboQrLogin,
|
||||
sendCustomerWeiboSmsCode,
|
||||
startCustomerBinding,
|
||||
startCustomerLogin,
|
||||
startCustomerWeiboQrLogin,
|
||||
} from '@/services/customer-fulfillment'
|
||||
import type { CustomerWeiboCountryCode } from '@/services/customer-fulfillment'
|
||||
import type {
|
||||
CustomerBindingInfo,
|
||||
CustomerLoginInfo,
|
||||
@@ -43,6 +52,14 @@ const { Text, Title, Paragraph } = Typography
|
||||
|
||||
const POLL_INTERVAL_MS = 2000
|
||||
|
||||
const DEFAULT_WEIBO_COUNTRY_CODES: CustomerWeiboCountryCode[] = [
|
||||
{ code: '86', name: '中国' },
|
||||
{ code: '852', name: '中国香港' },
|
||||
{ code: '886', name: '中国台湾' },
|
||||
{ code: '853', name: '中国澳门' },
|
||||
{ code: '81', name: '日本' },
|
||||
]
|
||||
|
||||
export default function CustomerFulfillmentPage() {
|
||||
const { message } = App.useApp()
|
||||
const { orderNo: routeOrderNo } = useParams<{ orderNo?: string }>()
|
||||
@@ -55,6 +72,17 @@ export default function CustomerFulfillmentPage() {
|
||||
const [bindingInfo, setBindingInfo] = useState<CustomerBindingInfo | null>(null)
|
||||
const [loginQrImage, setLoginQrImage] = useState('')
|
||||
const [bindingActionLoading, setBindingActionLoading] = useState(false)
|
||||
const [loginMode, setLoginMode] = useState<'sms' | 'qr'>('sms')
|
||||
const [mobile, setMobile] = useState('')
|
||||
const [countryCode, setCountryCode] = useState('86')
|
||||
const [countryCodes, setCountryCodes] = useState<CustomerWeiboCountryCode[]>(
|
||||
DEFAULT_WEIBO_COUNTRY_CODES,
|
||||
)
|
||||
const [smsCode, setSmsCode] = useState('')
|
||||
const [smsSending, setSmsSending] = useState(false)
|
||||
const [smsLogging, setSmsLogging] = useState(false)
|
||||
const [smsCountdown, setSmsCountdown] = useState(0)
|
||||
const [smsMfaRequired, setSmsMfaRequired] = useState(false)
|
||||
const startingRef = useRef(false)
|
||||
const hydratingBindingRef = useRef(false)
|
||||
|
||||
@@ -62,6 +90,7 @@ export default function CustomerFulfillmentPage() {
|
||||
() => tasks.find((task) => task.id === selectedTaskId) || tasks[0] || null,
|
||||
[selectedTaskId, tasks],
|
||||
)
|
||||
const isWeibo = selectedTask?.platform === 'weibo'
|
||||
|
||||
const load = useCallback(
|
||||
async (silent = false) => {
|
||||
@@ -101,6 +130,21 @@ export default function CustomerFulfillmentPage() {
|
||||
void markCustomerFulfillmentOpened(orderNo, selectedTask.id).catch(() => undefined)
|
||||
}, [orderNo, selectedTask?.id])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTask || selectedTask.platform !== 'weibo' || !orderNo) return
|
||||
let cancelled = false
|
||||
void fetchCustomerWeiboCountryCodes(orderNo, selectedTask.id)
|
||||
.then((response) => {
|
||||
if (cancelled) return
|
||||
const list = response.data?.countryCodes || []
|
||||
if (list.length > 0) setCountryCodes(list)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [orderNo, selectedTask?.id, selectedTask?.platform])
|
||||
|
||||
useEffect(() => {
|
||||
if (loginInfo?.qrImage) {
|
||||
setLoginQrImage(loginInfo.qrImage)
|
||||
@@ -117,12 +161,21 @@ export default function CustomerFulfillmentPage() {
|
||||
}).then(setLoginQrImage)
|
||||
}, [loginInfo?.qrContent, loginInfo?.qrImage])
|
||||
|
||||
useEffect(() => {
|
||||
if (smsCountdown <= 0) return
|
||||
const timer = window.setTimeout(() => setSmsCountdown((value) => value - 1), 1000)
|
||||
return () => window.clearTimeout(timer)
|
||||
}, [smsCountdown])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTask || !orderNo) return
|
||||
if (!['qr_pending', 'scanned'].includes(selectedTask.loginStatus)) return
|
||||
const tick = async () => {
|
||||
try {
|
||||
const response = await pollCustomerLogin(orderNo, selectedTask.id)
|
||||
const response =
|
||||
selectedTask.platform === 'weibo'
|
||||
? await pollCustomerWeiboQrLogin(orderNo, selectedTask.id)
|
||||
: await pollCustomerLogin(orderNo, selectedTask.id)
|
||||
if (response.data.task) {
|
||||
setTasks((current) =>
|
||||
current.map((task) => (task.id === response.data.task.id ? response.data.task : task)),
|
||||
@@ -137,10 +190,10 @@ export default function CustomerFulfillmentPage() {
|
||||
void tick()
|
||||
}, POLL_INTERVAL_MS)
|
||||
return () => window.clearInterval(timer)
|
||||
}, [orderNo, selectedTask?.id, selectedTask?.loginStatus])
|
||||
}, [orderNo, selectedTask?.id, selectedTask?.loginStatus, selectedTask?.platform])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTask || !orderNo) return
|
||||
if (!selectedTask || !orderNo || selectedTask.platform === 'weibo') return
|
||||
if (
|
||||
selectedTask.loginStatus !== 'authenticated' ||
|
||||
selectedTask.bindingStatus === 'confirmed'
|
||||
@@ -185,7 +238,14 @@ export default function CustomerFulfillmentPage() {
|
||||
}, [message, orderNo, selectedTask?.bindingStatus, selectedTask?.id, selectedTask?.loginStatus])
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedTask || !orderNo || bindingInfo || hydratingBindingRef.current) return
|
||||
if (
|
||||
!selectedTask ||
|
||||
!orderNo ||
|
||||
selectedTask.platform === 'weibo' ||
|
||||
bindingInfo ||
|
||||
hydratingBindingRef.current
|
||||
)
|
||||
return
|
||||
if (
|
||||
selectedTask.loginStatus !== 'authenticated' ||
|
||||
selectedTask.bindingStatus !== 'role_ready'
|
||||
@@ -216,7 +276,10 @@ export default function CustomerFulfillmentPage() {
|
||||
if (!selectedTask) return
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await startCustomerLogin(orderNo, selectedTask.id)
|
||||
const response =
|
||||
selectedTask.platform === 'weibo'
|
||||
? await startCustomerWeiboQrLogin(orderNo, selectedTask.id)
|
||||
: await startCustomerLogin(orderNo, selectedTask.id)
|
||||
setTasks((current) =>
|
||||
current.map((task) => (task.id === response.data.task.id ? response.data.task : task)),
|
||||
)
|
||||
@@ -229,6 +292,69 @@ export default function CustomerFulfillmentPage() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSendWeiboSms() {
|
||||
if (!selectedTask) return
|
||||
const phone = mobile.trim()
|
||||
if (!isValidWeiboMobile(countryCode, phone)) {
|
||||
message.warning('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
setSmsSending(true)
|
||||
try {
|
||||
const response = await sendCustomerWeiboSmsCode(orderNo, selectedTask.id, phone, countryCode)
|
||||
message.success(response.data.message || '验证码已发送,请注意查收')
|
||||
setSmsMfaRequired(false)
|
||||
setSmsCountdown(60)
|
||||
} catch (error) {
|
||||
const errorCode = (error as { errorCode?: string } | null)?.errorCode || ''
|
||||
if (errorCode === 'weibo_sms_mfa_required') {
|
||||
setSmsMfaRequired(true)
|
||||
message.warning('微博要求二次验证,请改用扫码登录')
|
||||
return
|
||||
}
|
||||
message.error(error instanceof Error ? error.message : '发送微博验证码失败')
|
||||
} finally {
|
||||
setSmsSending(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLoginWeiboSms() {
|
||||
if (!selectedTask) return
|
||||
const phone = mobile.trim()
|
||||
if (!isValidWeiboMobile(countryCode, phone)) {
|
||||
message.warning('请输入正确的手机号')
|
||||
return
|
||||
}
|
||||
if (!smsCode.trim()) {
|
||||
message.warning('请输入微博短信验证码')
|
||||
return
|
||||
}
|
||||
setSmsLogging(true)
|
||||
try {
|
||||
const response = await loginCustomerWeiboSms(orderNo, selectedTask.id, {
|
||||
mobile: phone,
|
||||
code: smsCode.trim(),
|
||||
countryCode,
|
||||
})
|
||||
setTasks((current) =>
|
||||
current.map((task) => (task.id === response.data.task.id ? response.data.task : task)),
|
||||
)
|
||||
setLoginInfo(response.data.login)
|
||||
setBindingInfo(null)
|
||||
message.success('微博登录成功,等待客服处理')
|
||||
} catch (error) {
|
||||
const errorCode = (error as { errorCode?: string } | null)?.errorCode || ''
|
||||
if (errorCode === 'weibo_sms_mfa_required') {
|
||||
setSmsMfaRequired(true)
|
||||
message.warning('微博要求二次验证,请改用扫码登录')
|
||||
return
|
||||
}
|
||||
message.error(error instanceof Error ? error.message : '微博短信登录失败')
|
||||
} finally {
|
||||
setSmsLogging(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmBinding() {
|
||||
if (!selectedTask) return
|
||||
setBindingActionLoading(true)
|
||||
@@ -294,7 +420,9 @@ export default function CustomerFulfillmentPage() {
|
||||
subTitle={
|
||||
selectedTask.completionNote
|
||||
? selectedTask.completionNote
|
||||
: '客服已完成本订单的充值或赠送操作,请到平台账号中确认。'
|
||||
: isWeibo
|
||||
? '客服已完成本订单的微博发帖操作,请到微博账号中确认。'
|
||||
: '客服已完成本订单的充值或赠送操作,请到平台账号中确认。'
|
||||
}
|
||||
extra={
|
||||
selectedTask.completedAt
|
||||
@@ -307,13 +435,17 @@ export default function CustomerFulfillmentPage() {
|
||||
}
|
||||
|
||||
const config = selectedTask?.semiAutoConfig || {}
|
||||
const title = config.pageTitle || '账号授权与角色绑定'
|
||||
const title = config.pageTitle || (isWeibo ? '微博账号授权' : '账号授权与角色绑定')
|
||||
const description =
|
||||
config.pageDescription || '请按页面指引完成平台登录和游戏角色绑定,客服随后会处理您的订单。'
|
||||
config.pageDescription ||
|
||||
(isWeibo
|
||||
? '请使用本人微博账号完成登录,登录成功后客服会处理您的订单。'
|
||||
: '请按页面指引完成平台登录和游戏角色绑定,客服随后会处理您的订单。')
|
||||
const currentStep = resolveStep(selectedTask, bindingInfo)
|
||||
const loginQrVisible =
|
||||
selectedTask && ['qr_pending', 'scanned'].includes(selectedTask.loginStatus)
|
||||
const bindingQrVisible =
|
||||
selectedTask?.platform !== 'weibo' &&
|
||||
selectedTask?.loginStatus === 'authenticated' &&
|
||||
['qr_pending', 'scanned', 'role_ready'].includes(selectedTask.bindingStatus)
|
||||
const bindingQrPending = ['qr_pending', 'scanned'].includes(selectedTask?.bindingStatus || '')
|
||||
@@ -374,12 +506,20 @@ export default function CustomerFulfillmentPage() {
|
||||
<Col xs={24} lg={16}>
|
||||
<Steps
|
||||
current={currentStep}
|
||||
items={[
|
||||
{ title: '登录平台', icon: <QrcodeOutlined /> },
|
||||
{ title: '扫码绑定', icon: <SafetyCertificateOutlined /> },
|
||||
{ title: '客服处理', icon: <LoadingOutlined /> },
|
||||
{ title: '完成' },
|
||||
]}
|
||||
items={
|
||||
isWeibo
|
||||
? [
|
||||
{ title: '登录微博', icon: <QrcodeOutlined /> },
|
||||
{ title: '客服处理', icon: <LoadingOutlined /> },
|
||||
{ title: '完成' },
|
||||
]
|
||||
: [
|
||||
{ title: '登录平台', icon: <QrcodeOutlined /> },
|
||||
{ title: '扫码绑定', icon: <SafetyCertificateOutlined /> },
|
||||
{ title: '客服处理', icon: <LoadingOutlined /> },
|
||||
{ title: '完成' },
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
@@ -397,35 +537,160 @@ export default function CustomerFulfillmentPage() {
|
||||
{selectedTask?.loginStatus === 'waiting_login' ||
|
||||
selectedTask?.loginStatus === 'failed' ||
|
||||
selectedTask?.loginStatus === 'expired' ? (
|
||||
<Card title="第一步:登录平台账号">
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="请使用本人账号登录"
|
||||
description={
|
||||
config.loginInstructions ||
|
||||
'点击下方按钮后使用虎牙 App 扫码登录。登录仅用于本次订单履约,不会在页面展示账号 Cookie。'
|
||||
}
|
||||
<Card title={isWeibo ? '第一步:登录微博账号' : '第一步:登录平台账号'}>
|
||||
{isWeibo ? (
|
||||
<Tabs
|
||||
activeKey={loginMode}
|
||||
onChange={(key) => setLoginMode(key as 'sms' | 'qr')}
|
||||
items={[
|
||||
{
|
||||
key: 'sms',
|
||||
label: '手机号登录',
|
||||
children: (
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="使用微博绑定手机号登录"
|
||||
description={
|
||||
config.loginInstructions ||
|
||||
'验证码将发送到你的微博绑定手机号。登录仅用于本次订单履约,不会在页面展示账号 Cookie。'
|
||||
}
|
||||
/>
|
||||
{smsMfaRequired ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message="微博要求二次验证"
|
||||
description="当前网络环境触发了微博人机验证,短信登录无法自动完成,请改用扫码登录。"
|
||||
action={
|
||||
<Button size="small" onClick={() => setLoginMode('qr')}>
|
||||
去扫码
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
<Space.Compact style={{ width: '100%', maxWidth: 520 }}>
|
||||
<Select
|
||||
value={countryCode}
|
||||
onChange={(value) => setCountryCode(String(value))}
|
||||
style={{ width: 110 }}
|
||||
options={countryCodes.map((item) => ({
|
||||
value: item.code,
|
||||
label: `+${item.code}`,
|
||||
name: item.name,
|
||||
}))}
|
||||
optionRender={(option) => (
|
||||
<Space size={6}>
|
||||
<span>{(option.data as { name?: string }).name}</span>
|
||||
<Text type="secondary">+{option.data.value}</Text>
|
||||
</Space>
|
||||
)}
|
||||
filterOption={(input, option) =>
|
||||
String(
|
||||
(option as { name?: string } | undefined)?.name || '',
|
||||
).includes(input) || String(option?.value || '').includes(input)
|
||||
}
|
||||
showSearch
|
||||
popupMatchSelectWidth={240}
|
||||
/>
|
||||
<Input
|
||||
value={mobile}
|
||||
onChange={(event) => setMobile(event.target.value)}
|
||||
placeholder="手机号"
|
||||
maxLength={countryCode === '86' ? 11 : 15}
|
||||
inputMode="numeric"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<Button
|
||||
loading={smsSending}
|
||||
disabled={smsCountdown > 0}
|
||||
onClick={() => void handleSendWeiboSms()}
|
||||
>
|
||||
{smsCountdown > 0 ? `重新发送(${smsCountdown}s)` : '发送验证码'}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<Space.Compact style={{ width: '100%', maxWidth: 520 }}>
|
||||
<Input
|
||||
value={smsCode}
|
||||
onChange={(event) => setSmsCode(event.target.value)}
|
||||
placeholder="请输入短信验证码"
|
||||
maxLength={8}
|
||||
onPressEnter={() => void handleLoginWeiboSms()}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
loading={smsLogging}
|
||||
onClick={() => void handleLoginWeiboSms()}
|
||||
>
|
||||
登录微博
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<Text type="secondary">
|
||||
如微博要求二次验证导致登录失败,请改用扫码登录。
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'qr',
|
||||
label: '扫码登录',
|
||||
children: (
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="请使用本人账号登录"
|
||||
description={
|
||||
config.loginInstructions ||
|
||||
'点击下方按钮后使用微博 App 扫码登录。登录仅用于本次订单履约,不会在页面展示账号 Cookie。'
|
||||
}
|
||||
/>
|
||||
<Button type="primary" size="large" onClick={() => void handleStartLogin()}>
|
||||
获取登录二维码
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Button type="primary" size="large" onClick={() => void handleStartLogin()}>
|
||||
获取登录二维码
|
||||
</Button>
|
||||
</Space>
|
||||
) : (
|
||||
<Space direction="vertical" size={14} style={{ width: '100%' }}>
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="请使用本人账号登录"
|
||||
description={
|
||||
config.loginInstructions ||
|
||||
'点击下方按钮后使用虎牙 App 扫码登录。登录仅用于本次订单履约,不会在页面展示账号 Cookie。'
|
||||
}
|
||||
/>
|
||||
<Button type="primary" size="large" onClick={() => void handleStartLogin()}>
|
||||
获取登录二维码
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{loginQrVisible ? (
|
||||
<Card title="使用虎牙 App 扫码登录">
|
||||
<Card title={isWeibo ? '使用微博 App 扫码登录' : '使用虎牙 App 扫码登录'}>
|
||||
<Space direction="vertical" align="center" size={14} style={{ width: '100%' }}>
|
||||
{loginQrImage ? (
|
||||
<Image src={loginQrImage} alt="虎牙登录二维码" width={260} preview={false} />
|
||||
<Image
|
||||
src={loginQrImage}
|
||||
alt={isWeibo ? '微博登录二维码' : '虎牙登录二维码'}
|
||||
width={260}
|
||||
preview={false}
|
||||
/>
|
||||
) : (
|
||||
<Spin tip="生成二维码..." />
|
||||
)}
|
||||
<Text type="secondary">{loginInfo?.message || '等待扫码'}</Text>
|
||||
{selectedTask.loginStatus === 'scanned' ? (
|
||||
<Tag color="cyan">已扫码,请在 App 确认</Tag>
|
||||
<Tag color="cyan">
|
||||
{isWeibo ? '已扫码,请在微博客户端确认登录' : '已扫码,请在 App 确认'}
|
||||
</Tag>
|
||||
) : null}
|
||||
<Button onClick={() => void handleStartLogin()}>重新获取二维码</Button>
|
||||
</Space>
|
||||
@@ -503,8 +768,12 @@ export default function CustomerFulfillmentPage() {
|
||||
<Card>
|
||||
<Result
|
||||
status="info"
|
||||
title="账号已绑定,客服正在处理"
|
||||
subTitle="客服会使用本次授权完成充值或赠送,请稍后刷新页面查看结果。"
|
||||
title={isWeibo ? '微博账号已授权,客服正在处理' : '账号已绑定,客服正在处理'}
|
||||
subTitle={
|
||||
isWeibo
|
||||
? '客服会使用本次授权完成微博发帖,请稍后刷新页面查看结果。'
|
||||
: '客服会使用本次授权完成充值或赠送,请稍后刷新页面查看结果。'
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
) : null}
|
||||
@@ -513,8 +782,20 @@ export default function CustomerFulfillmentPage() {
|
||||
)
|
||||
}
|
||||
|
||||
function isValidWeiboMobile(countryCode: string, value: string) {
|
||||
const digits = value.replace(/\D/g, '')
|
||||
if (!digits) return false
|
||||
if (countryCode === '86') return /^1\d{10}$/.test(digits)
|
||||
return digits.length >= 5 && digits.length <= 15
|
||||
}
|
||||
|
||||
function resolveStep(task: CustomerSemiAutoTask | null, binding: CustomerBindingInfo | null) {
|
||||
if (!task) return 0
|
||||
if (task.platform === 'weibo') {
|
||||
if (task.executionStatus === 'completed') return 2
|
||||
if (task.bindingStatus === 'confirmed') return 1
|
||||
return 0
|
||||
}
|
||||
if (task.executionStatus === 'completed') return 3
|
||||
if (task.bindingStatus === 'confirmed') return 2
|
||||
if (task.loginStatus === 'authenticated' || binding) return 1
|
||||
|
||||
@@ -53,6 +53,7 @@ export type DevMockKuaishouIndustryVoucherResult = {
|
||||
|
||||
export type DevMockSemiAutoResult = {
|
||||
platform: 'semi_auto'
|
||||
semiAutoPlatform: 'huya' | 'weibo'
|
||||
orderId: number
|
||||
orderNo: string
|
||||
orderItemId: number
|
||||
@@ -89,6 +90,7 @@ export function createKuaishouIndustryVoucherDevMock(payload: { sellerId?: strin
|
||||
}
|
||||
|
||||
export function createSemiAutoDevMock(payload: {
|
||||
platform?: 'huya' | 'weibo'
|
||||
orderNo?: string
|
||||
skuName?: string
|
||||
quantity?: number
|
||||
|
||||
@@ -54,3 +54,49 @@ export function confirmCustomerBinding(orderNo: string, taskId: number) {
|
||||
{},
|
||||
)
|
||||
}
|
||||
|
||||
export type CustomerWeiboCountryCode = {
|
||||
code: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export function fetchCustomerWeiboCountryCodes(orderNo: string, taskId: number) {
|
||||
return apiGet<{ countryCodes: CustomerWeiboCountryCode[] }>(
|
||||
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/weibo/config`,
|
||||
)
|
||||
}
|
||||
|
||||
export function sendCustomerWeiboSmsCode(
|
||||
orderNo: string,
|
||||
taskId: number,
|
||||
mobile: string,
|
||||
countryCode: string,
|
||||
) {
|
||||
return apiPost<{ ok: boolean; message: string }>(
|
||||
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/weibo/sms/send`,
|
||||
{ mobile, countryCode },
|
||||
)
|
||||
}
|
||||
|
||||
export function loginCustomerWeiboSms(
|
||||
orderNo: string,
|
||||
taskId: number,
|
||||
input: { mobile: string; code: string; countryCode: string },
|
||||
) {
|
||||
return apiPost<{ task: CustomerSemiAutoTask; login: CustomerLoginInfo }>(
|
||||
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/weibo/sms/login`,
|
||||
input,
|
||||
)
|
||||
}
|
||||
|
||||
export function startCustomerWeiboQrLogin(orderNo: string, taskId: number) {
|
||||
return apiGet<{ task: CustomerSemiAutoTask; login: CustomerLoginInfo }>(
|
||||
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/weibo/qrcode`,
|
||||
)
|
||||
}
|
||||
|
||||
export function pollCustomerWeiboQrLogin(orderNo: string, taskId: number) {
|
||||
return apiGet<{ task: CustomerSemiAutoTask; login: CustomerLoginInfo | null }>(
|
||||
`/api/v1/customer-fulfillment/${encodeURIComponent(orderNo)}/tasks/${taskId}/weibo/qrcode/status`,
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user