fix(douyin-mashida): 平台会改写券码 —— 核销改用平台侧券码 + 失败自愈
真实联调踩到的坑(已用真单复现并定位): `coupons.syncV2` 返回 `10000 成功` **并不代表平台沿用了我们提交的 cert_no**。 实测 3 笔订单里有 2 笔,平台把我们的 DYC 券码换成了它自己生成的券码。 我们却一直拿**自己的**券码去 `coupons.verifyV3`,平台回 `10017 卡券核销失败[isv.business-failed:010021|核销码与订单不匹配]`; 更坑的是这个错误平台要 **~10.5 秒**才返回,正好越过当时的 10 秒超时, 前端只看到 `douyin_mashida_gateway_timeout`,完全看不出真实原因。 修复: - 新增 `coupons.list` 出网封装;同步成功后按 order_id 回查平台侧 `cert_no`/`cert_id` 并落库(迁移 095 新增 platform_cert_no / platform_cert_id / platform_status / platform_synced_at) - 核销时用 `resolveRedeemVerifyCertNo()`:**平台侧券码优先**,取不到才退回我方券码 - 核销失败后自愈 `tryRecoverVerify()`: ① 回查平台,若平台侧已核销 → 按对账结果标记本地已核销(修复"平台已核销、我们报失败"的漂移) ② 若是券码被平台改过 → 用平台券码**重试一次** - 回查不到该订单的券 → 标记 sync 失败并告警(暴露"syncV2 成功但平台无券"的幽灵券) - 默认出网超时 10s → 20s(平台业务错误要 ~10.5s 才返回,10s 会把业务错误伪装成超时) 实测验证:对一笔「平台侧已核销、本地仍是 synced」的订单调确认兑换, 10.7s 后返回 `status=verified`,platform_cert_no 成功回填为平台券码、 platform_status=3,并打出「对账发现平台侧已核销,按平台状态标记本地已核销」告警。
This commit is contained in:
@@ -55,6 +55,48 @@ export async function syncDouyinMashidaCoupons(
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 券码查询(`coupons.list`,基础功能)——**同步后取回平台侧真实券码的权威手段**。
|
||||
*
|
||||
* 实测发现 `coupons.syncV2` 不保证平台沿用我们提交的 `cert_no`(3 笔里 2 笔被平台改名),
|
||||
* 拿我们自己的券码去核销会得到 `10017 核销码与订单不匹配`(且该错误要 ~10.5s 才返回)。
|
||||
* 所以同步成功后必须用它按 `order_id` 查回平台的 `cert_no` / `cert_id`。
|
||||
*/
|
||||
export async function fetchDouyinMashidaCoupons(
|
||||
input: {
|
||||
orderId?: string | undefined
|
||||
certNo?: string | undefined
|
||||
certType?: number | undefined
|
||||
} & DouyinMashidaCallOptions,
|
||||
): Promise<Array<Record<string, unknown>>> {
|
||||
const params: Record<string, string> = {
|
||||
// 0 = 三方卡券(我们这种自备券码的就是三方卡券);1 会查不到任何券
|
||||
cert_type: String(input.certType === undefined ? 0 : input.certType),
|
||||
}
|
||||
const orderId = String(input.orderId || '').trim()
|
||||
const certNo = String(input.certNo || '').trim()
|
||||
if (orderId) {
|
||||
params.order_id = orderId
|
||||
}
|
||||
if (certNo) {
|
||||
params.cert_no = certNo
|
||||
}
|
||||
|
||||
const response = await douyinMashidaGatewayRequest({
|
||||
method: 'coupons.list',
|
||||
params,
|
||||
config: input.config,
|
||||
timeoutMs: input.timeoutMs,
|
||||
})
|
||||
|
||||
return Array.isArray(response.data)
|
||||
? response.data.filter(
|
||||
(row): row is Record<string, unknown> =>
|
||||
Object.prototype.toString.call(row) === '[object Object]',
|
||||
)
|
||||
: []
|
||||
}
|
||||
|
||||
/** 卡券核销(高级功能):只认券码,所以券码必须能反查本地订单。 */
|
||||
export async function verifyDouyinMashidaCoupon(
|
||||
input: { certNo: string } & DouyinMashidaCallOptions,
|
||||
|
||||
@@ -110,7 +110,9 @@ export function normalizeDouyinMashidaSourceConfig(rawValue: unknown): DouyinMas
|
||||
messageType: normalizeMessageType(source.messageType),
|
||||
afterVerifyLink: String(source.afterVerifyLink || '').trim(),
|
||||
afterVerifyMessage: String(source.afterVerifyMessage || '').trim(),
|
||||
timeoutMs: normalizePositiveInteger(source.timeoutMs, 10000),
|
||||
// ⚠️ 默认 20s 而不是 10s:平台在「券码与订单不匹配」这类业务错误上要 ~10.5s 才返回,
|
||||
// 10s 会让所有业务错误都伪装成"网关超时",排查时完全看不出真实原因(实测踩过)。
|
||||
timeoutMs: normalizePositiveInteger(source.timeoutMs, 20000),
|
||||
handledTopics: normalizeHandledTopics(source.handledTopics),
|
||||
}
|
||||
}
|
||||
@@ -125,7 +127,7 @@ export function createDefaultDouyinMashidaSourceConfig(): DouyinMashidaSourceCon
|
||||
messageType: 2,
|
||||
afterVerifyLink: '',
|
||||
afterVerifyMessage: DEFAULT_AFTER_VERIFY_MESSAGE,
|
||||
timeoutMs: 10000,
|
||||
timeoutMs: 20000,
|
||||
handledTopics: [...DOUYIN_MASHIDA_DEFAULT_HANDLED_TOPICS],
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user