- 创建订单日报系统,支持每天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
254 lines
7.8 KiB
TypeScript
254 lines
7.8 KiB
TypeScript
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;
|
||
}
|
||
} |