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

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 };
}