增加订单号查询

This commit is contained in:
yml2213
2026-05-31 17:39:17 +08:00
parent e8c41f774c
commit 9219ad18a5
5 changed files with 530 additions and 25 deletions
@@ -62,6 +62,8 @@ import {
maskPhone,
} from "./mappers.js";
import { createHttpError } from "../../../../utils/http.js";
import { query as dbQuery } from "../../../../db/client.js";
import { parseJsonObject } from "../../../../utils/task-json.js";
import type {
AdminCloudtentaclesCatalogQueryInput,
@@ -79,6 +81,54 @@ import type {
type JsonObject = Record<string, any>;
type LocalCloudtentaclesTask = {
taskId: number;
taskNo: string;
platformOrderId: string;
orderShopId: string;
orderShopName: string;
taskStatus: string;
deliveryStatus: string;
resultCode: string;
resultMessage: string;
createdAt: string;
updatedAt: string;
skuCode: string;
skuName: string;
sourceKey: string;
vnId: number;
vnPhone: string;
roleName: string;
roleId: string;
localDispatchStatus: string;
localDispatchAt: string;
consumeShopId: string;
consumeShopName: string;
};
type LocalCloudtentaclesTaskRow = {
id: number;
task_no: string;
platform_order_id: string;
shop_id: string;
shop_name: string;
task_status: string;
delivery_status: string;
result_code: string;
result_message: string;
context_json: unknown;
created_at: string;
updated_at: string;
sku_code?: string;
sku_name?: string;
kuaishou_source_key?: string;
kuaishou_vn_phone?: string;
kuaishou_role_name?: string;
kuaishou_role_id?: string;
kuaishou_dispatch_status?: string;
kuaishou_consume_status?: string;
};
export function listAdminCloudtentaclesSources() {
const list = listCloudtentaclesSources();
const sessions = getAllCloudtentaclesSessionStates();
@@ -333,14 +383,35 @@ export async function getAdminCloudtentaclesKnapsack(
export async function listAdminCloudtentaclesDeliveryRecords(
payload: AdminCloudtentaclesDeliveryRecordQueryInput = {}
) {
const context = resolveAdminCloudtentaclesSessionPayload(payload);
const platformOrderId = normalizeText(payload.platformOrderId);
const preferredSourceKey = normalizeText(payload.sourceKey);
const orderMatchedTasks = platformOrderId
? await listLocalCloudtentaclesTasks({
platformOrderId,
limit: 50,
})
: [];
const orderSourceKey =
orderMatchedTasks.find((task) => !preferredSourceKey || task.sourceKey === preferredSourceKey)
?.sourceKey || orderMatchedTasks.find((task) => task.sourceKey)?.sourceKey;
const resolvedSourceKey =
orderSourceKey ||
preferredSourceKey ||
"default";
const context = resolveAdminCloudtentaclesSessionPayload({
...payload,
sourceKey: resolvedSourceKey,
});
const recordWindow = resolveRecordQueryWindow(payload, orderMatchedTasks);
const recordPage = platformOrderId ? 1 : payload.page;
const recordSize = platformOrderId ? 1000 : payload.size;
const query = {
...context,
...pickDefined({
page: payload.page,
size: payload.size,
startDate: payload.startDate,
endDate: payload.endDate,
page: recordPage,
size: recordSize,
startDate: recordWindow.startDate,
endDate: recordWindow.endDate,
recordCode: payload.recordCode,
}),
};
@@ -368,16 +439,59 @@ export async function listAdminCloudtentaclesDeliveryRecords(
}
const recordItems: JsonObject[] = Array.isArray(records.items) ? records.items : [];
const localTasks =
platformOrderId || recordItems.length === 0
? orderMatchedTasks
: await listLocalCloudtentaclesTasks({
sourceKey: resolvedSourceKey,
startDate: recordWindow.startDate,
endDate: recordWindow.endDate,
limit: 800,
});
const enrichedItems = recordItems
.map((record) => {
const localMatch = findBestLocalTaskForRecord(record, localTasks);
return {
...record,
...(skuImageByName.get(normalizeSkuName(record.name)) || {
skuId: 0,
skuName: "",
skuImage: "",
}),
...mapLocalTaskRecordEnhancement(localMatch?.task || null, localMatch?.confidence || ""),
};
})
.filter((record) => {
if (!platformOrderId) {
return true;
}
return normalizeText(record.platformOrderId) === platformOrderId;
});
const fallbackItems = platformOrderId
? orderMatchedTasks
.filter((task) => !enrichedItems.some((record) => Number(record.taskId || 0) === task.taskId))
.map((task) => ({
...createLocalTaskFallbackRecord(task),
...(skuImageByName.get(normalizeSkuName(task.skuName)) || {
skuId: 0,
skuName: "",
skuImage: "",
}),
...mapLocalTaskRecordEnhancement(task, "local"),
}))
: [];
const finalItems = platformOrderId ? [...enrichedItems, ...fallbackItems] : enrichedItems;
return {
...records,
items: recordItems.map((record) => ({
...record,
...(skuImageByName.get(normalizeSkuName(record.name)) || {
skuId: 0,
skuName: "",
skuImage: "",
}),
})),
sourceKey: resolvedSourceKey,
queryStartDate: recordWindow.startDate,
queryEndDate: recordWindow.endDate,
total: platformOrderId ? finalItems.length : records.total,
page: platformOrderId ? 1 : records.page,
size: platformOrderId ? recordSize : records.size,
localOrderMatched: platformOrderId ? orderMatchedTasks.length > 0 : false,
items: finalItems,
};
}
@@ -478,3 +592,284 @@ function pickDefined(values: JsonObject = {}) {
function normalizeSkuName(value: unknown) {
return String(value || "").trim().replace(/\s+/g, "").toLowerCase();
}
function normalizeText(value: unknown) {
return String(value || "").trim();
}
function normalizePhone(value: unknown) {
return normalizeText(value).replace(/\D+/g, "");
}
function normalizeNumber(value: unknown) {
const parsed = Number(value);
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
}
function asRecord(value: unknown): JsonObject {
return value && typeof value === "object" && !Array.isArray(value)
? (value as JsonObject)
: {};
}
function pickFirstNonEmptyText(values: unknown[]) {
for (const value of values) {
const normalized = normalizeText(value);
if (normalized) {
return normalized;
}
}
return "";
}
function resolveRecordQueryWindow(
payload: AdminCloudtentaclesDeliveryRecordQueryInput = {},
tasks: LocalCloudtentaclesTask[] = []
) {
if (tasks.length === 0) {
return {
startDate: payload.startDate,
endDate: payload.endDate,
};
}
const timestamps = tasks
.flatMap((task) => [task.createdAt, task.updatedAt, task.localDispatchAt])
.map((value) => new Date(value).getTime())
.filter((value) => Number.isFinite(value));
if (timestamps.length === 0) {
return {
startDate: payload.startDate,
endDate: payload.endDate,
};
}
const hourMs = 60 * 60 * 1000;
return {
startDate: new Date(Math.min(...timestamps) - hourMs).toISOString(),
endDate: new Date(Math.max(...timestamps) + 6 * hourMs).toISOString(),
};
}
async function listLocalCloudtentaclesTasks(options: {
platformOrderId?: string | undefined;
sourceKey?: string | undefined;
startDate?: string | undefined;
endDate?: string | undefined;
limit?: number;
} = {}): Promise<LocalCloudtentaclesTask[]> {
const filters: string[] = ["ft.executor_key = 'kuaishou_ct_assisted'"];
const params: unknown[] = [];
const platformOrderId = normalizeText(options.platformOrderId);
const sourceKey = normalizeText(options.sourceKey);
const startDate = normalizeText(options.startDate);
const endDate = normalizeText(options.endDate);
if (platformOrderId) {
params.push(`%${platformOrderId}%`);
filters.push(`ft.platform_order_id ILIKE $${params.length}`);
}
if (sourceKey) {
params.push(sourceKey);
filters.push(
`COALESCE(NULLIF(kcts.source_key, ''), ft.context_json #>> '{kuaishouCloudFulfillment,binding,resolvedSourceKey}', '') = $${params.length}`
);
}
if (startDate) {
params.push(startDate);
filters.push(`ft.updated_at >= ($${params.length})::timestamptz - interval '1 day'`);
}
if (endDate) {
params.push(endDate);
filters.push(`ft.created_at <= ($${params.length})::timestamptz + interval '1 day'`);
}
params.push(Math.min(Math.max(Number(options.limit || 500), 1), 1000));
const result = await dbQuery<LocalCloudtentaclesTaskRow>(
`
SELECT
ft.id,
ft.task_no,
ft.platform_order_id,
ft.shop_id,
ft.shop_name,
ft.task_status,
ft.delivery_status,
ft.result_code,
ft.result_message,
ft.context_json,
ft.created_at,
ft.updated_at,
oi.sku_code,
oi.sku_name,
kcts.source_key AS kuaishou_source_key,
kcts.vn_phone AS kuaishou_vn_phone,
kcts.role_name AS kuaishou_role_name,
kcts.role_id AS kuaishou_role_id,
kcts.dispatch_status AS kuaishou_dispatch_status,
kcts.consume_status AS kuaishou_consume_status
FROM fulfillment_tasks ft
LEFT JOIN order_items oi ON oi.id = ft.order_item_id
LEFT JOIN kuaishou_cloud_task_states kcts ON kcts.task_id = ft.id
WHERE ${filters.join(" AND ")}
ORDER BY ft.id DESC
LIMIT $${params.length}
`,
params
);
return result.rows.map(mapLocalCloudtentaclesTask);
}
function mapLocalCloudtentaclesTask(row: LocalCloudtentaclesTaskRow): LocalCloudtentaclesTask {
const context = parseJsonObject(row.context_json);
const flow = asRecord(context.kuaishouCloudFulfillment);
const binding = asRecord(flow.binding);
const role = asRecord(flow.role);
const dispatch = asRecord(flow.dispatch);
const consume = asRecord(flow.consume);
return {
taskId: Number(row.id || 0),
taskNo: normalizeText(row.task_no),
platformOrderId: normalizeText(row.platform_order_id),
orderShopId: normalizeText(row.shop_id),
orderShopName: normalizeText(row.shop_name),
taskStatus: normalizeText(row.task_status),
deliveryStatus: normalizeText(row.delivery_status),
resultCode: normalizeText(row.result_code),
resultMessage: normalizeText(row.result_message),
createdAt: normalizeText(row.created_at),
updatedAt: normalizeText(row.updated_at),
skuCode: normalizeText(row.sku_code),
skuName: pickFirstNonEmptyText([row.sku_name, flow.internalSkuName]),
sourceKey: pickFirstNonEmptyText([row.kuaishou_source_key, binding.resolvedSourceKey]),
vnId: normalizeNumber(binding.vnId),
vnPhone: pickFirstNonEmptyText([row.kuaishou_vn_phone, binding.vnPhone]),
roleName: pickFirstNonEmptyText([row.kuaishou_role_name, role.name, binding.roleName]),
roleId: pickFirstNonEmptyText([row.kuaishou_role_id, role.rid, binding.roleId]),
localDispatchStatus: pickFirstNonEmptyText([row.kuaishou_dispatch_status, dispatch.status]),
localDispatchAt: normalizeText(dispatch.dispatchAt),
consumeShopId: normalizeText(consume.shopId),
consumeShopName: normalizeText(consume.shopName),
};
}
function findBestLocalTaskForRecord(
record: JsonObject,
tasks: LocalCloudtentaclesTask[] = []
) {
let best: { task: LocalCloudtentaclesTask; score: number; confidence: string } | null = null;
for (const task of tasks) {
const score = scoreLocalTaskRecordMatch(record, task);
if (score <= 0 || (best && score <= best.score)) {
continue;
}
best = {
task,
score,
confidence: score >= 80 ? "high" : score >= 45 ? "medium" : "low",
};
}
return best && best.score >= 35 ? best : null;
}
function scoreLocalTaskRecordMatch(record: JsonObject, task: LocalCloudtentaclesTask) {
let score = 0;
const recordVnId = normalizeNumber(record.virtualNumberId);
const recordPhone = normalizePhone(record.phone);
const taskPhone = normalizePhone(task.vnPhone);
const recordName = normalizeSkuName(record.name);
const taskSkuName = normalizeSkuName(task.skuName);
const recordRoleName = normalizeSkuName(record.fulfillUser);
const taskRoleName = normalizeSkuName(task.roleName);
if (recordVnId > 0 && task.vnId > 0 && recordVnId === task.vnId) {
score += 50;
}
if (recordPhone && taskPhone && recordPhone === taskPhone) {
score += 30;
}
if (recordName && taskSkuName && recordName === taskSkuName) {
score += 20;
}
if (recordRoleName && taskRoleName && recordRoleName.includes(taskRoleName)) {
score += 10;
}
const recordTime = new Date(record.createdAt).getTime();
const dispatchTime = new Date(task.localDispatchAt || task.updatedAt || task.createdAt).getTime();
if (Number.isFinite(recordTime) && Number.isFinite(dispatchTime)) {
const distanceHours = Math.abs(recordTime - dispatchTime) / (60 * 60 * 1000);
if (distanceHours <= 6) {
score += 15;
} else if (distanceHours <= 24) {
score += 6;
}
}
return score;
}
function mapLocalTaskRecordEnhancement(
task: LocalCloudtentaclesTask | null,
confidence = ""
) {
return {
platformOrderId: task?.platformOrderId || "",
taskId: task?.taskId || 0,
taskNo: task?.taskNo || "",
orderShopId: task?.orderShopId || "",
orderShopName: task?.orderShopName || "",
consumeShopId: task?.consumeShopId || "",
consumeShopName: task?.consumeShopName || "",
roleName: task?.roleName || "",
roleId: task?.roleId || "",
localTaskStatus: task?.taskStatus || "",
localDeliveryStatus: task?.deliveryStatus || "",
localDispatchStatus: task?.localDispatchStatus || "",
localDispatchAt: task?.localDispatchAt || "",
localResultCode: task?.resultCode || "",
localResultMessage: task?.resultMessage || "",
matchConfidence: confidence,
};
}
function createLocalTaskFallbackRecord(task: LocalCloudtentaclesTask) {
return {
createdAt: task.localDispatchAt || task.updatedAt || task.createdAt,
recordId: "",
virtualNumberId: task.vnId,
userId: 0,
count: 1,
cdk: "",
exchangeUrl: "",
name: task.skuName,
phone: task.vnPhone,
status: mapLocalDispatchStatusToCloudStatus(task.localDispatchStatus),
fulfillUser: task.roleName,
operatorName: "",
raw: {},
};
}
function mapLocalDispatchStatusToCloudStatus(status: string) {
const normalized = normalizeText(status);
if (normalized === "success") {
return 4;
}
if (normalized === "failed") {
return 3;
}
if (normalized === "pending") {
return 2;
}
return 0;
}