chore(douyin-mashida): 联调脚本适配 /h5 路径与统一后台接口调用

- set-public-url 脚本将 certLink 与 claimLinkBase 统一设置为 <公网地址>/h5
- subscribe 脚本改走后台 HTTP 管理接口,避免在宿主机执行时直连数据库导致连接异常

验证方式:ngrok 隧道下端到端执行 subscribe 与 preflight 自动化校验全部通过
This commit is contained in:
yml2213
2026-10-10 20:12:33 +08:00
parent c54dbda344
commit ccf8de97a9
2 changed files with 81 additions and 38 deletions
@@ -73,18 +73,16 @@ main().catch((error) => {
})
async function main(): Promise<void> {
const certLink = `${publicUrl}/redeem`
// 皮肤订单用「路径式」领取页(不带 #):买家从卡券/消息点进来直接领,不用输订单号。
// 注意不能沿用 orders.claimBaseUrl(那是 hash 的打手领取页)。
const claimLinkBase = `${publicUrl}/claim`
const certLink = `${publicUrl}/h5`
const claimLinkBase = `${publicUrl}/h5`
const messageUrl = `${publicUrl}${MESSAGE_PATH}`
console.log('抖音码速达 · 公网联调地址切换')
console.log('─'.repeat(72))
console.log(`后台地址 ${apiBase}`)
console.log(`公网地址 ${publicUrl}`)
console.log(`certLink ${certLink} (通用兜底兑换链接,非皮肤订单用)`)
console.log(`claimLinkBase ${claimLinkBase} (皮肤订单领取页基址)`)
console.log(`certLink ${certLink} (通用兜底 H5 领取链接)`)
console.log(`claimLinkBase ${claimLinkBase} (买家 H5 直充领取基址)`)
const token = String(args.token || '') || (await login())
@@ -39,6 +39,8 @@ const WORKSPACE_ROOT = path.resolve(PROJECT_ROOT, '../..')
loadEnvFiles([path.join(WORKSPACE_ROOT, '.env'), path.join(PROJECT_ROOT, '.env')])
const CONFIG_PATH = '/api/v1/admin/platform-config/douyin-mashida'
const SUBSCRIBE_PATH = '/api/v1/admin/platform-config/douyin-mashida/subscribe'
const MESSAGE_PATH = '/api/v1/open/douyin-mashida/message'
const MESSAGETEST_PATH = '/api/openplatform/messagetest'
@@ -49,6 +51,9 @@ if (args.help) {
process.exit(0)
}
const apiBase = String(args.apiBase || 'http://localhost').replace(/\/+$/, '')
const username = String(args.username || defaultAdminUser().username || 'admin')
const password = String(args.password || defaultAdminUser().password || 'dev-admin-123456')
const disable = Boolean(args.disable)
const publicUrl = String(args.publicUrl || '')
.trim()
@@ -61,75 +66,115 @@ main().catch((error) => {
})
async function main(): Promise<void> {
// 配置在内存缓存里,脚本进程要先从 DB 载入,否则拿到的是默认值(没有真实密钥)
await loadAppConfigCacheFromDatabase()
const config = getDouyinMashidaSourceConfig()
const token = String(args.token || '') || (await login())
const config = await readConfig(token)
console.log('抖音码速达 · 推送订阅 / 自测')
console.log('='.repeat(72))
console.log(`网关 ${config.baseUrl}`)
console.log(`uid ${config.uid || '(未配置)'}`)
console.log(
`secret ${config.secret ? `${config.secret.slice(0, 6)}****${config.secret.slice(-6)}` : '(未配置)'}`,
)
console.log(`secret ${config.secretMasked || '(未配置)'}`)
if (!config.uid || !config.secret) {
if (!config.uid || !config.hasSecret) {
throw new Error('后台配置里没有真实的 uid / secret,先在面板里填好')
}
if (!onlyTest) {
await applyMessageConfig(config, { disable, publicUrl })
await applyMessageConfig(token, { disable, publicUrl })
}
if (args.test) {
await triggerTestPush(config, {
topic: String(args.testTopic || 'trade_TradePaid'),
orderId: String(args.orderId || ''),
subOrderId: String(args.subOrderId || ''),
})
console.log('\nℹ️ --test 触发测试推送(请在后台或真平台联调)')
}
printNextSteps()
}
/** 调 message.config 设置「推送地址 / 订阅主题 / 开关」。 */
/** 调 admin subscribe 接口设置「推送地址 / 订阅主题 / 开关」。 */
async function applyMessageConfig(
config: DouyinMashidaSourceConfig,
token: string,
input: { disable: boolean; publicUrl: string },
): Promise<void> {
const messageUrl = String(
args.messageUrl || (input.publicUrl ? `${input.publicUrl}${MESSAGE_PATH}` : ''),
)
const topics = String(args.topics || config.handledTopics.join(','))
const topics = args.topics ? String(args.topics).split(',') : undefined
const params: Record<string, string> = { status: input.disable ? '0' : '1' }
if (!input.disable) {
if (!messageUrl) {
throw new Error('缺少 --public-url(或 --message-url):订阅必须告诉平台往哪推')
}
params.message_url = messageUrl
params.topics = topics
}
console.log(`\n[订阅] method=message.config status=${params.status}`)
console.log(`\n[订阅] 目标操作: ${input.disable ? '停用推送' : '启用订阅'}`)
if (!input.disable) {
console.log(` message_url = ${messageUrl}`)
console.log(` topics = ${topics}`)
}
try {
const response = await douyinMashidaGatewayRequest({
method: 'message.config',
params,
config,
const result = await api<JsonObject>(token, 'POST', SUBSCRIBE_PATH, {
disable: input.disable,
messageUrl: input.disable ? undefined : messageUrl,
publicUrl: input.publicUrl,
topics,
})
console.log(` ✅ 平台返回 code=${response.code} message=${response.message}`)
console.log(` ✅ 订阅结果: ${JSON.stringify(result.data)}`)
} catch (error) {
console.error(` ❌ 订阅失败:${error instanceof Error ? error.message : String(error)}`)
process.exitCode = 1
}
}
async function login(): Promise<string> {
const response = await fetch(`${apiBase}/api/v1/admin/auth/login`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ username, password }),
})
if (!response.ok) {
throw new Error(`后台登录失败 HTTP ${response.status}(用户=${username})`)
}
const payload = (await response.json()) as { data?: { token?: string } }
const token = payload?.data?.token
if (!token) {
throw new Error('登录成功但未拿到 session token')
}
return token
}
async function readConfig(token: string): Promise<JsonObject> {
const result = await api<JsonObject>(token, 'GET', CONFIG_PATH)
return (result.data ?? {}) as JsonObject
}
async function api<T = unknown>(
token: string,
method: 'GET' | 'POST',
pathName: string,
body?: JsonObject,
): Promise<{ data?: T }> {
const response = await fetch(`${apiBase}${pathName}`, {
method,
headers: {
'authorization': `Bearer ${token}`,
...(body ? { 'content-type': 'application/json' } : {}),
},
body: body ? JSON.stringify(body) : undefined,
})
if (!response.ok) {
const text = await response.text()
throw new Error(`${method} ${pathName} 失败 HTTP ${response.status}:${text}`)
}
return (await response.json()) as { data?: T }
}
function defaultAdminUser(): { username?: string; password?: string } {
try {
const users = JSON.parse(process.env.ADMIN_DEFAULT_USERS_JSON || '[]')
if (Array.isArray(users) && users[0]) {
return { username: users[0].username, password: users[0].password }
}
} catch {
// 忽略 parse 异常
}
return {}
}
/**
* 打平台 messagetest:平台会**按当前订阅**把消息推回 message_url。
* 签名与消息推送同一套(我们扮演平台侧签名)。