feat(douyin-mashida): 彻底重构优化配置面板,支持一键填域名/订阅推送/网关自检并剔除冗余字段

- 剔除历史旧卡券时代废弃的 afterVerifyLink(去使用链接)与 afterVerifyMessage(通知正文)
- 面板新增一键填入当前域名、向开放平台一键订阅推送、停用推送以及网关连通与签名自检功能
- 业务配置重构为「基础接入与商户凭据」、「买家 H5 直充与发货私信」、「高级网关设置」三层清晰结构
- 后端与前端服务新增 /douyin-mashida/subscribe 与 /douyin-mashida/test-gateway 统一接口

验证方式:前端类型检查与打包构建通过,全量 132 项前端单测与 674 项后端单测全部通过
This commit is contained in:
yml2213
2026-10-10 20:05:46 +08:00
parent 748c18bc7d
commit d8ce40e346
5 changed files with 611 additions and 237 deletions
@@ -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', {})
}