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:
@@ -1,6 +1,5 @@
|
||||
import { buildClaimUrl, buildSkinClaimUrl } from '../claim/claim-service.js'
|
||||
import { buildClaimUrl, buildConfiguredClaimUrl } from '../claim/claim-service.js'
|
||||
import { buildClaimIdentityAdminSummary } from '../claim/claim-identity.js'
|
||||
import { parseSkinClaimSubmission } from '../claim/claim-skin-service.js'
|
||||
import { getClaimTokenById } from '../../repositories/claim-token-repo.js'
|
||||
import {
|
||||
listOrderItemsByOrderId,
|
||||
@@ -37,7 +36,7 @@ import {
|
||||
} from '../../domain/task-status.js'
|
||||
import { getAffiliateDashConfig } from '../platforms/affiliate-dash/config.js'
|
||||
import { getDouyinMashidaSourceConfig } from '../platforms/douyin-mashida/source-config-service.js'
|
||||
import { isDouyinMashidaSkinExecutor } from '../fulfillment/executors/types.js'
|
||||
import { isDouyinMashidaProvider } from '../platforms/douyin-mashida/order-mapper.js'
|
||||
import {
|
||||
mergeAffiliateDashSkuMapping,
|
||||
resolveAffiliateDashDisplayProductName,
|
||||
@@ -81,7 +80,7 @@ import type {
|
||||
AdminTaskListQueryInput,
|
||||
AdminViewerSessionInput,
|
||||
} from '../../types/admin/read-inputs.js'
|
||||
import type { KuaishouIndustryVoucherRow, TaskRow } from '../../types/repository/rows.js'
|
||||
import type { KuaishouIndustryVoucherRow, OrderRow, TaskRow } from '../../types/repository/rows.js'
|
||||
|
||||
export async function getAdminOrders(
|
||||
query: AdminOrderListQueryInput = {},
|
||||
@@ -310,11 +309,10 @@ export async function getAdminTaskDetail(
|
||||
const taskContext = parseTaskContext(task)
|
||||
const taskState = parseTaskState(task)
|
||||
const viewerContext = createAdminViewerContext(session)
|
||||
// 统一对外领取入口为本站领取链接;平台 H5 仅作内部上下文。
|
||||
// ⚠️ 皮肤自动发货走**路径式**领取页(`<claimLinkBase>/<token>`):
|
||||
// 全局 buildClaimUrl 用的是 orders.claimBaseUrl(默认 hash 形式 `…/#/claim`),
|
||||
// 那是**打手**领取页;皮肤任务若用它,运营复制给买家会把人带进绑定 UID 的打手流程。
|
||||
const claimUrl = resolveAdminTaskClaimUrl(task, claimToken)
|
||||
// 后台回显的领取链接口径按**订单来源**决定(不再按 executor_key):
|
||||
// 抖音码速达订单用后台配置的 `claimLinkBase`(买家领取页,走 affiliate_dash 领取流程);
|
||||
// 其它来源沿用全局 buildClaimUrl(orders.claimBaseUrl)。
|
||||
const claimUrl = resolveAdminTaskClaimUrl(claimToken, order)
|
||||
const screenshotUrl = await resolveAdminTaskScreenshotUrl(task, viewerContext)
|
||||
const cloudSourceLabelMap = buildCloudSourceLabelMap()
|
||||
const kuaishouCloudFulfillment =
|
||||
@@ -390,11 +388,6 @@ export async function getAdminTaskDetail(
|
||||
claimUrl,
|
||||
}
|
||||
: null,
|
||||
// 皮肤自动发货:买家在领取页提交的游戏账号。运营要靠它发货,但属买家隐私,
|
||||
// 因此与 claimToken 的明文一样按 canViewSensitiveTaskData 门禁下发。
|
||||
claimSubmission: viewerContext.canViewSensitiveTaskData
|
||||
? mapClaimSubmissionForAdmin(task)
|
||||
: null,
|
||||
artifacts: viewerContext.canViewSensitiveTaskData ? safeParseJson(task.artifacts_json) : {},
|
||||
screenshotUrl,
|
||||
review: {
|
||||
@@ -443,48 +436,32 @@ export async function getAdminTaskDetail(
|
||||
/**
|
||||
* 后台展示的领取链接。
|
||||
*
|
||||
* - 皮肤自动发货(`douyin_mashida_skin`):`claimLinkBase` + token(路径式,买家领取页);
|
||||
* 未配 `claimLinkBase` 时退回全局 claim 链接,至少链接不会为空。
|
||||
* - 其它执行器:沿用全局 `buildClaimUrl`(hash 打手领取页),历史行为不变。
|
||||
* - 抖音码速达订单(`provider='douyin_mashida'`):`claimLinkBase` + token(买家领取页,
|
||||
* 走 affiliate_dash 领取流程);未配 `claimLinkBase` 时退回全局 claim 链接,至少链接不会为空。
|
||||
* - 其它来源:沿用全局 `buildClaimUrl`(orders.claimBaseUrl),历史行为不变。
|
||||
*
|
||||
* ⚠️ 这里按 **provider** 判断,不按 executor_key:抖音皮肤单已改走 `affiliate_dash` 通道,
|
||||
* 按执行器判断会失效。拿不到 provider(order 为空)时按「其它来源」处理。
|
||||
*/
|
||||
function resolveAdminTaskClaimUrl(
|
||||
task: Pick<TaskRow, 'executor_key'>,
|
||||
claimToken: { token: string } | null | undefined,
|
||||
order: Pick<OrderRow, 'provider'> | null | undefined,
|
||||
): string {
|
||||
const token = String(claimToken?.token || '').trim()
|
||||
if (!token) {
|
||||
return ''
|
||||
}
|
||||
|
||||
if (isDouyinMashidaSkinExecutor(task.executor_key)) {
|
||||
const skinUrl = buildSkinClaimUrl(getDouyinMashidaSourceConfig().claimLinkBase, token)
|
||||
if (skinUrl) {
|
||||
return skinUrl
|
||||
if (isDouyinMashidaProvider(order?.provider)) {
|
||||
const claimUrl = buildConfiguredClaimUrl(getDouyinMashidaSourceConfig().claimLinkBase, token)
|
||||
if (claimUrl) {
|
||||
return claimUrl
|
||||
}
|
||||
}
|
||||
|
||||
return buildClaimUrl(token)
|
||||
}
|
||||
|
||||
/**
|
||||
* 皮肤自动发货买家提交内容(游戏账号)。
|
||||
*
|
||||
* 只回传发货必需的字段;无提交(尚未领取)返回 null,前端据此区分「待领取」。
|
||||
* 敏感数据,调用方必须先判 `canViewSensitiveTaskData`。
|
||||
*/
|
||||
function mapClaimSubmissionForAdmin(task: Partial<TaskRow>): JsonRecord | null {
|
||||
const submission = parseSkinClaimSubmission(task)
|
||||
const gameAccount = String(submission.gameAccount || '').trim()
|
||||
if (!gameAccount) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
gameAccount,
|
||||
submittedAt: String(submission.submittedAt || '').trim(),
|
||||
}
|
||||
}
|
||||
|
||||
function mapKuaishouIndustryVoucherContext(value: unknown): JsonRecord | null {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return null
|
||||
|
||||
@@ -41,11 +41,11 @@ function mapAdminDouyinMashidaConfig(config: DouyinMashidaSourceConfig) {
|
||||
secretMasked: maskSecret(config.secret),
|
||||
hasSecret: Boolean(config.secret),
|
||||
certLink: config.certLink,
|
||||
/** 皮肤自动发货领取页基址(路径式,如 https://x.ngrok-free.app/claim) */
|
||||
/** 买家领取页基址(affiliate-dash 领取流程,如 https://<域名>/claim) */
|
||||
claimLinkBase: config.claimLinkBase,
|
||||
/** 皮肤自动发货「付款后发消息」模板,占位符 {link}/{sku}/{orderNo}/{productTitle} */
|
||||
/** 付款后发领取链接的 message 模板,占位符 {link}/{sku}/{orderNo}/{productTitle} */
|
||||
messageTemplate: config.messageTemplate,
|
||||
/** 皮肤自动发货「付款后发消息」开关 */
|
||||
/** 「付款后发领取链接」开关 */
|
||||
claimMessageEnabled: config.claimMessageEnabled,
|
||||
messageType: config.messageType,
|
||||
afterVerifyLink: config.afterVerifyLink,
|
||||
@@ -54,7 +54,7 @@ function mapAdminDouyinMashidaConfig(config: DouyinMashidaSourceConfig) {
|
||||
handledTopics: config.handledTopics,
|
||||
/**
|
||||
* 联调自查:uid / secret / 兑换链接 三项都齐了才算配置完整。
|
||||
* 语义保持不变(不因皮肤新字段变化),避免影响既有脚本与面板判断。
|
||||
* 语义保持不变(不因领取链接新字段变化),避免影响既有脚本与面板判断。
|
||||
*/
|
||||
ready: Boolean(config.enabled && config.uid && config.secret && config.certLink),
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user