Files
ylt_diy/src/lib/report-generator.ts
user9994793890 ba91796dc1 feat: 实现订单日报系统
- 创建订单日报系统,支持每天8:01自动生成前一天(早8点到当天早8点)的日报
- 新建两个数据库表:t_daily_report_config(配置表)和 t_daily_report_history(历史表)
- 原表 t_equipment_charge_order 只读取不修改,严格遵守用户要求
- 支持按企业ID或用户ID拆分日报,用户ID会提示是企业用户还是普通用户
- 用户可自定义选择报表字段,所有字段使用中文显示
- 实现配置管理、日报生成、历史查看的完整功能
- 代码检查全部通过,静态检查无错误

Coze-Commit-Type: user
Coze-User-ID: 3722323274763196
Coze-Conversation-ID: 9894087
2026-07-07 16:14:42 +08:00

254 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import pool from '@/lib/db';
import { FIELD_MAPPING, STATE_MAPPING, ORDER_TYPE_MAPPING, PAY_METHOD_MAPPING, PAY_STATUS_MAPPING } from '@/lib/field-mapping';
import * as XLSX from 'xlsx';
import { writeFileSync, mkdirSync } from 'fs';
import path from 'path';
// 日报生成配置
interface ReportConfig {
config_id: number;
split_type: string; // 'company_id' 或 'user_id'
selected_fields: string[]; // 用户选择的字段
}
// 日报生成结果
interface ReportResult {
success: boolean;
reportDate: string;
startTime: string;
endTime: string;
splitType: string;
splitValue: string;
totalOrders: number;
totalAmount: number;
filePath?: string;
error?: string;
}
// 生成日报
export async function generateDailyReport(
splitType: string,
splitValue: string,
selectedFields: string[],
reportDate?: string // 可选的报表日期,默认为昨天
): Promise<ReportResult> {
try {
// 计算时间范围:前一天早8点到当天早8点
const targetDate = reportDate ? new Date(reportDate) : new Date();
targetDate.setDate(targetDate.getDate() - 1); // 昨天
const startTime = new Date(targetDate);
startTime.setHours(8, 0, 0, 0);
const endTime = new Date(targetDate);
endTime.setDate(endTime.getDate() + 1);
endTime.setHours(8, 0, 0, 0);
// 查询订单数据
const queryField = splitType === 'company_id' ? 'company_id' : 'user_id';
const fieldList = selectedFields.join(', ');
const [orders] = await pool.execute<any[]>(
`SELECT ${fieldList} FROM t_equipment_charge_order
WHERE state = 3
AND report_time >= ?
AND report_time < ?
AND ${queryField} = ?
ORDER BY report_time ASC`,
[startTime, endTime, splitValue]
);
if (!Array.isArray(orders) || orders.length === 0) {
return {
success: false,
reportDate: targetDate.toISOString().split('T')[0],
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
splitType,
splitValue,
totalOrders: 0,
totalAmount: 0,
error: '该时间段内没有订单数据'
};
}
// 处理数据:将字段名转换为中文,并转换特殊字段的值
const processedOrders = orders.map(order => {
const processed: Record<string, any> = {};
for (const [key, value] of Object.entries(order)) {
// 使用中文字段名
const chineseName = FIELD_MAPPING[key] || key;
// 特殊字段值转换
if (key === 'state' && typeof value === 'number') {
processed[chineseName] = STATE_MAPPING[value] || value;
} else if (key === 'order_type' && typeof value === 'number') {
processed[chineseName] = ORDER_TYPE_MAPPING[value] || value;
} else if (key === 'pay_method' && typeof value === 'number') {
processed[chineseName] = PAY_METHOD_MAPPING[value] || value;
} else if (key === 'pay_status' && typeof value === 'number') {
processed[chineseName] = PAY_STATUS_MAPPING[value] || value;
} else {
processed[chineseName] = value;
}
}
return processed;
});
// 生成Excel文件
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.json_to_sheet(processedOrders);
// 设置列宽
const columnWidths = selectedFields.map(field => ({
wch: Math.max(FIELD_MAPPING[field]?.length || 10, 15)
}));
worksheet['!cols'] = columnWidths;
XLSX.utils.book_append_sheet(workbook, worksheet, '日报');
// 生成文件名
const fileName = `日报_${splitType === 'company_id' ? '企业' : '用户'}_${splitValue}_${targetDate.toISOString().split('T')[0]}.xlsx`;
const reportsDir = path.join(process.cwd(), 'public', 'reports');
// 确保目录存在
mkdirSync(reportsDir, { recursive: true });
const filePath = path.join(reportsDir, fileName);
// 写入文件
writeFileSync(filePath, XLSX.write(workbook, { type: 'buffer', bookType: 'xlsx' }));
// 计算统计信息
const totalOrders = orders.length;
const totalAmount = orders.reduce((sum: number, order: any) => {
return sum + (parseFloat(order.actual_pay_amount) || 0);
}, 0);
return {
success: true,
reportDate: targetDate.toISOString().split('T')[0],
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
splitType,
splitValue,
totalOrders,
totalAmount,
filePath: `/reports/${fileName}`
};
} catch (error) {
return {
success: false,
reportDate: '',
startTime: '',
endTime: '',
splitType,
splitValue,
totalOrders: 0,
totalAmount: 0,
error: error instanceof Error ? error.message : '未知错误'
};
}
}
// 批量生成日报(用于定时任务)
export async function generateBatchReports(configId: number): Promise<ReportResult[]> {
try {
// 获取配置
const [configs] = await pool.execute<any[]>(
'SELECT * FROM t_daily_report_config WHERE id = ? AND is_active = 1',
[configId]
);
if (!Array.isArray(configs) || configs.length === 0) {
throw new Error('配置不存在或已禁用');
}
const config = configs[0];
const splitType = config.split_type;
const selectedFields = JSON.parse(config.selected_fields);
// 获取所有需要生成日报的企业/用户列表
const queryField = splitType === 'company_id' ? 'company_id' : 'user_id';
// 计算时间范围
const targetDate = new Date();
targetDate.setDate(targetDate.getDate() - 1);
const startTime = new Date(targetDate);
startTime.setHours(8, 0, 0, 0);
const endTime = new Date(targetDate);
endTime.setDate(endTime.getDate() + 1);
endTime.setHours(8, 0, 0, 0);
// 获取该时间段内的所有企业/用户
const [entities] = await pool.execute<any[]>(
`SELECT DISTINCT ${queryField} as entity_id
FROM t_equipment_charge_order
WHERE state = 3
AND report_time >= ?
AND report_time < ?
AND ${queryField} IS NOT NULL`,
[startTime, endTime]
);
if (!Array.isArray(entities) || entities.length === 0) {
return [];
}
// 批量生成日报
const results: ReportResult[] = [];
for (const entity of entities) {
const entityId = entity.entity_id;
const result = await generateDailyReport(splitType, String(entityId), selectedFields);
// 保存到历史记录表
if (result.success) {
await pool.execute(
`INSERT INTO t_daily_report_history
(report_date, start_time, end_time, split_type, split_value, config_id, total_orders, total_amount, file_path, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
result.reportDate,
result.startTime,
result.endTime,
result.splitType,
result.splitValue,
configId,
result.totalOrders,
result.totalAmount,
result.filePath || null,
1
]
);
} else {
await pool.execute(
`INSERT INTO t_daily_report_history
(report_date, start_time, end_time, split_type, split_value, config_id, total_orders, total_amount, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
result.reportDate,
result.startTime,
result.endTime,
result.splitType,
result.splitValue,
configId,
result.totalOrders,
result.totalAmount,
0
]
);
}
results.push(result);
}
return results;
} catch (error) {
throw error;
}
}