H1-H10 优化OCR实时体验闭环

这个提交包含在:
2026-05-09 11:48:57 +08:00
父节点 549bd81e30
当前提交 c26cf9b954
共修改 34 个文件,包含 1217 行新增和 184 行删除
+104
查看文件
@@ -0,0 +1,104 @@
const { EventEmitter } = require("node:events");
function uniqueStrings(values) {
return Array.from(
new Set(
(Array.isArray(values) ? values : [])
.map((value) => (typeof value === "string" ? value.trim() : ""))
.filter(Boolean),
),
);
}
function createAppEventBus() {
const emitter = new EventEmitter();
emitter.setMaxListeners(500);
let sequence = 0;
return {
publish(event) {
const payload = {
id: `${Date.now()}-${++sequence}`,
createdAt: new Date().toISOString(),
...event,
audienceEmployeeIds: uniqueStrings(event?.audienceEmployeeIds),
};
emitter.emit("event", payload);
return payload;
},
subscribe(handler) {
emitter.on("event", handler);
return () => emitter.off("event", handler);
},
};
}
function buildInvoiceUpdatedEvent({
invoiceId,
status,
updatedAt = null,
audienceEmployeeIds = [],
}) {
return {
type: "invoice.updated",
invoiceId,
status,
updatedAt: updatedAt ?? new Date().toISOString(),
audienceEmployeeIds: uniqueStrings(audienceEmployeeIds),
};
}
function publishInvoiceUpdated(fastify, event) {
if (!fastify?.appEvents || typeof fastify.appEvents.publish !== "function") {
return null;
}
return fastify.appEvents.publish(buildInvoiceUpdatedEvent(event));
}
function canReceiveEvent(employee, event, options = {}) {
if (!employee) {
return false;
}
if (event?.type === "invoice.updated" && options.canManageInvoice) {
return true;
}
const audience = uniqueStrings(event?.audienceEmployeeIds);
if (!audience.length) {
return false;
}
return audience.includes(employee.id);
}
function toClientEvent(event) {
if (!event) {
return null;
}
if (event.type === "invoice.updated") {
return {
id: event.id,
type: event.type,
invoiceId: event.invoiceId,
status: event.status ?? null,
updatedAt: event.updatedAt ?? event.createdAt ?? null,
createdAt: event.createdAt ?? null,
};
}
return {
id: event.id,
type: event.type,
createdAt: event.createdAt ?? null,
};
}
module.exports = {
buildInvoiceUpdatedEvent,
canReceiveEvent,
createAppEventBus,
publishInvoiceUpdated,
toClientEvent,
};
+95
查看文件
@@ -8,6 +8,14 @@ const INVOICE_UNAVAILABLE_REASONS = {
usedByCompany: "used_by_company",
};
const OCR_FIELD_LABELS = {
invoiceCode: "发票代码",
invoiceNumber: "发票号码",
invoiceDate: "开票日期",
totalAmount: "总金额",
sellerName: "销售方",
};
function buildInvoiceDedupFilters({ fileSha256 = null, invoiceCode = null, invoiceNumber = null }) {
const filters = [];
if (fileSha256) {
@@ -182,13 +190,100 @@ function mapInvoiceUnavailableReason(reason) {
}
}
function parseMissingInvoiceOcrFields(rawError) {
if (typeof rawError !== "string") {
return [];
}
const matched = rawError.match(/^missing_required_fields:(.+)$/);
if (!matched?.[1]) {
return [];
}
return matched[1]
.split(",")
.map((field) => field.trim())
.filter(Boolean);
}
function formatInvoiceOcrFailureMessage(rawError) {
const missingFields = parseMissingInvoiceOcrFields(rawError);
if (missingFields.length) {
const labels = missingFields.map((field) => OCR_FIELD_LABELS[field] ?? field);
return `票据识别缺少关键字段:${labels.join("、")}。`;
}
if (rawError === "mock_ocr_failed") {
return "票据识别失败,请重试或人工补齐关键信息。";
}
if (!rawError) {
return "票据识别失败,请稍后重试。";
}
return "票据识别失败,请重新识别或手动处理。";
}
function resolveInvoiceAvailability(invoice) {
if (!invoice) {
return {
reason: "not_found",
message: "未找到发票",
};
}
if (invoice.status === "pending_ocr") {
return {
reason: "pending_ocr",
message: "发票正在识别,完成后会自动刷新",
};
}
if (invoice.status === "ocr_failed") {
const rawError = String(invoice.ocr_last_error ?? "");
if (rawError === "duplicate_invoice_code_number" || rawError.includes("duplicate_invoice")) {
return {
reason: "duplicate_invoice",
message: "已识别出相同发票,不能重复添加",
};
}
return {
reason: "ocr_failed",
message: formatInvoiceOcrFailureMessage(rawError),
};
}
if (isUsedInvoice(invoice)) {
return {
reason: INVOICE_UNAVAILABLE_REASONS.usedByCompany,
message: mapInvoiceUnavailableReason(INVOICE_UNAVAILABLE_REASONS.usedByCompany),
};
}
if (invoice.status === "archived") {
return {
reason: "archived",
message: "发票已归档,不可添加",
};
}
return {
reason: "available",
message: "发票可添加",
};
}
module.exports = {
INVOICE_UNAVAILABLE_REASONS,
buildInvoiceDedupFilters,
classifyInvoiceConflict,
findInvoiceDedupConflicts,
formatInvoiceOcrFailureMessage,
isUsedInvoice,
mapInvoiceUnavailableReason,
parseMissingInvoiceOcrFields,
resolveInvoiceAvailability,
resolveInvoiceDedupBlocker,
resolveUploadDedupDecision,
};