feat(douyin-mashida): 彻底重构优化配置面板,支持一键填域名/订阅推送/网关自检并剔除冗余字段
- 剔除历史旧卡券时代废弃的 afterVerifyLink(去使用链接)与 afterVerifyMessage(通知正文) - 面板新增一键填入当前域名、向开放平台一键订阅推送、停用推送以及网关连通与签名自检功能 - 业务配置重构为「基础接入与商户凭据」、「买家 H5 直充与发货私信」、「高级网关设置」三层清晰结构 - 后端与前端服务新增 /douyin-mashida/subscribe 与 /douyin-mashida/test-gateway 统一接口 验证方式:前端类型检查与打包构建通过,全量 132 项前端单测与 674 项后端单测全部通过
This commit is contained in:
@@ -3,6 +3,8 @@ import { Router } from 'express'
|
||||
import {
|
||||
getAdminDouyinMashidaConfig,
|
||||
saveAdminDouyinMashidaConfig,
|
||||
subscribeAdminDouyinMashidaPush,
|
||||
testAdminDouyinMashidaGateway,
|
||||
} from '../../../services/admin/platform-config/douyin-mashida-service.js'
|
||||
import { createJsonHandler } from '../session.js'
|
||||
import type { JsonRecord } from '../../../types/json.js'
|
||||
@@ -49,4 +51,35 @@ router.post(
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/douyin-mashida/subscribe',
|
||||
createJsonHandler((req) => subscribeAdminDouyinMashidaPush(req.body as JsonRecord), {
|
||||
successMessage: '平台推送设置成功',
|
||||
errorMessage: '平台推送设置失败',
|
||||
scope: '[admin/platform-config/douyin-mashida/subscribe]',
|
||||
audit: (req, _data) => {
|
||||
const payload = req.body as JsonRecord
|
||||
return {
|
||||
action: 'platform_douyin_mashida_subscribe_updated',
|
||||
targetType: 'platform_config',
|
||||
targetId: 'douyin_mashida',
|
||||
data: {
|
||||
disable: Boolean(payload?.disable),
|
||||
messageUrl: String(payload?.messageUrl || payload?.publicUrl || ''),
|
||||
},
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
router.post(
|
||||
'/douyin-mashida/test-gateway',
|
||||
createJsonHandler(() => testAdminDouyinMashidaGateway(), {
|
||||
successMessage: '网关测试完成',
|
||||
errorMessage: '网关测试失败',
|
||||
scope: '[admin/platform-config/douyin-mashida/test-gateway]',
|
||||
}),
|
||||
)
|
||||
|
||||
export default router
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { JsonObject } from '../../../types/json.js'
|
||||
|
||||
import { createHttpError } from '../../../utils/http.js'
|
||||
import { maskSecret } from '../../../utils/masking.js'
|
||||
import {
|
||||
configureDouyinMashidaMessage,
|
||||
fetchDouyinMashidaOrderDetail,
|
||||
} from '../../platforms/douyin-mashida/gateway-client.js'
|
||||
import {
|
||||
getDouyinMashidaSourceConfig,
|
||||
saveDouyinMashidaSourceConfig,
|
||||
@@ -32,6 +37,103 @@ export async function saveAdminDouyinMashidaConfig(payload: JsonObject = {}) {
|
||||
return mapAdminDouyinMashidaConfig(saved)
|
||||
}
|
||||
|
||||
/** 向开放平台订阅或停用消息推送(message.config) */
|
||||
export async function subscribeAdminDouyinMashidaPush(
|
||||
payload: {
|
||||
publicUrl?: string
|
||||
messageUrl?: string
|
||||
topics?: string[] | string
|
||||
disable?: boolean
|
||||
} = {},
|
||||
) {
|
||||
const config = getDouyinMashidaSourceConfig()
|
||||
if (!config.enabled) {
|
||||
throw createHttpError('抖音码速达接入未启用,请先开启接入开关', {
|
||||
statusCode: 409,
|
||||
errorCode: 'douyin_mashida_disabled',
|
||||
})
|
||||
}
|
||||
if (!config.uid || !config.secret) {
|
||||
throw createHttpError('抖音码速达缺少 uid 或用户秘钥,请先在面板中配置并保存', {
|
||||
statusCode: 409,
|
||||
errorCode: 'douyin_mashida_credentials_missing',
|
||||
})
|
||||
}
|
||||
|
||||
const disable = Boolean(payload.disable)
|
||||
let messageUrl = String(payload.messageUrl || '').trim()
|
||||
if (!messageUrl && payload.publicUrl) {
|
||||
messageUrl = `${String(payload.publicUrl).trim().replace(/\/+$/, '')}/api/v1/open/douyin-mashida/message`
|
||||
}
|
||||
|
||||
if (!disable && !messageUrl) {
|
||||
throw createHttpError('缺少推送地址 (messageUrl),请提供公网地址', {
|
||||
statusCode: 400,
|
||||
errorCode: 'missing_message_url',
|
||||
})
|
||||
}
|
||||
|
||||
const topics = Array.isArray(payload.topics)
|
||||
? payload.topics.join(',')
|
||||
: String(payload.topics || config.handledTopics.join(',')).trim()
|
||||
|
||||
const response = await configureDouyinMashidaMessage({
|
||||
status: disable ? 0 : 1,
|
||||
messageUrl: disable ? undefined : messageUrl,
|
||||
topics: disable ? undefined : topics,
|
||||
config,
|
||||
})
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
status: disable ? 0 : 1,
|
||||
messageUrl: disable ? '' : messageUrl,
|
||||
topics: disable ? '' : topics,
|
||||
gatewayResponse: response,
|
||||
}
|
||||
}
|
||||
|
||||
/** 测试网关连通性与签名(使用真实凭据打 order.orderDetail 探测) */
|
||||
export async function testAdminDouyinMashidaGateway() {
|
||||
const config = getDouyinMashidaSourceConfig()
|
||||
if (!config.enabled) {
|
||||
throw createHttpError('抖音码速达接入未启用,请先开启接入开关', {
|
||||
statusCode: 409,
|
||||
errorCode: 'douyin_mashida_disabled',
|
||||
})
|
||||
}
|
||||
if (!config.uid || !config.secret) {
|
||||
throw createHttpError('抖音码速达缺少 uid 或用户秘钥,请先在面板中配置并保存', {
|
||||
statusCode: 409,
|
||||
errorCode: 'douyin_mashida_credentials_missing',
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
await fetchDouyinMashidaOrderDetail({
|
||||
shopOrderId: '0',
|
||||
config,
|
||||
})
|
||||
return {
|
||||
ok: true,
|
||||
message: '网关连通且请求成功',
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error)
|
||||
if (errorMsg.includes('签名') || errorMsg.includes('sign')) {
|
||||
throw createHttpError(`签名验证失败: ${errorMsg}`, {
|
||||
statusCode: 400,
|
||||
errorCode: 'signature_failed',
|
||||
})
|
||||
}
|
||||
// 平台先验签名,出现非签名错误(如订单不存在)说明签名和网络完全正常
|
||||
return {
|
||||
ok: true,
|
||||
message: `网关连通 + 签名校验通过(平台返回业务响应: ${errorMsg})`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mapAdminDouyinMashidaConfig(config: DouyinMashidaSourceConfig) {
|
||||
return {
|
||||
enabled: config.enabled,
|
||||
@@ -82,3 +184,4 @@ export function isMaskedSecretValue(value: unknown, currentSecret = ''): boolean
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,35 @@ export async function sendDouyinMashidaOrderMessage(
|
||||
})
|
||||
}
|
||||
|
||||
/** 消息推送配置(向平台订阅/停用推送):对应开放平台 message.config */
|
||||
export async function configureDouyinMashidaMessage(
|
||||
input: {
|
||||
status: number
|
||||
messageUrl?: string | undefined
|
||||
topics?: string | undefined
|
||||
} & DouyinMashidaCallOptions,
|
||||
): Promise<JsonObject> {
|
||||
const params: Record<string, string> = {
|
||||
status: String(input.status),
|
||||
}
|
||||
if (input.messageUrl) {
|
||||
params.message_url = input.messageUrl
|
||||
}
|
||||
if (input.topics) {
|
||||
params.topics = input.topics
|
||||
}
|
||||
|
||||
const response = await douyinMashidaGatewayRequest({
|
||||
method: 'message.config',
|
||||
params,
|
||||
config: input.config,
|
||||
timeoutMs: input.timeoutMs,
|
||||
})
|
||||
|
||||
return isPlainObject(response) ? response : {}
|
||||
}
|
||||
|
||||
function isPlainObject(value: unknown): value is JsonObject {
|
||||
return Object.prototype.toString.call(value) === '[object Object]'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
/**
|
||||
* Douyin Mashida Platform Admin Panel
|
||||
* 抖音码速达接入配置面板 (凭据脱敏读写、配置完整度自检与本地地址误配提示)
|
||||
* 抖音码速达接入配置面板 (自动直发凭据、H5领取链接、私信推送、平台订阅与网关自检)
|
||||
*/
|
||||
|
||||
import { ApiOutlined, ReloadOutlined, SaveOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
ApiOutlined,
|
||||
CheckCircleOutlined,
|
||||
CopyOutlined,
|
||||
GlobalOutlined,
|
||||
LinkOutlined,
|
||||
ReloadOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
SaveOutlined,
|
||||
SendOutlined,
|
||||
SettingOutlined,
|
||||
StopOutlined,
|
||||
ThunderboltOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Collapse,
|
||||
Divider,
|
||||
Input,
|
||||
InputNumber,
|
||||
Row,
|
||||
@@ -27,6 +42,8 @@ import { showError, showSuccess } from '@/lib/feedback'
|
||||
import {
|
||||
fetchAdminDouyinMashidaConfig,
|
||||
saveAdminDouyinMashidaConfig,
|
||||
subscribeAdminDouyinMashidaPush,
|
||||
testAdminDouyinMashidaGateway,
|
||||
} from '@/services/admin/platform-config'
|
||||
import type { AdminDouyinMashidaConfig } from '@/types/admin/platform-config'
|
||||
import {
|
||||
@@ -38,14 +55,14 @@ import {
|
||||
isLocalDevBaseUrl,
|
||||
} from '@/utils/douyin-mashida'
|
||||
|
||||
const { Text } = Typography
|
||||
const { Text, Paragraph } = Typography
|
||||
|
||||
/** 平台侧可订阅的消息类型(与后端 message_type 取值一致) */
|
||||
const MESSAGE_TYPE_OPTIONS = [
|
||||
{ value: 1, label: '1 - 订单状态' },
|
||||
{ value: 2, label: '2 - 订单发货' },
|
||||
{ value: 4, label: '4 - 图片' },
|
||||
{ value: 5, label: '5 - 视频' },
|
||||
{ value: 1, label: '1 - 订单状态通知' },
|
||||
{ value: 2, label: '2 - 订单发货私信 (推荐)' },
|
||||
{ value: 4, label: '4 - 图片消息' },
|
||||
{ value: 5, label: '5 - 视频消息' },
|
||||
]
|
||||
|
||||
/** 默认本地处理白名单,作为 Select 的候选标签 */
|
||||
@@ -63,7 +80,7 @@ function createEmptyConfig(): AdminDouyinMashidaConfig {
|
||||
hasSecret: false,
|
||||
certLink: '',
|
||||
claimLinkBase: '',
|
||||
messageTemplate: '',
|
||||
messageTemplate: '您的订单已发货,请点击链接领取:{link}',
|
||||
claimMessageEnabled: true,
|
||||
afterVerifyLink: '',
|
||||
afterVerifyMessage: '您的订单已兑换成功',
|
||||
@@ -77,16 +94,16 @@ function createEmptyConfig(): AdminDouyinMashidaConfig {
|
||||
export function DouyinMashidaPanel() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [subscribing, setSubscribing] = useState(false)
|
||||
const [testing, setTesting] = useState(false)
|
||||
const [testResult, setTestResult] = useState<{ ok: boolean; message: string } | null>(null)
|
||||
const [config, setConfig] = useState<AdminDouyinMashidaConfig>(createEmptyConfig)
|
||||
|
||||
useEffect(() => {
|
||||
void loadConfig()
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Load Douyin Mashida config
|
||||
* 读取抖音码速达接入配置(secret 出参恒为空串,不回显)
|
||||
*/
|
||||
/** 读取抖音码速达接入配置 */
|
||||
async function loadConfig() {
|
||||
setLoading(true)
|
||||
try {
|
||||
@@ -101,10 +118,7 @@ export function DouyinMashidaPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save config. Empty / masked secret keeps the stored value (backend semantics).
|
||||
* 保存配置:secret 留空或保持掩码即「不修改」,不会被空串覆盖。
|
||||
*/
|
||||
/** 保存配置 */
|
||||
async function handleSave() {
|
||||
setSaving(true)
|
||||
try {
|
||||
@@ -125,6 +139,59 @@ export function DouyinMashidaPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
/** 一键填入当前访问域名 */
|
||||
function handleFillCurrentOrigin() {
|
||||
const origin = window.location.origin
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
claimLinkBase: `${origin}/claim`,
|
||||
certLink: `${origin}/redeem`,
|
||||
afterVerifyLink: `${origin}/redeem`,
|
||||
}))
|
||||
showSuccess('已自动填入当前域名,请点击右上角「保存配置」以生效')
|
||||
}
|
||||
|
||||
/** 向平台订阅或停用推送 */
|
||||
async function handleSubscribe(disable = false) {
|
||||
setSubscribing(true)
|
||||
try {
|
||||
const targetUrl = buildDouyinMashidaMessageUrl(window.location.origin)
|
||||
await subscribeAdminDouyinMashidaPush({
|
||||
publicUrl: window.location.origin,
|
||||
messageUrl: targetUrl,
|
||||
topics: config.handledTopics,
|
||||
disable,
|
||||
})
|
||||
if (disable) {
|
||||
showSuccess('已向开放平台发送停用推送指令')
|
||||
} else {
|
||||
showSuccess(`推送订阅成功!平台将向 ${targetUrl} 推送订单事件`)
|
||||
}
|
||||
} catch (error) {
|
||||
showError(error instanceof Error ? error.message : (disable ? '停用推送失败' : '推送订阅失败'))
|
||||
} finally {
|
||||
setSubscribing(false)
|
||||
}
|
||||
}
|
||||
|
||||
/** 测试网关连通性与 MD5 签名 */
|
||||
async function handleTestGateway() {
|
||||
setTesting(true)
|
||||
setTestResult(null)
|
||||
try {
|
||||
const response = await testAdminDouyinMashidaGateway()
|
||||
const msg = response?.data?.message || '网关连通且签名验证通过'
|
||||
setTestResult({ ok: true, message: msg })
|
||||
showSuccess('网关连通性测试通过')
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : '网关连通性测试失败'
|
||||
setTestResult({ ok: false, message: msg })
|
||||
showError(msg)
|
||||
} finally {
|
||||
setTesting(false)
|
||||
}
|
||||
}
|
||||
|
||||
function updateField<K extends keyof AdminDouyinMashidaConfig>(
|
||||
key: K,
|
||||
value: AdminDouyinMashidaConfig[K],
|
||||
@@ -157,15 +224,20 @@ export function DouyinMashidaPanel() {
|
||||
return (
|
||||
<Card
|
||||
title={
|
||||
<Space>
|
||||
<ApiOutlined style={{ color: '#1677ff' }} />
|
||||
<Text strong style={{ fontSize: 15 }}>
|
||||
抖音码速达接入配置
|
||||
</Text>
|
||||
<Space size={10}>
|
||||
<ThunderboltOutlined style={{ color: '#1677ff', fontSize: 18 }} />
|
||||
<div>
|
||||
<Text strong style={{ fontSize: 16 }}>
|
||||
抖音码速达配置
|
||||
</Text>
|
||||
<Text type="secondary" style={{ fontSize: 12, marginLeft: 8 }}>
|
||||
抖店开放平台 · 自动发货直充闭环
|
||||
</Text>
|
||||
</div>
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Space size={8}>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void loadConfig()}>
|
||||
刷新
|
||||
</Button>
|
||||
@@ -175,28 +247,91 @@ export function DouyinMashidaPanel() {
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Space direction="vertical" size={12} style={{ width: '100%', marginBottom: 16 }}>
|
||||
<Space wrap align="center">
|
||||
<Text strong>接入状态:</Text>
|
||||
<Tag color={config.ready ? 'success' : 'warning'}>
|
||||
{config.ready ? '配置完整' : '待补齐'}
|
||||
</Tag>
|
||||
<Badge
|
||||
status={config.enabled ? 'processing' : 'default'}
|
||||
text={
|
||||
<Text type={config.enabled ? 'success' : 'secondary'}>
|
||||
{config.enabled ? '接入开关已开启' : '接入开关已关闭'}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
{/* 顶部状态与快速通知栏 */}
|
||||
<Card
|
||||
size="small"
|
||||
style={{ background: '#fafafa', borderColor: '#f0f0f0' }}
|
||||
bodyStyle={{ padding: '12px 16px' }}
|
||||
>
|
||||
<Row gutter={[16, 12]} align="middle" justify="space-between">
|
||||
<Col xs={24} md={10}>
|
||||
<Space wrap align="center">
|
||||
<Text strong>接入就绪状态:</Text>
|
||||
<Tag color={config.ready ? 'success' : 'warning'}>
|
||||
{config.ready ? '✅ 配置就绪' : '⚠️ 待补齐凭据'}
|
||||
</Tag>
|
||||
<Badge
|
||||
status={config.enabled ? 'processing' : 'default'}
|
||||
text={
|
||||
<Text type={config.enabled ? 'success' : 'secondary'}>
|
||||
{config.enabled ? '接入已开启' : '接入已停用'}
|
||||
</Text>
|
||||
}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={14} style={{ textAlign: 'right' }}>
|
||||
<Space wrap size={8}>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<GlobalOutlined />}
|
||||
onClick={handleFillCurrentOrigin}
|
||||
>
|
||||
一键填入当前域名
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<CheckCircleOutlined />}
|
||||
loading={testing}
|
||||
onClick={handleTestGateway}
|
||||
>
|
||||
测试网关与签名
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
ghost
|
||||
icon={<SendOutlined />}
|
||||
loading={subscribing}
|
||||
onClick={() => void handleSubscribe(false)}
|
||||
>
|
||||
向平台订阅推送
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
danger
|
||||
icon={<StopOutlined />}
|
||||
loading={subscribing}
|
||||
onClick={() => void handleSubscribe(true)}
|
||||
>
|
||||
停用平台推送
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{/* 平台推送回调地址展示 */}
|
||||
<div style={{ marginTop: 12, paddingTop: 10, borderTop: '1px dashed #e8e8e8' }}>
|
||||
<Space wrap align="center">
|
||||
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||
平台侧消息推送地址 (message_url):
|
||||
</Text>
|
||||
<Text code copyable={{ text: messageUrl }} style={{ fontSize: 13 }}>
|
||||
{messageUrl}
|
||||
</Text>
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 告警提醒区 */}
|
||||
{!config.ready && missingFields.length > 0 ? (
|
||||
<Alert
|
||||
type="warning"
|
||||
showIcon
|
||||
message={`配置尚未完整,还缺:${missingFields.join('、')}`}
|
||||
description="接入开关、商户 uid、用户秘钥、兑换链接四项齐全后,接入状态才会变为「配置完整」。"
|
||||
message={`配置尚未完整,还缺少:${missingFields.join('、')}`}
|
||||
description="请补齐商户 UID、用户秘钥与兑换链接,保存后即可完成接入就绪。"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -204,210 +339,261 @@ export function DouyinMashidaPanel() {
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="当前指向本地地址,不会请求真实码速达"
|
||||
description="baseUrl 命中 127.0.0.1 / localhost / host.docker.internal:出网请求打不到真实平台,平台也推不进来。"
|
||||
message="当前网关为本地测试桩地址"
|
||||
description="baseUrl 命中本地/内网 host,如需对接真实抖店开放平台,请使用 https://dy-openplatform-api.wanmahui.com"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<Alert
|
||||
type="info"
|
||||
showIcon
|
||||
message="平台侧需要配置的消息推送地址"
|
||||
description={
|
||||
<Text code copyable={{ text: messageUrl }}>
|
||||
{messageUrl}
|
||||
</Text>
|
||||
{testResult ? (
|
||||
<Alert
|
||||
type={testResult.ok ? 'success' : 'error'}
|
||||
showIcon
|
||||
closable
|
||||
message={testResult.ok ? '网关连通性测试通过' : '网关连通性测试失败'}
|
||||
description={testResult.message}
|
||||
onClose={() => setTestResult(null)}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{/* 板块一:基础凭据 */}
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<Space>
|
||||
<SafetyCertificateOutlined style={{ color: '#1677ff' }} />
|
||||
<Text strong>基础接入与商户凭据</Text>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Row gutter={[20, 16]}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>接入开关</Text>
|
||||
<Space align="center">
|
||||
<Switch
|
||||
checked={config.enabled}
|
||||
onChange={(checked) => updateField('enabled', checked)}
|
||||
/>
|
||||
<Tag color={config.enabled ? 'success' : 'default'}>
|
||||
{config.enabled ? '已启用' : '已停用'}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
控制是否接收并处理码速达进单
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>抖店商户号 (uid)</Text>
|
||||
<Input
|
||||
value={config.uid}
|
||||
onChange={(event) => updateField('uid', event.target.value)}
|
||||
placeholder="抖店商户号/店铺ID,例如 305760014"
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
平台分配的商户店铺 ID
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={12}>
|
||||
<SecretFieldControl
|
||||
label="用户秘钥 (secret)"
|
||||
field="douyinMashidaSecret"
|
||||
masked={config.secretMasked}
|
||||
hasSecret={config.hasSecret}
|
||||
value={config.secret}
|
||||
onChange={(value) => updateField('secret', value)}
|
||||
placeholder="输入码速达平台分配的用户秘钥(留空保持原值)"
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col xs={24}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>官方网关地址 (baseUrl)</Text>
|
||||
<Input
|
||||
value={config.baseUrl}
|
||||
onChange={(event) => updateField('baseUrl', event.target.value)}
|
||||
placeholder={DEFAULT_BASE_URL}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
码速达 OpenAPI 官方生产地址:{DEFAULT_BASE_URL}
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 板块二:买家 H5 自动直发与私信配置 */}
|
||||
<Card
|
||||
size="small"
|
||||
title={
|
||||
<Space>
|
||||
<LinkOutlined style={{ color: '#52c41a' }} />
|
||||
<Text strong>买家 H5 直充领取与私信发货</Text>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Row gutter={[20, 16]}>
|
||||
<Col xs={24} md={12}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>买家领取页基址 (claimLinkBase)</Text>
|
||||
<Input
|
||||
value={config.claimLinkBase}
|
||||
onChange={(event) => updateField('claimLinkBase', event.target.value)}
|
||||
placeholder="https://你的公网域名/claim"
|
||||
/>
|
||||
{claimLinkPreview ? (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
直充链接示例:<Text code>{claimLinkPreview}</Text>
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
买家点进此链接输入游戏 UID 即可完成直充领取与自动核销
|
||||
</Text>
|
||||
)}
|
||||
{claimLinkWarning ? (
|
||||
<Text type="danger" style={{ fontSize: 12 }}>
|
||||
{claimLinkWarning}
|
||||
</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>通用兑换链接 (certLink)</Text>
|
||||
<Input
|
||||
value={config.certLink}
|
||||
onChange={(event) => updateField('certLink', event.target.value)}
|
||||
placeholder="https://你的公网域名/redeem"
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
通用兜底兑换链接(无商品映射或旧单卡券兜底)
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={6}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>付款后自动下发私信</Text>
|
||||
<Space align="center">
|
||||
<Switch
|
||||
checked={config.claimMessageEnabled}
|
||||
onChange={(checked) => updateField('claimMessageEnabled', checked)}
|
||||
/>
|
||||
<Tag color={config.claimMessageEnabled ? 'processing' : 'default'}>
|
||||
{config.claimMessageEnabled ? '自动发私信' : '不发私信'}
|
||||
</Tag>
|
||||
</Space>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
买家付款后自动将直充链接通过抖店私信发给买家
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={18}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>发货私信文案模板 (messageTemplate)</Text>
|
||||
<Input
|
||||
value={config.messageTemplate}
|
||||
onChange={(event) => updateField('messageTemplate', event.target.value)}
|
||||
placeholder="您的订单已发货,请点击链接领取:{link}"
|
||||
/>
|
||||
<Space wrap size={6} style={{ marginTop: 2 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
支持占位符:
|
||||
</Text>
|
||||
<Tag color="blue">{'{link}'} 领取链接</Tag>
|
||||
<Tag color="cyan">{'{sku}'} 内部SKU</Tag>
|
||||
<Tag color="purple">{'{orderNo}'} 订单号</Tag>
|
||||
<Tag color="geekblue">{'{productTitle}'} 商品标题</Tag>
|
||||
</Space>
|
||||
{messageTemplateWarning ? (
|
||||
<Text type="danger" style={{ fontSize: 12 }}>
|
||||
{messageTemplateWarning}
|
||||
</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 板块三:高级网关与事件过滤(折叠面板) */}
|
||||
<Collapse
|
||||
ghost
|
||||
items={[
|
||||
{
|
||||
key: 'advanced',
|
||||
label: (
|
||||
<Space>
|
||||
<SettingOutlined style={{ color: '#8c8c8c' }} />
|
||||
<Text type="secondary" strong>
|
||||
高级网关与事件过滤设置(可选)
|
||||
</Text>
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<Row gutter={[20, 16]}>
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>私信消息类型 (messageType)</Text>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={config.messageType}
|
||||
onChange={(value) => updateField('messageType', Number(value))}
|
||||
options={MESSAGE_TYPE_OPTIONS}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
默认 2(订单发货私信)
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>网关出网超时 (timeoutMs)</Text>
|
||||
<InputNumber
|
||||
min={1000}
|
||||
step={1000}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="毫秒"
|
||||
value={config.timeoutMs}
|
||||
onChange={(value) => updateField('timeoutMs', Number(value || 20000))}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
建议保持 20000 毫秒(20秒)
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>处理 Topic 白名单 (handledTopics)</Text>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
value={config.handledTopics}
|
||||
onChange={(value: string[]) => updateField('handledTopics', value)}
|
||||
options={DEFAULT_HANDLED_TOPICS.map((topic) => ({
|
||||
value: topic,
|
||||
label: topic,
|
||||
}))}
|
||||
placeholder="选择或输入要处理的 topic"
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
白名单外事件仅留痕,不触发业务动作
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col xs={24} sm={12} md={6}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>接入开关</Text>
|
||||
<Space align="center">
|
||||
<Switch
|
||||
checked={config.enabled}
|
||||
onChange={(checked) => updateField('enabled', checked)}
|
||||
/>
|
||||
<Tag color={config.enabled ? 'success' : 'default'}>
|
||||
{config.enabled ? '已启用' : '已停用'}
|
||||
</Tag>
|
||||
</Space>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={18}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>网关地址 (baseUrl)</Text>
|
||||
<Input
|
||||
value={config.baseUrl}
|
||||
onChange={(event) => updateField('baseUrl', event.target.value)}
|
||||
placeholder={DEFAULT_BASE_URL}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
真实联调请填 https://dy-openplatform-api.wanmahui.com(本地地址收不到平台推送)
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>抖店商户号 (uid)</Text>
|
||||
<Input
|
||||
value={config.uid}
|
||||
onChange={(event) => updateField('uid', event.target.value)}
|
||||
placeholder="平台方分配的商户 uid"
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<SecretFieldControl
|
||||
label="用户秘钥 (secret)"
|
||||
field="douyinMashidaSecret"
|
||||
masked={config.secretMasked}
|
||||
hasSecret={config.hasSecret}
|
||||
value={config.secret}
|
||||
onChange={(value) => updateField('secret', value)}
|
||||
placeholder="输入新的用户秘钥(留空保持原值)"
|
||||
/>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>兑换链接 (certLink)</Text>
|
||||
<Input
|
||||
value={config.certLink}
|
||||
onChange={(event) => updateField('certLink', event.target.value)}
|
||||
placeholder="https://你的域名/redeem"
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
通用兜底兑换链接(皮肤订单解析不出领取链接时才用它)
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>领取链接基址 (claimLinkBase)</Text>
|
||||
<Input
|
||||
value={config.claimLinkBase}
|
||||
onChange={(event) => updateField('claimLinkBase', event.target.value)}
|
||||
placeholder="https://你的域名/claim"
|
||||
/>
|
||||
{claimLinkPreview ? (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
皮肤订单最终链接:{claimLinkPreview}
|
||||
</Text>
|
||||
) : (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
皮肤自动发货订单的领取页基址,留空则回退到上面的兑换链接
|
||||
</Text>
|
||||
)}
|
||||
{claimLinkWarning ? (
|
||||
<Text type="danger" style={{ fontSize: 12 }}>
|
||||
{claimLinkWarning}
|
||||
</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>发消息模板 (messageTemplate)</Text>
|
||||
<Input
|
||||
value={config.messageTemplate}
|
||||
onChange={(event) => updateField('messageTemplate', event.target.value)}
|
||||
placeholder="您的订单已发货,请点击链接领取:{link}"
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
占位符:{'{link}'} 领取链接、{'{sku}'} 内部SKU、{'{orderNo}'} 订单号、
|
||||
{'{productTitle}'} 商品标题;留空用默认文案
|
||||
</Text>
|
||||
{messageTemplateWarning ? (
|
||||
<Text type="danger" style={{ fontSize: 12 }}>
|
||||
{messageTemplateWarning}
|
||||
</Text>
|
||||
) : null}
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>付款后发领取消息 (claimMessageEnabled)</Text>
|
||||
<Switch
|
||||
checked={config.claimMessageEnabled}
|
||||
onChange={(checked) => updateField('claimMessageEnabled', checked)}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
关闭后只把领取链接写进卡券,不发订单消息
|
||||
</Text>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>去使用链接 (afterVerifyLink)</Text>
|
||||
<Input
|
||||
value={config.afterVerifyLink}
|
||||
onChange={(event) => updateField('afterVerifyLink', event.target.value)}
|
||||
placeholder="兑换成功后发给买家,留空则只发文案"
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={16}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>通知正文 (afterVerifyMessage)</Text>
|
||||
<Input
|
||||
value={config.afterVerifyMessage}
|
||||
onChange={(event) => updateField('afterVerifyMessage', event.target.value)}
|
||||
placeholder="您的订单已兑换成功"
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>消息类型 (messageType)</Text>
|
||||
<Select
|
||||
style={{ width: '100%' }}
|
||||
value={config.messageType}
|
||||
onChange={(value) => updateField('messageType', Number(value))}
|
||||
options={MESSAGE_TYPE_OPTIONS}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>出网超时 (timeoutMs)</Text>
|
||||
<InputNumber
|
||||
min={1000}
|
||||
step={1000}
|
||||
style={{ width: '100%' }}
|
||||
addonAfter="毫秒"
|
||||
value={config.timeoutMs}
|
||||
onChange={(value) => updateField('timeoutMs', Number(value || 10000))}
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} sm={12} md={8}>
|
||||
<Space direction="vertical" size={4} style={{ width: '100%' }}>
|
||||
<Text strong>处理 topic 白名单 (handledTopics)</Text>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
value={config.handledTopics}
|
||||
onChange={(value: string[]) => updateField('handledTopics', value)}
|
||||
options={DEFAULT_HANDLED_TOPICS.map((topic) => ({ value: topic, label: topic }))}
|
||||
placeholder="选择或输入要处理的 topic"
|
||||
/>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -13,3 +13,26 @@ export function fetchAdminDouyinMashidaConfig() {
|
||||
export function saveAdminDouyinMashidaConfig(payload: AdminDouyinMashidaConfig) {
|
||||
return apiPost<AdminDouyinMashidaConfig>('/api/v1/admin/platform-config/douyin-mashida', payload)
|
||||
}
|
||||
|
||||
export function subscribeAdminDouyinMashidaPush(payload: {
|
||||
publicUrl?: string
|
||||
messageUrl?: string
|
||||
topics?: string[] | string
|
||||
disable?: boolean
|
||||
}) {
|
||||
return apiPost<{
|
||||
ok: boolean
|
||||
status: number
|
||||
messageUrl: string
|
||||
topics: string
|
||||
gatewayResponse?: unknown
|
||||
}>('/api/v1/admin/platform-config/douyin-mashida/subscribe', payload)
|
||||
}
|
||||
|
||||
export function testAdminDouyinMashidaGateway() {
|
||||
return apiPost<{
|
||||
ok: boolean
|
||||
message: string
|
||||
}>('/api/v1/admin/platform-config/douyin-mashida/test-gateway', {})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user