|
|
|
@@ -3,20 +3,16 @@ import ExcelJS from 'exceljs';
|
|
|
|
|
import * as fs from 'fs';
|
|
|
|
|
import * as path from 'path';
|
|
|
|
|
import PDFDocument from 'pdfkit';
|
|
|
|
|
import {
|
|
|
|
|
pickExportColumns,
|
|
|
|
|
type ExportColumnDef,
|
|
|
|
|
} from '../../common/export/column-export.util';
|
|
|
|
|
|
|
|
|
|
const EXPORT_HEADERS = [
|
|
|
|
|
'订单号',
|
|
|
|
|
'下单时间',
|
|
|
|
|
'状态',
|
|
|
|
|
'商品',
|
|
|
|
|
'规格',
|
|
|
|
|
'数量',
|
|
|
|
|
'实付',
|
|
|
|
|
'好客权益',
|
|
|
|
|
'收货人',
|
|
|
|
|
'手机',
|
|
|
|
|
'收货地址',
|
|
|
|
|
] as const;
|
|
|
|
|
const DELIVERY_TYPE_LABELS: Record<string, string> = {
|
|
|
|
|
LOCAL: '同城',
|
|
|
|
|
CROSS_CITY: '跨城',
|
|
|
|
|
ON_SITE_PICKUP: '现场提货',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export type OrderExportRow = {
|
|
|
|
|
orderNo: string;
|
|
|
|
@@ -25,7 +21,9 @@ export type OrderExportRow = {
|
|
|
|
|
productName: string;
|
|
|
|
|
productSpec: string;
|
|
|
|
|
quantity: number;
|
|
|
|
|
deliveryType: string;
|
|
|
|
|
payAmount: number;
|
|
|
|
|
logisticsFee: string;
|
|
|
|
|
benefitBrief: string;
|
|
|
|
|
receiverName: string;
|
|
|
|
|
receiverPhone: string;
|
|
|
|
@@ -95,7 +93,9 @@ export function mapOrderToExportRow(order: {
|
|
|
|
|
productName: order.productName,
|
|
|
|
|
productSpec: order.productSpec,
|
|
|
|
|
quantity: order.quantity,
|
|
|
|
|
deliveryType: DELIVERY_TYPE_LABELS[order.deliveryType ?? ''] || order.deliveryType || '',
|
|
|
|
|
payAmount: Number(order.payAmount),
|
|
|
|
|
logisticsFee: '',
|
|
|
|
|
benefitBrief: formatBenefitBrief(order),
|
|
|
|
|
receiverName: order.receiverName,
|
|
|
|
|
receiverPhone: order.receiverPhone,
|
|
|
|
@@ -103,22 +103,49 @@ export function mapOrderToExportRow(order: {
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function rowToCells(row: OrderExportRow): string[] {
|
|
|
|
|
function orderExportColumnDefs(): ExportColumnDef<OrderExportRow>[] {
|
|
|
|
|
return [
|
|
|
|
|
row.orderNo,
|
|
|
|
|
row.createdAt,
|
|
|
|
|
row.status,
|
|
|
|
|
row.productName,
|
|
|
|
|
row.productSpec,
|
|
|
|
|
String(row.quantity),
|
|
|
|
|
row.payAmount.toFixed(2),
|
|
|
|
|
row.benefitBrief,
|
|
|
|
|
row.receiverName,
|
|
|
|
|
row.receiverPhone,
|
|
|
|
|
row.address,
|
|
|
|
|
{ key: '订单号', header: '订单号', value: (r) => r.orderNo },
|
|
|
|
|
{ key: '下单时间', header: '下单时间', value: (r) => r.createdAt },
|
|
|
|
|
{ key: '状态', header: '状态', value: (r) => r.status },
|
|
|
|
|
{ key: '商品', header: '商品', value: (r) => r.productName },
|
|
|
|
|
{ key: '规格', header: '规格', value: (r) => r.productSpec },
|
|
|
|
|
{ key: '数量', header: '数量', value: (r) => r.quantity },
|
|
|
|
|
{ key: '配送方式', header: '配送方式', value: (r) => r.deliveryType },
|
|
|
|
|
{ key: '实付', header: '实付', value: (r) => r.payAmount.toFixed(2) },
|
|
|
|
|
{ key: '运费', header: '运费', value: (r) => r.logisticsFee },
|
|
|
|
|
{ key: '好客权益', header: '好客权益', value: (r) => r.benefitBrief },
|
|
|
|
|
{ key: '收货人', header: '收货人', value: (r) => r.receiverName },
|
|
|
|
|
{ key: '电话', header: '电话', value: (r) => r.receiverPhone },
|
|
|
|
|
{ key: '地址', header: '地址', value: (r) => r.address },
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ORDER_COLUMN_ALIASES: Record<string, string> = {
|
|
|
|
|
orderNo: '订单号',
|
|
|
|
|
createdAt: '下单时间',
|
|
|
|
|
status: '状态',
|
|
|
|
|
productName: '商品',
|
|
|
|
|
productSpec: '规格',
|
|
|
|
|
quantity: '数量',
|
|
|
|
|
deliveryType: '配送方式',
|
|
|
|
|
payAmount: '实付',
|
|
|
|
|
logisticsFee: '运费',
|
|
|
|
|
benefitBrief: '好客权益',
|
|
|
|
|
receiverName: '收货人',
|
|
|
|
|
receiverPhone: '电话',
|
|
|
|
|
phone: '电话',
|
|
|
|
|
receiverAddress: '地址',
|
|
|
|
|
address: '地址',
|
|
|
|
|
手机: '电话',
|
|
|
|
|
收货地址: '地址',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function normalizeOrderColumns(columnKeys?: string[]): string[] | undefined {
|
|
|
|
|
if (!columnKeys?.length) return undefined;
|
|
|
|
|
return columnKeys.map((k) => ORDER_COLUMN_ALIASES[k] ?? k);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolvePdfFontPath(): string {
|
|
|
|
|
const candidates = [
|
|
|
|
|
process.env.EXPORT_PDF_FONT_PATH,
|
|
|
|
@@ -138,79 +165,42 @@ function resolvePdfFontPath(): string {
|
|
|
|
|
throw new Error('未找到可用于 PDF 的中文字体,请将字体文件放到 server/dukang-api/assets/fonts/');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function buildOrdersXlsx(rows: OrderExportRow[]): Promise<Buffer> {
|
|
|
|
|
export async function buildOrdersXlsx(rows: OrderExportRow[], columnKeys?: string[]): Promise<Buffer> {
|
|
|
|
|
const cols = pickExportColumns(orderExportColumnDefs(), normalizeOrderColumns(columnKeys));
|
|
|
|
|
const workbook = new ExcelJS.Workbook();
|
|
|
|
|
const sheet = workbook.addWorksheet('订单');
|
|
|
|
|
sheet.addRow([...EXPORT_HEADERS]);
|
|
|
|
|
sheet.addRow(cols.map((c) => c.header));
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
sheet.addRow(rowToCells(row));
|
|
|
|
|
sheet.addRow(cols.map((c) => c.value(row)));
|
|
|
|
|
}
|
|
|
|
|
sheet.columns.forEach((col) => {
|
|
|
|
|
col.width = 16;
|
|
|
|
|
});
|
|
|
|
|
sheet.getColumn(4).width = 22;
|
|
|
|
|
sheet.getColumn(8).width = 36;
|
|
|
|
|
sheet.getColumn(11).width = 36;
|
|
|
|
|
const buffer = await workbook.xlsx.writeBuffer();
|
|
|
|
|
return Buffer.from(buffer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function buildOrdersPdf(rows: OrderExportRow[]): Promise<Buffer> {
|
|
|
|
|
export async function buildOrdersPdf(rows: OrderExportRow[], columnKeys?: string[]): Promise<Buffer> {
|
|
|
|
|
const cols = pickExportColumns(orderExportColumnDefs(), normalizeOrderColumns(columnKeys));
|
|
|
|
|
const fontPath = resolvePdfFontPath();
|
|
|
|
|
const chunks: Buffer[] = [];
|
|
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
|
const doc = new PDFDocument({
|
|
|
|
|
size: 'A4',
|
|
|
|
|
layout: 'landscape',
|
|
|
|
|
margin: 24,
|
|
|
|
|
bufferPages: true,
|
|
|
|
|
});
|
|
|
|
|
doc.on('data', (chunk) => chunks.push(chunk as Buffer));
|
|
|
|
|
const doc = new PDFDocument({ size: 'A4', layout: 'landscape', margin: 24, bufferPages: true });
|
|
|
|
|
doc.on('data', (c) => chunks.push(c as Buffer));
|
|
|
|
|
doc.on('end', () => resolve(Buffer.concat(chunks)));
|
|
|
|
|
doc.on('error', reject);
|
|
|
|
|
|
|
|
|
|
doc.registerFont('zh', fontPath);
|
|
|
|
|
doc.font('zh');
|
|
|
|
|
|
|
|
|
|
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
|
|
|
|
const colWidths = [72, 78, 48, 88, 64, 32, 48, 110, 48, 72, 120];
|
|
|
|
|
const scale = pageWidth / colWidths.reduce((sum, w) => sum + w, 0);
|
|
|
|
|
const widths = colWidths.map((w) => w * scale);
|
|
|
|
|
const rowHeight = 28;
|
|
|
|
|
const fontSize = 7;
|
|
|
|
|
let y = doc.page.margins.top;
|
|
|
|
|
|
|
|
|
|
const drawRow = (cells: string[], isHeader = false) => {
|
|
|
|
|
let x = doc.page.margins.left;
|
|
|
|
|
const height = isHeader ? 24 : rowHeight;
|
|
|
|
|
if (y + height > doc.page.height - doc.page.margins.bottom) {
|
|
|
|
|
doc.addPage({ size: 'A4', layout: 'landscape', margin: 24 });
|
|
|
|
|
y = doc.page.margins.top;
|
|
|
|
|
}
|
|
|
|
|
doc.fontSize(isHeader ? 8 : fontSize);
|
|
|
|
|
cells.forEach((cell, index) => {
|
|
|
|
|
doc.rect(x, y, widths[index], height).stroke('#dddddd');
|
|
|
|
|
doc.text(cell || '', x + 2, y + 4, {
|
|
|
|
|
width: widths[index] - 4,
|
|
|
|
|
height: height - 6,
|
|
|
|
|
lineBreak: true,
|
|
|
|
|
});
|
|
|
|
|
x += widths[index];
|
|
|
|
|
});
|
|
|
|
|
y += height;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
drawRow([...EXPORT_HEADERS], true);
|
|
|
|
|
doc.font(fontPath);
|
|
|
|
|
doc.fontSize(10).text(cols.map((c) => c.header).join(' | '));
|
|
|
|
|
doc.moveDown(0.5);
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
drawRow(rowToCells(row));
|
|
|
|
|
doc.text(cols.map((c) => String(c.value(row))).join(' | '));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
doc.end();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildExportFilename(format: 'xlsx' | 'pdf', count: number): string {
|
|
|
|
|
const stamp = formatShanghaiDateTime(new Date()).slice(0, 10);
|
|
|
|
|
return `订单导出_${stamp}_${count}条.${format}`;
|
|
|
|
|
const stamp = new Date().toISOString().slice(0, 10);
|
|
|
|
|
return `订单导出_${stamp}_${count}.${format}`;
|
|
|
|
|
}
|
|
|
|
|