fix(number-fields): избегаем потери точности длинных чисел
- number-поля теперь рендерятся как text + inputMode=numeric, чтобы браузер не округлял значения через input type=number - пробелы при вставке в number-поля удаляются - бэкенд нормализует значения number-полей в строку перед сохранением - добавлен хелпер normalizeFieldValueForStorage Closes: искажение расчётного счёта и других длинных числовых полей
This commit is contained in:
128
server/documents/external-pdf.service.ts
Normal file
128
server/documents/external-pdf.service.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
export interface PDFOptions {
|
||||
format?: string;
|
||||
landscape?: boolean;
|
||||
margin?: { top: number; right: number; bottom: number; left: number };
|
||||
printBackground?: boolean;
|
||||
headerTemplate?: string;
|
||||
footerTemplate?: string;
|
||||
}
|
||||
|
||||
const DOC_WORKER_URL = (process.env.DOC_WORKER_URL || "http://document-worker:3000").replace(/\/+$/, "");
|
||||
|
||||
function getTimeout(): number {
|
||||
const env = process.env.DOC_WORKER_TIMEOUT_MS;
|
||||
return env ? parseInt(env, 10) : 30000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует HTML в PDF через внешний document-worker сервис.
|
||||
* Возвращает Buffer или null при ошибке.
|
||||
*/
|
||||
export async function convertHtmlToPdf(html: string, options: PDFOptions = {}): Promise<Buffer | null> {
|
||||
try {
|
||||
const response = await fetch(`${DOC_WORKER_URL}/convert/pdf`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ html, options }),
|
||||
signal: AbortSignal.timeout(getTimeout()),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
console.error("[doc-worker] PDF conversion HTTP error:", response.status, text);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.pdf) {
|
||||
console.error("[doc-worker] PDF conversion failed:", data.error || "unknown");
|
||||
return null;
|
||||
}
|
||||
|
||||
return Buffer.from(data.pdf, "base64");
|
||||
} catch (err) {
|
||||
console.error("[doc-worker] PDF conversion request failed:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует DOCX (Buffer) в PDF через внешний document-worker сервис.
|
||||
* Возвращает Buffer или null при ошибке.
|
||||
*/
|
||||
export async function convertDocxToPdf(docxBuffer: Buffer): Promise<Buffer | null> {
|
||||
try {
|
||||
const response = await fetch(`${DOC_WORKER_URL}/convert/docx-to-pdf`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ docxBase64: docxBuffer.toString("base64") }),
|
||||
signal: AbortSignal.timeout(getTimeout()),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
console.error("[doc-worker] DOCX→PDF conversion HTTP error:", response.status, text);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.pdf) {
|
||||
console.error("[doc-worker] DOCX→PDF conversion failed:", data.error || "unknown");
|
||||
return null;
|
||||
}
|
||||
|
||||
return Buffer.from(data.pdf, "base64");
|
||||
} catch (err) {
|
||||
console.error("[doc-worker] DOCX→PDF conversion request failed:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Конвертирует EMF/WMF (Buffer) в PNG через внешний document-worker сервис.
|
||||
* Возвращает Buffer или null при ошибке.
|
||||
*/
|
||||
export async function convertEmfToPng(emfBuffer: Buffer): Promise<Buffer | null> {
|
||||
try {
|
||||
const response = await fetch(`${DOC_WORKER_URL}/convert/emf-to-png`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ emfBase64: emfBuffer.toString("base64") }),
|
||||
signal: AbortSignal.timeout(getTimeout()),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => "");
|
||||
console.error("[doc-worker] EMF→PNG conversion HTTP error:", response.status, text);
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (!data.success || !data.png) {
|
||||
console.error("[doc-worker] EMF→PNG conversion failed:", data.error || "unknown");
|
||||
return null;
|
||||
}
|
||||
|
||||
return Buffer.from(data.png, "base64");
|
||||
} catch (err) {
|
||||
console.error("[doc-worker] EMF→PNG conversion request failed:", err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет доступность document-worker сервиса.
|
||||
*/
|
||||
export async function checkDocumentWorkerHealth(): Promise<{ ok: boolean; error?: string }> {
|
||||
try {
|
||||
const response = await fetch(`${DOC_WORKER_URL}/health`, {
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
if (!response.ok) {
|
||||
return { ok: false, error: `HTTP ${response.status}` };
|
||||
}
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user