优化 UID 履约闸门、运营可见性与一键兑换

lewan 发货强制 expectedUid 并取消无 UID 旧路径回落;任务详情展示 UID 匹配态;领取页匹配后可一键兑换;补充 identity 单测并收口短链说明。
This commit is contained in:
yml2213
2026-07-10 13:51:02 +08:00
parent dc89188f7a
commit 8de56b0354
13 changed files with 383 additions and 152 deletions
@@ -4,7 +4,7 @@ import type { TaskRow } from "../../../types/repository/rows.js";
import { createHttpError } from "../../../utils/http.js";
import { getCloudtentaclesBindInfo } from "../../platforms/cloudtentacles/virtual-number-service.js";
import {
getClaimIdentityFromContext,
assertLewanAutoFulfillmentUidReady,
isClaimUidMatched,
normalizeClaimUid,
} from "../../claim/claim-identity.js";
@@ -36,7 +36,12 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
const flow = normalizeKuaishouCloudFlow(
input.flow || taskContext.kuaishouCloudFulfillment
);
const expectedUid = getClaimIdentityFromContext(taskContext).expectedUid;
// 新策略:lewan 自动发货必须有 expectedUid,旧单无 UID 不可静默回落
const expectedUid = assertLewanAutoFulfillmentUidReady(task, {
allowMockSkip: true,
errorCodePrefix,
});
if (!flow.binding.vnId || !flow.binding.vnKey) {
throw createHttpError("当前任务缺少可同步角色的虚拟号信息,暂时不能发货", {
@@ -62,76 +67,30 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
);
}
const confirmedRoleName = String(
flow.binding.roleName || flow.role.name || task.role_name || ""
).trim();
const confirmedRoleId = String(
flow.binding.roleId || flow.role.rid || task.role_id || ""
).trim();
const liveBoundUid = normalizeClaimUid(roleInfo.rid);
// 优先:声明 UID 与云端当前绑定角色一致
if (expectedUid) {
if (!isClaimUidMatched(expectedUid, liveBoundUid)) {
await createTaskEvent(
task.id,
"kuaishou_cloud_dispatch_uid_mismatch",
{
source,
vnId: flow.binding.vnId,
expectedUid,
cloudtentaclesRoleName: roleInfo.name,
cloudtentaclesRoleId: roleInfo.rid,
actor: input.actor || null,
},
now
);
throw createHttpError(
`cloudtentacles 当前绑定角色 ID${roleInfo.rid})与用户填写 UID${expectedUid})不一致,不能发货`,
{
statusCode: 409,
errorCode: `${errorCodePrefix}_dispatch_uid_mismatch`,
}
);
}
} else {
// 兼容旧任务:无 expectedUid 时仍比对「确认角色」
const roleMatches = isSameKuaishouCloudRole(
if (expectedUid && !isClaimUidMatched(expectedUid, liveBoundUid)) {
await createTaskEvent(
task.id,
"kuaishou_cloud_dispatch_uid_mismatch",
{
name: confirmedRoleName,
rid: confirmedRoleId,
source,
vnId: flow.binding.vnId,
expectedUid,
cloudtentaclesRoleName: roleInfo.name,
cloudtentaclesRoleId: roleInfo.rid,
actor: input.actor || null,
},
{
name: roleInfo.name,
rid: roleInfo.rid,
}
now
);
if (!roleMatches) {
await createTaskEvent(
task.id,
"kuaishou_cloud_dispatch_role_mismatch",
{
source,
vnId: flow.binding.vnId,
confirmedRoleName,
confirmedRoleId,
cloudtentaclesRoleName: roleInfo.name,
cloudtentaclesRoleId: roleInfo.rid,
actor: input.actor || null,
},
now
);
throw createHttpError(
`cloudtentacles 当前绑定角色(${roleInfo.name}/${roleInfo.rid})与客户确认角色(${confirmedRoleName || "-"}/${confirmedRoleId || "-"})不一致,请重新刷新角色后再发货`,
{
statusCode: 409,
errorCode: `${errorCodePrefix}_dispatch_role_mismatch`,
}
);
}
throw createHttpError(
`cloudtentacles 当前绑定角色 ID${roleInfo.rid})与用户填写 UID${expectedUid})不一致,不能发货`,
{
statusCode: 409,
errorCode: `${errorCodePrefix}_dispatch_uid_mismatch`,
}
);
}
const nextFlow = normalizeKuaishouCloudFlow({
@@ -191,62 +150,4 @@ export async function syncKuaishouCloudRoleInfoBeforeDispatch(
};
}
function isSameKuaishouCloudRole(
confirmedRole: { name: string; rid: string },
cloudtentaclesRole: { name: string; rid: string }
) {
if (confirmedRole.rid && cloudtentaclesRole.rid) {
return confirmedRole.rid === cloudtentaclesRole.rid;
}
const confirmedName = normalizeKuaishouCloudRoleNameForCompare(
confirmedRole.name
);
const cloudtentaclesName = normalizeKuaishouCloudRoleNameForCompare(
cloudtentaclesRole.name
);
if (!confirmedName || !cloudtentaclesName) {
return true;
}
return confirmedName === cloudtentaclesName;
}
function normalizeKuaishouCloudRoleNameForCompare(value: unknown) {
const name = String(value || "").trim();
if (!name) {
return "";
}
const parts = name
.split("-")
.map((part) => part.trim())
.filter(Boolean);
const devicePart = parts[0] || "";
const channelPart = parts[1] || "";
if (
parts.length >= 3 &&
isKuaishouCloudDevicePart(devicePart) &&
isKuaishouCloudChannelPart(channelPart)
) {
return normalizeTextForCompare(parts.slice(2).join("-"));
}
return normalizeTextForCompare(name);
}
function isKuaishouCloudDevicePart(value: string) {
return ["安卓", "android", "ios", "苹果"].includes(value.toLowerCase());
}
function isKuaishouCloudChannelPart(value: string) {
return ["qq", "微信", "vx", "wechat", "v"].includes(value.toLowerCase());
}
function normalizeTextForCompare(value: unknown) {
return String(value || "")
.trim()
.toLowerCase()
.replace(/\s+/g, "");
}