chore(douyin-mashida): 联调脚本适配 /h5 路径与统一后台接口调用
- set-public-url 脚本将 certLink 与 claimLinkBase 统一设置为 <公网地址>/h5 - subscribe 脚本改走后台 HTTP 管理接口,避免在宿主机执行时直连数据库导致连接异常 验证方式:ngrok 隧道下端到端执行 subscribe 与 preflight 自动化校验全部通过
This commit is contained in:
@@ -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。
|
||||
* 签名与消息推送同一套(我们扮演平台侧签名)。
|
||||
|
||||
Reference in New Issue
Block a user