You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
174 lines
5.1 KiB
174 lines
5.1 KiB
import { prisma } from "@/services/db";
|
|
import { getEnv } from "@/lib/env";
|
|
import { runWithMailCcOwner } from "@/lib/cc-owner";
|
|
import { logger } from "@/lib/logger";
|
|
import { writeAudit } from "@/services/audit";
|
|
import { assertTransition } from "@/services/state-machine";
|
|
import { executeInstructionWrite } from "@/services/cc/instruction-write";
|
|
import type { MailStatus, ShipmentRow } from "@/types/mail";
|
|
|
|
function kindForMailType(
|
|
mailType: string,
|
|
): "transfer" | "hold_split" | "label" | null {
|
|
if (mailType === "TRANSFER") return "transfer";
|
|
if (mailType === "INSTRUCTION_HOLD_SPLIT") return "hold_split";
|
|
if (mailType === "INSTRUCTION_LABEL") return "label";
|
|
return null;
|
|
}
|
|
|
|
function autoExecEnabled(mailType: string): boolean {
|
|
const env = getEnv();
|
|
if (mailType === "TRANSFER") return env.ENABLE_AUTO_EXEC_TRANSFER;
|
|
if (mailType === "INSTRUCTION_HOLD_SPLIT") return env.ENABLE_AUTO_EXEC_HOLD_SPLIT;
|
|
if (mailType === "INSTRUCTION_LABEL") return env.ENABLE_AUTO_EXEC_LABEL;
|
|
return false;
|
|
}
|
|
|
|
export function isAutoExecEligible(mailType: string): boolean {
|
|
// 四类主业务走人工确认,不再自动写
|
|
if (
|
|
mailType === "WORK_ORDER" ||
|
|
mailType === "DO_UPLOAD" ||
|
|
mailType === "TRANSFER" ||
|
|
mailType === "NEW_CONTAINER"
|
|
) {
|
|
return false;
|
|
}
|
|
if (getEnv().AUTO_EXEC_REQUIRE_CONFIRM) return false;
|
|
return autoExecEnabled(mailType) && kindForMailType(mailType) != null;
|
|
}
|
|
|
|
/** 解析后或 due 调度:对 AUTO_EXECUTING 邮件调 CC 写接口 */
|
|
export async function runAutoExec(mailId: bigint): Promise<{
|
|
ok: boolean;
|
|
status: string;
|
|
error?: string;
|
|
}> {
|
|
return runWithMailCcOwner(mailId, () => runAutoExecInner(mailId));
|
|
}
|
|
|
|
async function runAutoExecInner(mailId: bigint): Promise<{
|
|
ok: boolean;
|
|
status: string;
|
|
error?: string;
|
|
}> {
|
|
const mail = await prisma.mailMessage.findUnique({
|
|
where: { id: mailId },
|
|
include: { parseResult: true },
|
|
});
|
|
if (!mail) return { ok: false, status: "MISSING", error: "NOT_FOUND" };
|
|
|
|
const kind = kindForMailType(mail.mailType);
|
|
if (!kind || !autoExecEnabled(mail.mailType)) {
|
|
if (mail.status === "AUTO_EXECUTING") {
|
|
await prisma.mailMessage.update({
|
|
where: { id: mailId },
|
|
data: { status: "PARSED", lastError: "AUTO_EXEC_DISABLED" },
|
|
});
|
|
}
|
|
return { ok: false, status: "SKIPPED", error: "DISABLED" };
|
|
}
|
|
|
|
const from = mail.status as MailStatus;
|
|
if (from !== "AUTO_EXECUTING") {
|
|
assertTransition(from, "AUTO_EXECUTING");
|
|
await prisma.mailMessage.update({
|
|
where: { id: mailId },
|
|
data: { status: "AUTO_EXECUTING", version: { increment: 1 } },
|
|
});
|
|
}
|
|
|
|
const header = mail.parseResult?.containerHeader as
|
|
| { F_ContainerNo?: string; F_Instruction?: string }
|
|
| undefined;
|
|
const shipments = ((mail.parseResult?.shipments || []) as unknown) as ShipmentRow[];
|
|
|
|
const result = await executeInstructionWrite(kind, {
|
|
mailId: mailId.toString(),
|
|
containerNo: header?.F_ContainerNo,
|
|
subject: mail.subject,
|
|
shipments,
|
|
remark: header?.F_Instruction || null,
|
|
});
|
|
|
|
const action =
|
|
kind === "transfer"
|
|
? "AUTO_EXEC_TRANSFER"
|
|
: kind === "hold_split"
|
|
? "AUTO_EXEC_HOLD_SPLIT"
|
|
: "AUTO_EXEC_LABEL";
|
|
|
|
await writeAudit({
|
|
actor: "system",
|
|
action,
|
|
mailId,
|
|
payload: {
|
|
mode: result.mode,
|
|
ok: result.ok,
|
|
request: result.request,
|
|
response: result.response,
|
|
error: result.error,
|
|
},
|
|
});
|
|
|
|
if (result.ok) {
|
|
assertTransition("AUTO_EXECUTING", "SUCCESS");
|
|
await prisma.mailMessage.update({
|
|
where: { id: mailId },
|
|
data: {
|
|
status: "SUCCESS",
|
|
lastError: result.mode === "mock" ? "AUTO_EXEC_MOCK_OK" : null,
|
|
version: { increment: 1 },
|
|
},
|
|
});
|
|
if (header?.F_ContainerNo) {
|
|
await prisma.containerImport.create({
|
|
data: {
|
|
mailId,
|
|
containerNo: header.F_ContainerNo,
|
|
externalId: result.externalId || null,
|
|
requestBody: JSON.stringify(result.request),
|
|
responseBody: JSON.stringify(result.response),
|
|
status: "SUCCESS",
|
|
shipmentsHash: "auto-exec",
|
|
importKind: "forecast",
|
|
},
|
|
}).catch(() => undefined);
|
|
}
|
|
logger.info({ mailId: String(mailId), kind, mode: result.mode }, "auto_exec.ok");
|
|
return { ok: true, status: "SUCCESS" };
|
|
}
|
|
|
|
assertTransition("AUTO_EXECUTING", "FAILED");
|
|
await prisma.mailMessage.update({
|
|
where: { id: mailId },
|
|
data: {
|
|
status: "FAILED",
|
|
lastError: (result.error || "AUTO_EXEC_FAILED").slice(0, 1000),
|
|
version: { increment: 1 },
|
|
},
|
|
});
|
|
return { ok: false, status: "FAILED", error: result.error };
|
|
}
|
|
|
|
export async function processDueAutoExec(limit = 10): Promise<{
|
|
processed: number;
|
|
ok: number;
|
|
fail: number;
|
|
}> {
|
|
const rows = await prisma.mailMessage.findMany({
|
|
where: { status: "AUTO_EXECUTING" },
|
|
orderBy: { updatedAt: "asc" },
|
|
take: limit,
|
|
select: { id: true },
|
|
});
|
|
let ok = 0;
|
|
let fail = 0;
|
|
for (const r of rows) {
|
|
const res = await runAutoExec(r.id);
|
|
if (res.ok) ok += 1;
|
|
else fail += 1;
|
|
}
|
|
return { processed: rows.length, ok, fail };
|
|
}
|