增加订单号查询
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -170,6 +170,7 @@ export type AdminCloudtentaclesDeliveryRecordQueryInput = AdminCloudtentaclesCat
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
recordCode?: number | string
|
||||
platformOrderId?: string
|
||||
}
|
||||
|
||||
export type AdminCloudtentaclesSkuBuyInput = {
|
||||
|
||||
Reference in New Issue
Block a user