fix(douyin-mashida): 领取消息只发给有自动发货领取单元的单,人工单不再假发货
根因:notifyBuyerClaimLink 对 `/h5` 形态的 claimLinkBase 走了一条捷径——只拼订单号
(`${base}/${orderNo}`)就发消息,跳过了「该订单项是否有自动发货领取单元」的校验。
领取 token 只由 affiliate_dash 执行器在建单时创建,未命中 AFFILIATE_DASH_SKU_MAPPING
回落 manual_dispatch 的人工单、以及自动准备失败的订单都没有 token,却照样收到
「您的订单已发货,请点击链接领取」(线上实测:非映射商品「套装-牛仔情诗」)。
- claim-link.ts:新增 buildDouyinMashidaBuyerClaimUrl(无自动领取单元一律返回空串 = 不发消息)
与 isDouyinMashidaH5ClaimBase(/h5 形态判定收敛到一处)
- notify-service.ts:两种基址形态统一先 resolveOrderItemClaimLink 拿 taskId 作为「会自动发货」
的证据,缺失则跳过并留痕「该订单项没有自动发货领取单元,跳过领取链接消息」
- claim-link.test.ts:补齐人工单不发消息、/h5 与 token 形态拼接、基址/订单号缺失等用例
验证:npm run check 全绿(prettier + SQL guard + eslint + tsc + 703 单测 + build)
This commit is contained in:
@@ -2,7 +2,9 @@ import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import {
|
||||
buildDouyinMashidaBuyerClaimUrl,
|
||||
buildDouyinMashidaClaimMessage,
|
||||
isDouyinMashidaH5ClaimBase,
|
||||
renderDouyinMashidaMessageTemplate,
|
||||
resolveDouyinMashidaClaimUrl,
|
||||
} from './claim-link.js'
|
||||
@@ -102,3 +104,86 @@ test('发消息正文:无领取链接则不发(返回空串),默认模
|
||||
'幸运币2个 已到账,订单 6930:https://x/claim/1',
|
||||
)
|
||||
})
|
||||
|
||||
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(
|
||||
buildDouyinMashidaBuyerClaimUrl({
|
||||
claimLinkBase: 'https://ks.khhao.com/h5',
|
||||
orderNo: '6918490107300556291',
|
||||
hasAutoClaimUnit: false,
|
||||
}),
|
||||
'',
|
||||
)
|
||||
|
||||
// token 形态基址同样不发:人工单本来就没有 token,不能拿订单号去凑
|
||||
assert.equal(
|
||||
buildDouyinMashidaBuyerClaimUrl({
|
||||
claimLinkBase: 'https://ks.khhao.com/claim',
|
||||
orderNo: '6918490107300556291',
|
||||
hasAutoClaimUnit: false,
|
||||
tokenClaimUrl: '',
|
||||
}),
|
||||
'',
|
||||
)
|
||||
})
|
||||
|
||||
test('买家领取消息链接:有自动发货领取单元时按基址形态拼接', () => {
|
||||
// /h5 形态:拼订单号(结尾斜杠、结尾多斜杠都容忍)
|
||||
assert.equal(
|
||||
buildDouyinMashidaBuyerClaimUrl({
|
||||
claimLinkBase: 'https://ks.khhao.com/h5',
|
||||
orderNo: '6918490107300556291',
|
||||
hasAutoClaimUnit: true,
|
||||
}),
|
||||
'https://ks.khhao.com/h5/6918490107300556291',
|
||||
)
|
||||
assert.equal(
|
||||
buildDouyinMashidaBuyerClaimUrl({
|
||||
claimLinkBase: ' https://ks.khhao.com/h5/// ',
|
||||
orderNo: '6918490107300556291',
|
||||
hasAutoClaimUnit: true,
|
||||
}),
|
||||
'https://ks.khhao.com/h5/6918490107300556291',
|
||||
)
|
||||
|
||||
// token 形态:只用 resolveOrderItemClaimLink 解析出的链接
|
||||
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',
|
||||
)
|
||||
})
|
||||
|
||||
test('买家领取消息链接:基址或订单号缺失时返回空串(不发半条消息)', () => {
|
||||
assert.equal(
|
||||
buildDouyinMashidaBuyerClaimUrl({
|
||||
claimLinkBase: '',
|
||||
orderNo: '6918490107300556291',
|
||||
hasAutoClaimUnit: true,
|
||||
}),
|
||||
'',
|
||||
)
|
||||
assert.equal(
|
||||
buildDouyinMashidaBuyerClaimUrl({
|
||||
claimLinkBase: 'https://ks.khhao.com/h5',
|
||||
orderNo: '',
|
||||
hasAutoClaimUnit: true,
|
||||
}),
|
||||
'',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -28,6 +28,66 @@ export function resolveDouyinMashidaClaimUrl(
|
||||
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/'))
|
||||
}
|
||||
|
||||
/**
|
||||
* 买家消息里真正要发出去的领取链接(**空串 = 这条消息不许发**)。
|
||||
*
|
||||
* 硬约束:只有该订单项**确实有自动发货领取单元**(`affiliate_dash` 执行器在建单时
|
||||
* 创建的 claim token,见 `fulfillment/executors/affiliate-dash-executor.ts`)才发链接。
|
||||
* 领取 token 是「这笔单会自动发货」的唯一证据:
|
||||
* - 未命中 `AFFILIATE_DASH_SKU_MAPPING` → 显式回落人工 `manual_dispatch`,没有 token;
|
||||
* - 自动准备失败 → 任务转人工复核,同样没有 token。
|
||||
* 这两类订单发给买家「您的订单已发货,请点击链接领取」是**假发货**,买家点进去也无货可领。
|
||||
*
|
||||
* 历史缺陷:`/h5` 基址原先走了一条捷径——只拼订单号、跳过履约任务校验,
|
||||
* 于是人工单也照样收到领取消息(线上实测:未命中映射的「套装-牛仔情诗」单被发了链接)。
|
||||
*
|
||||
* 拼接规则按基址形态分流:
|
||||
* - `/h5` 形态 → `claimLinkBase` + `/` + 订单号;
|
||||
* - 其它形态 → `resolveOrderItemClaimLink` 解析出的 token 链接。
|
||||
*/
|
||||
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(/\/+$/, '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 模板占位符替换:支持 `{link}` / `{sku}` / `{orderNo}` / `{productTitle}`。
|
||||
* 未提供的占位符(或空值)原样保留,方便运营一眼看出漏配,而不是发出一条缺链接的消息。
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
resolveDouyinMashidaParentOrderId,
|
||||
} from './order-mapper.js'
|
||||
import { resolveOrderItemClaimLink } from '../../claim/claim-service.js'
|
||||
import { buildDouyinMashidaClaimMessage } from './claim-link.js'
|
||||
import { buildDouyinMashidaBuyerClaimUrl, buildDouyinMashidaClaimMessage } from './claim-link.js'
|
||||
import {
|
||||
getDouyinMashidaSourceConfig,
|
||||
type DouyinMashidaSourceConfig,
|
||||
@@ -235,8 +235,13 @@ async function handlePaidMessage(input: {
|
||||
* 买家点链接 → 输 UID → 出二维码 → 扫码绑定 → 系统比对「绑定 UID == 填的 UID」→ 发货完成
|
||||
* (完全复用现成的 affiliate_dash 通道)。
|
||||
*
|
||||
* 只在「该订单项确实有履约任务 claim token + claimLinkBase 已配置 + 开关打开」时才发;
|
||||
* 拿不到链接的订单(例如未命中映射、走人工的订单)静默跳过,不打扰现有兑换流程。
|
||||
* 只在「该订单项确实有自动发货领取单元(claim token)+ claimLinkBase 已配置 + 开关打开」时才发。
|
||||
* 领取 token 由 `affiliate_dash` 执行器在建单时创建,是「这笔单会自动发货」的唯一证据:
|
||||
* 未命中映射回落人工 `manual_dispatch`、或自动准备失败的订单都拿不到 token,
|
||||
* 这类订单发给买家「已发货」属于假发货,一律**静默跳过**。
|
||||
*
|
||||
* ⚠️ 这条约束对 `/h5` 形态的基址同样成立(历史缺陷:`/h5` 曾走捷径跳过校验,
|
||||
* 结果人工单也收到了领取消息)。
|
||||
*/
|
||||
async function notifyBuyerClaimLink(input: {
|
||||
order: OrderRow
|
||||
@@ -254,22 +259,30 @@ async function notifyBuyerClaimLink(input: {
|
||||
}
|
||||
|
||||
const orderNo = resolveReceiptOrderId(input.order, input.orderItem)
|
||||
let claimUrl = ''
|
||||
let taskId: number | undefined
|
||||
|
||||
if (claimLinkBase.endsWith('/h5') || claimLinkBase.includes('/h5/')) {
|
||||
claimUrl = `${claimLinkBase}/${orderNo}`
|
||||
} else {
|
||||
const claimLink = await resolveOrderItemClaimLink({
|
||||
orderId: input.order.id,
|
||||
orderItemId: input.orderItem.id,
|
||||
claimLinkBase: input.config.claimLinkBase,
|
||||
})
|
||||
claimUrl = claimLink.claimUrl
|
||||
taskId = claimLink.taskId
|
||||
}
|
||||
// 先核对履约任务:`resolveOrderItemClaimLink` 只有在该订单项存在带 token 的领取单元时才给 taskId。
|
||||
const claimLink = await resolveOrderItemClaimLink({
|
||||
orderId: input.order.id,
|
||||
orderItemId: input.orderItem.id,
|
||||
claimLinkBase,
|
||||
})
|
||||
|
||||
const claimUrl = buildDouyinMashidaBuyerClaimUrl({
|
||||
claimLinkBase,
|
||||
orderNo,
|
||||
hasAutoClaimUnit: Boolean(claimLink.taskId),
|
||||
tokenClaimUrl: claimLink.claimUrl,
|
||||
})
|
||||
|
||||
if (!claimUrl) {
|
||||
logIntegration('[douyin-mashida/notify]', '该订单项没有自动发货领取单元,跳过领取链接消息', {
|
||||
orderId: input.order.id,
|
||||
orderItemId: input.orderItem.id,
|
||||
orderNo,
|
||||
claimLinkBase,
|
||||
taskCount: claimLink.taskCount,
|
||||
hasClaimUnit: Boolean(claimLink.taskId),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -290,7 +303,8 @@ async function notifyBuyerClaimLink(input: {
|
||||
orderId: input.order.id,
|
||||
orderItemId: input.orderItem.id,
|
||||
orderNo,
|
||||
taskId,
|
||||
taskId: claimLink.taskId,
|
||||
claimUrl,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user