refactor(douyin-mashida)!: 纠偏——删掉自建的皮肤通道,改回复用 affiliate_dash
上一轮我把皮肤交付做成了「另建一套执行器 + 独立领取接口 + 只提交账号就结束」, 跟买家实际要走的过程不一致(真正的完成信号是「扫码绑定后的 UID == 买家填的 UID」), 而且核销还触发了老通知逻辑,给买家**又发了一条 /redeem** —— 买家收到两条链接。 本次按用户要求改回**完全复用仓库现成的 affiliate_dash 通道**(= 旧的快手皮肤自动发货)。 **删掉** - 执行器 `douyin_mashida_skin`(含 registry/types/planner/bootstrap/路由白名单注册) - `services/claim/claim-skin-service.ts`、`routes/claim-skin.ts`、`/api/v1/claim-skin` 挂载 - 迁移 096 的 `claim_submission_json` 全链路(task-repo / rows / inputs / task-detail / admin-read-service 的 claimSubmission),并新增 **097 兜底迁移**把该列 DROP 掉 - 顺带清掉上一轮专为已删接口加的死代码(utils/http.ts 的 buildMessageSuccessPayload 等) **改回复用** - `resolveOrderItemClaimLink`:通用解析(只看订单项、不看执行器)→ `buildConfiguredClaimUrl(claimLinkBase, token)`;多履约单元取 task id 最小并告警; **绝不回退** hash 形态的 `buildClaimUrl` - `cert_link` 与付款后那条**唯一**消息共用同一个解析结果 - **核销后不再给抖音买家发第二条消息**(`shouldNotifyBuyerAfterVerify` 只放行非抖音 provider) - 买家侧走既有流程:输 UID → 二维码 → 扫码绑定 → 比对 UID → delivered/REDEEMED; 在 delivered 分支 best-effort 核销平台券(幂等、失败只 warn,不拖慢买家响应) - 后台回显链接改按 **order.provider** 判断(抖音用 `claimLinkBase`) **SQL 守卫**:允许「删除历史迁移」,但强制要求「同时新增兜底迁移」配对 (修改历史迁移仍禁止),并把改动来源换成 `git status --porcelain`(否则未跟踪的新迁移看不见) `npm run check`:tests 671 / pass 668 / fail 0 / skipped 3(含新增的物品图回退用例)
This commit is contained in:
@@ -13,7 +13,7 @@ import {
|
||||
} from '../../repositories/redeem-certificate-repo.js'
|
||||
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
|
||||
|
||||
import { resolveOrderItemSkinClaimLink } from '../fulfillment/executors/douyin-mashida-skin-executor.js'
|
||||
import { resolveOrderItemClaimLink } from '../claim/claim-service.js'
|
||||
import { createHttpError } from '../../utils/http.js'
|
||||
import { logIntegration } from '../../utils/logger.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
@@ -40,6 +40,11 @@ import {
|
||||
* 闭环:建单后**我们生成券码**(DYC + Crockford Base32)→ `coupons.syncV2` 交给平台展示给买家
|
||||
* → 买家在 `https://<域名>/redeem` 输入订单号兑换 → `coupons.verifyV3` 上报核销。
|
||||
*
|
||||
* 皮肤/礼包类订单(命中 affiliate-dash 映射)**不走**上面的输入订单号兑换:
|
||||
* 付款时按 `claimLinkBase` 把买家领取链接发给买家,买家在领取页输 UID → 扫码绑定 →
|
||||
* 「绑定 UID == 填的 UID」即发货完成(见 claim/kuaishou-cloud-claim-service.ts),
|
||||
* 完成时由该流程 best-effort 调 `verifyCertificateForOrderItem` 把平台券核销掉。
|
||||
*
|
||||
* 幂等:
|
||||
* - 发券:`(provider, order_id)` 唯一,重复回调只会复用同一张券;
|
||||
* - 同步:`sync_status='success'` 直接跳过,失败可重试;
|
||||
@@ -70,7 +75,7 @@ export async function ensureCertificateForOrderItem(input: {
|
||||
})
|
||||
}
|
||||
|
||||
// 皮肤自动发货:cert_link 用「本站领取链接」;其它情况沿用固定兑换链接 certLink(原行为不变)。
|
||||
// 该订单项已有领取 token 时 cert_link 用「买家领取链接」;其它情况沿用固定兑换链接 certLink(原行为不变)。
|
||||
const certLink = await resolveDouyinMashidaCertLink({
|
||||
order: input.order,
|
||||
orderItem: input.orderItem,
|
||||
@@ -121,12 +126,13 @@ export async function ensureCertificateForOrderItem(input: {
|
||||
* 解析本次同步要交给平台的 `cert_link`。
|
||||
*
|
||||
* 优先级(**只对新单生效**:已同步成功的券不会再改动):
|
||||
* 1. 皮肤自动发货任务已生成 claim token,且配置了 `claimLinkBase`
|
||||
* → `claimLinkBase/token`(买家领取页,路径式);
|
||||
* 1. 该订单项已有履约任务 claim token,且配置了 `claimLinkBase`
|
||||
* → `claimLinkBase/token`(买家领链接);
|
||||
* 2. 其它情况 → `config.certLink`(固定兑换链接,历史行为完全不变)。
|
||||
*
|
||||
* 同一个订单项按数量拆成多个履约单元时会有多个 token,这里取最小 task id 的那个,
|
||||
* 保证「同一张券每次解析出同一条链接」,并打 warn 留痕提示运营。
|
||||
* 链接解析走通用 helper(只看订单项的履约任务,不看执行器 key):
|
||||
* 抖音皮肤/礼包单已改走 affiliate_dash 通道,换执行器不会再让 cert_link 失效。
|
||||
* 同一个订单项按数量拆成多个履约单元时会有多个 token,helper 取最小 task id 并打 warn。
|
||||
*/
|
||||
export async function resolveDouyinMashidaCertLink(input: {
|
||||
order: OrderRow
|
||||
@@ -138,21 +144,23 @@ export async function resolveDouyinMashidaCertLink(input: {
|
||||
return fallback
|
||||
}
|
||||
|
||||
const skinLink = await resolveOrderItemSkinClaimLink({
|
||||
const claimLink = await resolveOrderItemClaimLink({
|
||||
orderId: input.order.id,
|
||||
orderItemId: input.orderItem.id,
|
||||
config: input.config,
|
||||
claimLinkBase: input.config.claimLinkBase,
|
||||
})
|
||||
|
||||
if (!skinLink.claimUrl) {
|
||||
if (skinLink.taskCount > 0) {
|
||||
if (!claimLink.claimUrl) {
|
||||
// 有履约任务却拿不到 token = 任务没被正确准备(配置/执行器问题),值得留痕;
|
||||
// 连任务都还没有属于建单顺序上的正常瞬时状态,静默回退即可。
|
||||
if (claimLink.taskCount > 0) {
|
||||
logIntegration(
|
||||
'[douyin-mashida/certificate]',
|
||||
'皮肤订单尚无可用 claim token,cert_link 回退固定兑换链接',
|
||||
'该订单项有履约任务但尚无 claim token,cert_link 回退固定兑换链接',
|
||||
{
|
||||
orderId: input.order.id,
|
||||
orderItemId: input.orderItem.id,
|
||||
skinTaskCount: skinLink.taskCount,
|
||||
taskCount: claimLink.taskCount,
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
@@ -160,21 +168,7 @@ export async function resolveDouyinMashidaCertLink(input: {
|
||||
return fallback
|
||||
}
|
||||
|
||||
if (skinLink.taskCount > 1) {
|
||||
logIntegration(
|
||||
'[douyin-mashida/certificate]',
|
||||
'同一订单项存在多个皮肤履约单元,cert_link 取首个 token',
|
||||
{
|
||||
orderId: input.order.id,
|
||||
orderItemId: input.orderItem.id,
|
||||
skinTaskCount: skinLink.taskCount,
|
||||
pickedTaskId: skinLink.pickedTaskId,
|
||||
},
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
|
||||
return skinLink.claimUrl
|
||||
return claimLink.claimUrl
|
||||
}
|
||||
|
||||
/** 调 `coupons.syncV2` 把券码 + 兑换/领取链接交给平台;失败会标记并抛出(让平台重推/人工重试)。 */
|
||||
@@ -312,20 +306,36 @@ export async function verifyCertificate(input: {
|
||||
orderId: found.order_id,
|
||||
})
|
||||
|
||||
// 核销成功后把「去使用链接」发给买家(best-effort):
|
||||
// 兑换本身已经成功,通知失败只记错误日志,不让用户看到兑换失败。
|
||||
await notifyBuyerAfterVerify({ certificate: verified || found, config }).catch((error) => {
|
||||
logIntegration(
|
||||
'[douyin-mashida/certificate]',
|
||||
'兑换成功通知买家失败(兑换结果不受影响)',
|
||||
{ certNo, orderId: found.order_id, error },
|
||||
{ level: 'error' },
|
||||
)
|
||||
})
|
||||
// 抖音码速达订单**不再**在核销后补发第二条消息:
|
||||
// 买家在付款建单时已经收到「唯一那条领取链接」(见 douyin-mashida/notify-service.ts),
|
||||
// 之后再发一条带链接的消息会让买家拿到第二条链接、分不清该点哪个。
|
||||
// 其它平台(本表未来接入的 provider)行为保持不变:继续按 afterVerifyMessage/afterVerifyLink 通知。
|
||||
if (shouldNotifyBuyerAfterVerify(verified || found)) {
|
||||
await notifyBuyerAfterVerify({ certificate: verified || found, config }).catch((error) => {
|
||||
logIntegration(
|
||||
'[douyin-mashida/certificate]',
|
||||
'兑换成功通知买家失败(兑换结果不受影响)',
|
||||
{ certNo, orderId: found.order_id, error },
|
||||
{ level: 'error' },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
return verified || found
|
||||
}
|
||||
|
||||
/**
|
||||
* 核销后是否还要给买家发「去使用链接」消息。
|
||||
*
|
||||
* 抖音码速达:**不发**(买家已拿到领取链接;抖音皮肤/礼包单的领取链接本身就是完成入口)。
|
||||
* 其它平台:保持原行为(发)。
|
||||
*/
|
||||
export function shouldNotifyBuyerAfterVerify(
|
||||
certificate: Pick<RedeemCertificateRow, 'provider'> | null | undefined,
|
||||
): boolean {
|
||||
return String(certificate?.provider || '').trim() !== DOUYIN_MASHIDA_PROVIDER
|
||||
}
|
||||
|
||||
/** 拼 order.message 的正文:默认文案 +(可选的)去使用链接。 */
|
||||
export function buildAfterVerifyMessage(config: DouyinMashidaSourceConfig): string {
|
||||
const message = String(config.afterVerifyMessage || '').trim() || DEFAULT_AFTER_VERIFY_MESSAGE
|
||||
|
||||
Reference in New Issue
Block a user