fix(claim): 统一所有买家领取链接为 H5 订单号格式

This commit is contained in:
yml2213
2026-11-02 22:11:22 +08:00
parent 3989b0a649
commit 4dc2f5ca90
38 changed files with 298 additions and 362 deletions
@@ -1,12 +1,11 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { buildOrderClaimUrl } from '../../claim/claim-service.js'
import {
buildDouyinMashidaBuyerClaimUrl,
buildDouyinMashidaClaimMessage,
isDouyinMashidaH5ClaimBase,
renderDouyinMashidaMessageTemplate,
resolveDouyinMashidaClaimUrl,
} from './claim-link.js'
import {
createDefaultDouyinMashidaSourceConfig,
@@ -45,22 +44,6 @@ test('抖音配置归一化:claimLinkBase 去尾斜杠,开关可关,模板
assert.equal(config.claimMessageEnabled, false)
})
test('买家领取链接 = claimLinkBase(去尾斜杠)+ / + token,绝不回退全局领取页', () => {
assert.equal(
resolveDouyinMashidaClaimUrl(
'AbC123',
buildConfig({ claimLinkBase: 'https://x.ngrok-free.app/claim/' }),
),
'https://x.ngrok-free.app/claim/AbC123',
)
assert.equal(resolveDouyinMashidaClaimUrl('AbC123', buildConfig()), '')
assert.equal(
resolveDouyinMashidaClaimUrl('', buildConfig({ claimLinkBase: 'https://x/claim' })),
'',
)
})
test('模板占位符替换:支持 link/sku/orderNo/productTitle,空值保留占位符', () => {
assert.equal(
renderDouyinMashidaMessageTemplate(
@@ -105,16 +88,6 @@ test('发消息正文:无领取链接则不发(返回空串),默认模
)
})
test('领取链接基址形态:/h5 与 /h5/xxx 判为 H5 订单号形态,其它不是', () => {
assert.equal(isDouyinMashidaH5ClaimBase('https://ks.khhao.com/h5'), true)
assert.equal(isDouyinMashidaH5ClaimBase('https://ks.khhao.com/h5/'), true)
assert.equal(isDouyinMashidaH5ClaimBase('https://ks.khhao.com/h5/anything'), true)
assert.equal(isDouyinMashidaH5ClaimBase(' https://ks.khhao.com/claim '), false)
assert.equal(isDouyinMashidaH5ClaimBase('https://ks.khhao.com/h5x'), false)
assert.equal(isDouyinMashidaH5ClaimBase(''), false)
assert.equal(isDouyinMashidaH5ClaimBase(undefined), false)
})
test('买家领取消息链接:没有自动发货领取单元(人工单)一律返回空串 = 不发消息', () => {
// 线上实测的缺陷场景:未命中映射的「套装-牛仔情诗」单回落人工,却收到了「已发货」领取链接
assert.equal(
@@ -126,19 +99,18 @@ test('买家领取消息链接:没有自动发货领取单元(人工单)
'',
)
// token 形态基址同样不发:人工单本来就没有 token,不能拿订单号去凑
// 历史 /claim 基址同样不发:人工单本来就没有自动发货单元
assert.equal(
buildDouyinMashidaBuyerClaimUrl({
claimLinkBase: 'https://ks.khhao.com/claim',
orderNo: '6918490107300556291',
hasAutoClaimUnit: false,
tokenClaimUrl: '',
}),
'',
)
})
test('买家领取消息链接:有自动发货领取单元时按基址形态拼接', () => {
test('买家领取消息链接:有自动发货领取单元时统一生成 /h5/订单号', () => {
// /h5 形态:拼订单号(结尾斜杠、结尾多斜杠都容忍)
assert.equal(
buildDouyinMashidaBuyerClaimUrl({
@@ -157,15 +129,14 @@ test('买家领取消息链接:有自动发货领取单元时按基址形态
'https://ks.khhao.com/h5/6918490107300556291',
)
// token 形态:只用 resolveOrderItemClaimLink 解析出的链接
// 历史 /claim 基址也归一化到 /h5,不再回退 token 链接
assert.equal(
buildDouyinMashidaBuyerClaimUrl({
claimLinkBase: 'https://ks.khhao.com/claim',
orderNo: '6918490107300556291',
hasAutoClaimUnit: true,
tokenClaimUrl: 'https://ks.khhao.com/claim/AbC123',
}),
'https://ks.khhao.com/claim/AbC123',
'https://ks.khhao.com/h5/6918490107300556291',
)
})
@@ -176,7 +147,7 @@ test('买家领取消息链接:基址或订单号缺失时返回空串(不
orderNo: '6918490107300556291',
hasAutoClaimUnit: true,
}),
'',
buildOrderClaimUrl('6918490107300556291'),
)
assert.equal(
buildDouyinMashidaBuyerClaimUrl({
@@ -1,4 +1,4 @@
import { buildConfiguredClaimUrl } from '../../claim/claim-service.js'
import { buildOrderClaimUrl } from '../../claim/claim-service.js'
import {
DEFAULT_CLAIM_MESSAGE_TEMPLATE,
getDouyinMashidaSourceConfig,
@@ -8,9 +8,7 @@ import {
/**
* 抖音码速达的买家领取链接与发货消息拼装(纯函数,便于单测)。
*
* 领取链接 = `config.claimLinkBase`(去尾斜杠) + `/` + token。
* ⚠️ 这里**只认** `claimLinkBase`,绝不回退 `runtimeConfig.orders.claimBaseUrl`
* (那条口径的默认基址属于主站默认领取页,两者不能互相回退)。
* 买家链接统一为 `/h5/<平台订单号>`;配置了更具体的领取站基址时沿用其站点。
*/
export type DouyinMashidaClaimMessageVars = {
@@ -20,25 +18,6 @@ export type DouyinMashidaClaimMessageVars = {
productTitle?: string | undefined
}
/** 买家领取链接;`claimLinkBase` 或 token 缺失时返回空串(调用方据此跳过发链接/发消息)。 */
export function resolveDouyinMashidaClaimUrl(
token: unknown,
config: DouyinMashidaSourceConfig = getDouyinMashidaSourceConfig(),
): string {
return buildConfiguredClaimUrl(config.claimLinkBase, token)
}
/**
* 领取链接基址是否是**路径式 H5 订单号**形态(`https://<域名>/h5`、`.../h5/xxx`)。
*
* 这种基址拼的是**订单号**(`/h5/<订单号>`),不是 claim token;两种形态的拼接规则不同,
* 判断只此一处,避免各调用方各写一份。
*/
export function isDouyinMashidaH5ClaimBase(claimLinkBase: unknown): boolean {
const base = normalizeClaimLinkBase(claimLinkBase)
return Boolean(base) && (base.endsWith('/h5') || base.includes('/h5/'))
}
/**
* 买家消息里真正要发出去的领取链接(**空串 = 这条消息不许发**)。
*
@@ -52,40 +31,21 @@ export function isDouyinMashidaH5ClaimBase(claimLinkBase: unknown): boolean {
* 历史缺陷:`/h5` 基址原先走了一条捷径——只拼订单号、跳过履约任务校验,
* 于是人工单也照样收到领取消息(线上实测:未命中映射的「套装-牛仔情诗」单被发了链接)。
*
* 拼接规则按基址形态分流:
* - `/h5` 形态 → `claimLinkBase` + `/` + 订单号;
* - 其它形态 → `resolveOrderItemClaimLink` 解析出的 token 链接。
* 拼接规则:有自动发货领取单元时,所有来源统一发 `/h5/<平台订单号>`。
* 历史配置的 `/#/claim`、`/claim` 等基址也会由公共 helper 归一化到 `/h5`。
*/
export function buildDouyinMashidaBuyerClaimUrl(input: {
claimLinkBase: unknown
/** 平台订单号(`/h5` 形态用) */
/** 平台订单号 */
orderNo: unknown
/** 该订单项是否有自动发货领取单元(`resolveOrderItemClaimLink().taskId` 非空即为有) */
hasAutoClaimUnit: boolean
/** token 形态链接(`resolveOrderItemClaimLink().claimUrl`) */
tokenClaimUrl?: unknown
}): string {
if (!input.hasAutoClaimUnit) {
return ''
}
const base = normalizeClaimLinkBase(input.claimLinkBase)
if (!base) {
return ''
}
if (isDouyinMashidaH5ClaimBase(base)) {
return buildConfiguredClaimUrl(base, String(input.orderNo ?? '').trim())
}
return String(input.tokenClaimUrl ?? '').trim()
}
/** 基址归一化:去首尾空白与结尾斜杠。 */
function normalizeClaimLinkBase(claimLinkBase: unknown): string {
return String(claimLinkBase ?? '')
.trim()
.replace(/\/+$/, '')
return buildOrderClaimUrl(input.orderNo, input.claimLinkBase || undefined)
}
/**
@@ -254,31 +254,28 @@ async function notifyBuyerClaimLink(input: {
const claimLinkBase = String(input.config.claimLinkBase || '')
.trim()
.replace(/\/+$/, '')
if (!claimLinkBase) {
return
}
const orderNo = resolveReceiptOrderId(input.order, input.orderItem)
const receiptOrderNo = resolveReceiptOrderId(input.order, input.orderItem)
const claimOrderNo = String(input.order.platform_order_id || receiptOrderNo).trim()
// 先核对履约任务:`resolveOrderItemClaimLink` 只有在该订单项存在带 token 的领取单元时才给 taskId。
const claimLink = await resolveOrderItemClaimLink({
orderId: input.order.id,
orderNo: claimOrderNo,
orderItemId: input.orderItem.id,
claimLinkBase,
})
const claimUrl = buildDouyinMashidaBuyerClaimUrl({
claimLinkBase,
orderNo,
orderNo: claimOrderNo,
hasAutoClaimUnit: Boolean(claimLink.taskId),
tokenClaimUrl: claimLink.claimUrl,
})
if (!claimUrl) {
logIntegration('[douyin-mashida/notify]', '该订单项没有自动发货领取单元,跳过领取链接消息', {
orderId: input.order.id,
orderItemId: input.orderItem.id,
orderNo,
orderNo: claimOrderNo,
claimLinkBase,
taskCount: claimLink.taskCount,
hasClaimUnit: Boolean(claimLink.taskId),
@@ -290,19 +287,24 @@ async function notifyBuyerClaimLink(input: {
config: input.config,
claimUrl,
sku: input.orderItem.sku_code,
orderNo,
orderNo: claimOrderNo,
productTitle: input.orderItem.sku_name,
})
if (!content) {
return
}
await sendDouyinMashidaOrderMessage({ orderId: orderNo, content, config: input.config })
await sendDouyinMashidaOrderMessage({
orderId: receiptOrderNo,
content,
config: input.config,
})
logIntegration('[douyin-mashida/notify]', '已把领取链接发给买家(order.message)', {
orderId: input.order.id,
orderItemId: input.orderItem.id,
orderNo,
orderNo: claimOrderNo,
receiptOrderNo,
taskId: claimLink.taskId,
claimUrl,
})
@@ -18,13 +18,11 @@ import {
* - `uid` 是抖店商户号;`secret` 是用户秘钥(两者都由平台方分配)。
* - `certLink` 是我们发给平台的固定兑换链接(`coupons.syncV2` 的 `cert_link`),
* **必须做成配置项**:链接一旦同步给平台就收不回来,换域名时只影响新券。
* 命中 affiliate-dash 映射的订单会优先用「买家领取链接」(`claimLinkBase` + token),
* 此时 `certLink` 退化为兜底。
* - `claimLinkBase` 是**买家领取页**基址,必须渲染 affiliate-dash 领取流程
* (输 UID → 二维码 → 扫码绑定 → 比对「绑定 UID == 填的 UID」完成发货)。
* 本仓库前端该页面是 `pages/claim/ClaimPage`,路径式 `https://<域名>/claim` 与
* hash 式 `https://<域名>/#/claim` 都会渲染它;它**不是** `runtimeConfig.orders.claimBaseUrl`
* (两条口径不能互相回退)。
* 买家领取链接统一为 `/h5/<订单号>`;未配 `claimLinkBase` 时使用全局领取站,
* `certLink` 仅在订单号或领取站不可用时兜底。
* - `claimLinkBase` 是**买家领取页**基址;出站链接会在其站点下统一生成
* `/h5/<平台订单号>`,历史 `/#/claim`、`/claim` 基址也会被归一化。
* 本仓库前端该页面是 `pages/claim/ClaimPage`,走 affiliate-dash 领取流程。
* - `handledTopics` 是本地处理白名单:白名单外的 topic 只落库留痕、不产生副作用。
*/