From ccf8de97a93a354db4db7b5f0636eb1446a7522e Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 10 Oct 2026 20:12:33 +0800 Subject: [PATCH] =?UTF-8?q?chore(douyin-mashida):=20=E8=81=94=E8=B0=83?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E9=80=82=E9=85=8D=20/h5=20=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E4=B8=8E=E7=BB=9F=E4=B8=80=E5=90=8E=E5=8F=B0=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - set-public-url 脚本将 certLink 与 claimLinkBase 统一设置为 <公网地址>/h5 - subscribe 脚本改走后台 HTTP 管理接口,避免在宿主机执行时直连数据库导致连接异常 验证方式:ngrok 隧道下端到端执行 subscribe 与 preflight 自动化校验全部通过 --- .../scripts/douyin-mashida-set-public-url.ts | 10 +- .../scripts/douyin-mashida-subscribe.ts | 109 +++++++++++++----- 2 files changed, 81 insertions(+), 38 deletions(-) diff --git a/apps/backend/scripts/douyin-mashida-set-public-url.ts b/apps/backend/scripts/douyin-mashida-set-public-url.ts index 2858e801..b0b179f0 100644 --- a/apps/backend/scripts/douyin-mashida-set-public-url.ts +++ b/apps/backend/scripts/douyin-mashida-set-public-url.ts @@ -73,18 +73,16 @@ main().catch((error) => { }) async function main(): Promise { - 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()) diff --git a/apps/backend/scripts/douyin-mashida-subscribe.ts b/apps/backend/scripts/douyin-mashida-subscribe.ts index a4ffbabc..25fe07d0 100644 --- a/apps/backend/scripts/douyin-mashida-subscribe.ts +++ b/apps/backend/scripts/douyin-mashida-subscribe.ts @@ -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 { - // 配置在内存缓存里,脚本进程要先从 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 { 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 = { 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(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 { + 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 { + const result = await api(token, 'GET', CONFIG_PATH) + return (result.data ?? {}) as JsonObject +} + +async function api( + 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。 * 签名与消息推送同一套(我们扮演平台侧签名)。