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:
yml2213
2026-10-09 16:13:39 +08:00
parent 56ad06239b
commit 4fce07a621
37 changed files with 590 additions and 1300 deletions
@@ -4,8 +4,14 @@ import {
} from '../../../repositories/claim-token-repo.js'
import { updateTask } from '../../../repositories/task-repo.js'
import { createTaskEvent } from '../../../repositories/task-event-repo.js'
import { createTaskClaimToken, buildClaimUrl } from '../../claim/claim-service.js'
import { resolveDouyinMashidaSkinClaimUrl } from '../../platforms/douyin-mashida/claim-link.js'
import { getOrderById } from '../../../repositories/order-repo.js'
import {
createTaskClaimToken,
buildClaimUrl,
buildConfiguredClaimUrl,
} from '../../claim/claim-service.js'
import { getDouyinMashidaSourceConfig } from '../../platforms/douyin-mashida/source-config-service.js'
import { isDouyinMashidaProvider } from '../../platforms/douyin-mashida/order-mapper.js'
import { notifyTaskAutoManualReview } from '../../notification/domain-notifications.js'
import { preparePaidFulfillmentTask } from '../../fulfillment/executors/registry.js'
import {
@@ -13,10 +19,7 @@ import {
canRegenerateClaimLinkStatus,
TASK_STATUS,
} from '../../../domain/task-status.js'
import {
isDouyinMashidaSkinExecutor,
isRetiredFulfillmentExecutor,
} from '../../fulfillment/executors/types.js'
import { isRetiredFulfillmentExecutor } from '../../fulfillment/executors/types.js'
import { createHttpError } from '../../../utils/http.js'
import { nowIso } from '../../../utils/time.js'
import {
@@ -38,6 +41,7 @@ import type {
AdminTaskActionResponse,
AdminTaskManualDispatchResponse,
} from '../../../types/admin/write-models.js'
import type { TaskRow } from '../../../types/repository/rows.js'
export async function closeAdminTask(
taskId: AdminEntityIdInput,
@@ -226,7 +230,7 @@ export async function retryAdminTask(
}
if (claimToken) {
response.claimUrl = resolveAdminTaskClaimUrl(effectiveTask, claimToken)
response.claimUrl = await resolveAdminTaskClaimUrl(effectiveTask, claimToken)
response.token = claimToken
}
@@ -234,15 +238,32 @@ export async function retryAdminTask(
}
/**
* 后台回显的领取链接口径:
* - 皮肤自动发货任务:路径式皮肤领取链接(`claimLinkBase` + token),**不能**用 hash 版通用领取页;
* 后台回显的领取链接口径(按**订单来源**判断,不按 executor_key):
* - 抖音码速达订单:后台配置的 `claimLinkBase` + token(买家领取页,走 affiliate_dash 领取流程);
* 未配 `claimLinkBase` 时退回通用领取页,至少链接不会为空;
* - 其它渠道:沿用 runtimeConfig 的通用领取页(行为完全不变)。
*/
function resolveAdminTaskClaimUrl(task: { executor_key?: unknown }, token: string): string {
if (isDouyinMashidaSkinExecutor(task.executor_key)) {
return resolveDouyinMashidaSkinClaimUrl(token)
async function resolveAdminTaskClaimUrl(
task: Pick<TaskRow, 'order_id'>,
token: string,
): Promise<string> {
const normalizedToken = String(token || '').trim()
if (!normalizedToken) {
return ''
}
return buildClaimUrl(token)
const order = await getOrderById(task.order_id)
if (isDouyinMashidaProvider(order?.provider)) {
const claimUrl = buildConfiguredClaimUrl(
getDouyinMashidaSourceConfig().claimLinkBase,
normalizedToken,
)
if (claimUrl) {
return claimUrl
}
}
return buildClaimUrl(normalizedToken)
}
export async function regenerateAdminTaskClaimLink(
@@ -323,7 +344,7 @@ export async function regenerateAdminTaskClaimLink(
return {
task: mapTaskActionPayload(updatedTask),
claimUrl: resolveAdminTaskClaimUrl(updatedTask, claimToken.token),
claimUrl: await resolveAdminTaskClaimUrl(updatedTask, claimToken.token),
token: claimToken.token,
}
}