feat: 完善日报系统功能

主要更新:
1. 【核心变更】从批量拆分改为单个企业/用户选择生成
   - 用户现在可以选择特定的企业或用户生成日报
   - 企业列表显示企业名称(如"国营-西南-79路")
   - 用户列表显示用户姓名,并标注用户类型(企业用户/普通用户)

2. 【新增功能】求和功能
   - 支持选择多个字段进行求和
   - 求和结果显示在Excel表格底部
   - 可对金额、电量等多列分别求和

3. 【数据库表结构更新】
   - t_daily_report_config 新增字段:split_name(企业/用户名称)、sum_fields(求和字段列表)
   - t_daily_report_history 新增字段:split_name、sum_results(求和结果JSON)

4. 【前端页面优化】
   - 配置管理:选择企业/用户时显示名称和订单数量
   - 日报生成:支持选择单个企业/用户、字段和求和字段
   - 历史记录:显示企业/用户名称和求和结果

5. 【API接口优化】
   - 企业列表API:返回company_name、company_no、company_group等信息
   - 用户列表API:返回user_name和用户类型标签
   - 日报生成API:支持sum_fields参数

6. 【删除临时文件】清理debug调试API接口

所有代码静态检查通过,企业列表和用户列表API测试成功

Coze-Commit-Type: user
Coze-User-ID: 3722323274763196
Coze-Conversation-ID: 9894087
This commit is contained in:
user9994793890
2026-07-07 16:52:51 +08:00
parent 13a7188af1
commit 05b031bb40
7 changed files with 361 additions and 144 deletions

View File

@@ -11,10 +11,12 @@ export async function GET() {
// 尝试查询配置(如果表不存在会失败)
let configs: any[] = [];
try {
const [configRows] = await pool.execute<any[]>(
const connection = await pool.getConnection();
const [configRows] = await connection.query<any[]>(
'SELECT * FROM t_daily_report_config WHERE is_active = 1 ORDER BY create_time DESC'
);
configs = configRows || [];
connection.release();
} catch (dbError) {
// 如果表不存在,返回空数组
console.log('配置表查询失败,可能是表未初始化:', dbError);
@@ -39,13 +41,20 @@ export async function GET() {
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { config_name, split_type, selected_fields } = body;
const {
config_name,
split_type,
split_value,
split_name,
selected_fields,
sum_fields
} = body;
// 验证必填字段
if (!config_name || !split_type || !selected_fields || selected_fields.length === 0) {
if (!config_name || !split_type || !split_value || !selected_fields || selected_fields.length === 0) {
return NextResponse.json({
success: false,
message: '缺少必填字段'
message: '缺少必填字段(配置名称、拆分方式、拆分值、选择的字段)'
}, { status: 400 });
}
@@ -57,17 +66,29 @@ export async function POST(request: NextRequest) {
}, { status: 400 });
}
// 获取连接
const connection = await pool.getConnection();
// 插入配置
const [result] = await pool.execute(
`INSERT INTO t_daily_report_config (config_name, split_type, selected_fields)
VALUES (?, ?, ?)`,
[config_name, split_type, JSON.stringify(selected_fields)]
await connection.query(
`INSERT INTO t_daily_report_config
(config_name, split_type, split_value, split_name, selected_fields, sum_fields)
VALUES (?, ?, ?, ?, ?, ?)`,
[
config_name,
split_type,
split_value,
split_name || null,
JSON.stringify(selected_fields),
sum_fields ? JSON.stringify(sum_fields) : null
]
);
connection.release();
return NextResponse.json({
success: true,
message: '配置创建成功',
data: result
message: '配置创建成功'
});
} catch (error) {
return NextResponse.json({

View File

@@ -1,55 +1,68 @@
import { NextRequest, NextResponse } from 'next/server';
import pool from '@/lib/db';
// GET - 获取企业列表或用户列表
// GET - 获取企业列表或用户列表(带名称)
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const type = searchParams.get('type'); // 'company' 或 'user'
// 获取连接
const connection = await pool.getConnection();
if (type === 'company') {
// 获取企业列表(根据company_id分组,并统计订单数量)
const [companies] = await pool.execute(`
// 获取企业列表(从 t_company 表读取企业名称)
const [companies] = await connection.query<any[]>(`
SELECT
company_id,
COUNT(*) as order_count,
SUM(actual_pay_amount) as total_amount
FROM t_equipment_charge_order
WHERE state = 3 AND company_id IS NOT NULL
GROUP BY company_id
c.id as company_id,
c.company_name,
c.company_no,
c.company_group,
COUNT(o.id) as order_count,
COALESCE(SUM(o.actual_pay_amount), 0) as total_amount
FROM t_company c
LEFT JOIN t_equipment_charge_order o ON c.id = o.company_id AND o.state = 3
GROUP BY c.id, c.company_name, c.company_no, c.company_group
ORDER BY order_count DESC
`);
connection.release();
return NextResponse.json({
success: true,
data: companies,
type: '企业'
});
} else if (type === 'user') {
// 获取用户列表(根据user_id分组,区分企业用户和普通用户)
const [users] = await pool.execute(`
// 获取用户列表(从 t_user 表读取用户姓名)
const [users] = await connection.query<any[]>(`
SELECT
user_id,
order_type,
COUNT(*) as order_count,
SUM(actual_pay_amount) as total_amount,
u.id as user_id,
u.user_name,
u.phone,
o.order_type,
COUNT(o.id) as order_count,
COALESCE(SUM(o.actual_pay_amount), 0) as total_amount,
CASE
WHEN order_type = 3 THEN '企业用户'
WHEN order_type = 1 THEN '普通用户'
WHEN o.order_type = 3 THEN '企业用户'
WHEN o.order_type = 1 THEN '普通用户'
ELSE '其他类型'
END as user_type_label
FROM t_equipment_charge_order
WHERE state = 3
GROUP BY user_id, order_type
FROM t_user u
LEFT JOIN t_equipment_charge_order o ON u.id = o.user_id AND o.state = 3
GROUP BY u.id, u.user_name, u.phone, o.order_type
ORDER BY order_count DESC
`);
connection.release();
return NextResponse.json({
success: true,
data: users,
type: '用户'
});
} else {
connection.release();
return NextResponse.json({
success: false,
message: '类型参数必须为 company 或 user'

View File

@@ -5,14 +5,22 @@ import { generateDailyReport, generateBatchReports } from '@/lib/report-generato
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { split_type, split_value, selected_fields, report_date, config_id } = body;
const {
split_type,
split_value,
split_name,
selected_fields,
sum_fields,
report_date,
config_id
} = body;
// 如果提供了config_id,则批量生成所有企业/用户的日报
// 如果提供了config_id,则根据配置生成日报
if (config_id) {
const results = await generateBatchReports(config_id);
return NextResponse.json({
success: true,
message: '批量日报生成完成',
message: '日报生成完成',
data: results
});
}
@@ -21,11 +29,18 @@ export async function POST(request: NextRequest) {
if (!split_type || !split_value || !selected_fields || selected_fields.length === 0) {
return NextResponse.json({
success: false,
message: '缺少必填字段'
message: '缺少必填字段(拆分方式、拆分值、选择的字段)'
}, { status: 400 });
}
const result = await generateDailyReport(split_type, split_value, selected_fields, report_date);
const result = await generateDailyReport(
split_type,
split_value,
split_name,
selected_fields,
sum_fields,
report_date
);
return NextResponse.json({
success: result.success,

View File

@@ -24,12 +24,17 @@ export default function Home() {
// 配置表单
const [configName, setConfigName] = useState('');
const [splitType, setSplitType] = useState('company_id');
const [splitValue, setSplitValue] = useState('');
const [splitName, setSplitName] = useState('');
const [selectedFields, setSelectedFields] = useState<string[]>([]);
const [sumFields, setSumFields] = useState<string[]>([]);
// 日报生成表单
const [reportSplitType, setReportSplitType] = useState('company_id');
const [reportSplitValue, setReportSplitValue] = useState('');
const [reportSplitName, setReportSplitName] = useState('');
const [reportSelectedFields, setReportSelectedFields] = useState<string[]>([]);
const [reportSumFields, setReportSumFields] = useState<string[]>([]);
const [reportDate, setReportDate] = useState('');
// 历史查询
@@ -46,6 +51,8 @@ export default function Home() {
} else {
loadUsers();
}
setSplitValue('');
setSplitName('');
}, [splitType]);
useEffect(() => {
@@ -54,6 +61,8 @@ export default function Home() {
} else {
loadUsers();
}
setReportSplitValue('');
setReportSplitName('');
}, [reportSplitType]);
const loadData = async () => {
@@ -112,10 +121,10 @@ export default function Home() {
};
const handleCreateConfig = async () => {
if (!configName || selectedFields.length === 0) {
if (!configName || !splitValue || selectedFields.length === 0) {
toast({
title: '错误',
description: '请填写配置名称并选择至少一个字段',
description: '请填写配置名称并选择企业和字段',
variant: 'destructive'
});
return;
@@ -129,7 +138,10 @@ export default function Home() {
body: JSON.stringify({
config_name: configName,
split_type: splitType,
selected_fields: selectedFields
split_value: splitValue,
split_name: splitName,
selected_fields: selectedFields,
sum_fields: sumFields
})
});
@@ -141,7 +153,10 @@ export default function Home() {
});
loadData();
setConfigName('');
setSplitValue('');
setSplitName('');
setSelectedFields([]);
setSumFields([]);
} else {
toast({
title: '错误',
@@ -177,7 +192,9 @@ export default function Home() {
body: JSON.stringify({
split_type: reportSplitType,
split_value: reportSplitValue,
split_name: reportSplitName,
selected_fields: reportSelectedFields,
sum_fields: reportSumFields,
report_date: reportDate
})
});
@@ -237,13 +254,19 @@ export default function Home() {
setLoading(false);
};
const handleFieldChange = (field: string, checked: boolean, type: 'config' | 'report') => {
const handleFieldChange = (field: string, checked: boolean, type: 'config' | 'report' | 'sum') => {
if (type === 'config') {
if (checked) {
setSelectedFields([...selectedFields, field]);
} else {
setSelectedFields(selectedFields.filter(f => f !== field));
}
} else if (type === 'sum') {
if (checked) {
setSumFields([...sumFields, field]);
} else {
setSumFields(sumFields.filter(f => f !== field));
}
} else {
if (checked) {
setReportSelectedFields([...reportSelectedFields, field]);
@@ -253,6 +276,24 @@ export default function Home() {
}
};
const handleReportSumFieldChange = (field: string, checked: boolean) => {
if (checked) {
setReportSumFields([...reportSumFields, field]);
} else {
setReportSumFields(reportSumFields.filter(f => f !== field));
}
};
const handleSplitValueChange = (value: string, name: string) => {
setSplitValue(value);
setSplitName(name);
};
const handleReportSplitValueChange = (value: string, name: string) => {
setReportSplitValue(value);
setReportSplitName(name);
};
return (
<div className="min-h-screen bg-gray-50 p-8">
<div className="max-w-7xl mx-auto">
@@ -260,7 +301,7 @@ export default function Home() {
<CardHeader>
<CardTitle className="text-2xl">订单日报系统</CardTitle>
<CardDescription>
每天早8点自动生成前一天的订单日报(早8点到当天早8点)
为选定的企业或用户生成订单日报(前一天早8点到当天早8点)
</CardDescription>
</CardHeader>
</Card>
@@ -287,26 +328,58 @@ export default function Home() {
id="configName"
value={configName}
onChange={(e) => setConfigName(e.target.value)}
placeholder="例如:企业日报配置"
placeholder="例如:国营-西南-79路日报"
/>
</div>
<div>
<Label htmlFor="splitType">拆分方式</Label>
<Select value={splitType} onValueChange={setSplitType}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="company_id">按企业ID拆分</SelectItem>
<SelectItem value="user_id">按用户ID拆分</SelectItem>
</SelectContent>
</Select>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="splitType">拆分方式</Label>
<Select value={splitType} onValueChange={setSplitType}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="company_id">按企业ID</SelectItem>
<SelectItem value="user_id">按用户ID</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label htmlFor="splitValue">
{splitType === 'company_id' ? '选择企业' : '选择用户'}
</Label>
<Select value={splitValue} onValueChange={(value) => {
const selectedItem = splitType === 'company_id'
? companies.find(c => String(c.company_id) === value)
: users.find(u => String(u.user_id) === value);
handleSplitValueChange(value, selectedItem?.company_name || selectedItem?.user_name || '');
}}>
<SelectTrigger>
<SelectValue placeholder="请选择" />
</SelectTrigger>
<SelectContent>
{splitType === 'company_id'
? companies.map((company) => (
<SelectItem key={company.company_id} value={String(company.company_id)}>
{company.company_name} ({company.order_count} 单)
</SelectItem>
))
: users.map((user) => (
<SelectItem key={`${user.user_id}-${user.order_type}`} value={String(user.user_id)}>
{user.user_name || `用户${user.user_id}`} ({user.user_type_label}, {user.order_count} 单)
</SelectItem>
))
}
</SelectContent>
</Select>
</div>
</div>
<div>
<Label>选择字段(已选 {selectedFields.length} 个)</Label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 mt-2 h-[300px] overflow-y-auto border rounded p-2">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 mt-2 h-[200px] overflow-y-auto border rounded p-2">
{availableFields.map((field) => (
<div key={field.field} className="flex items-center space-x-2">
<Checkbox
@@ -324,6 +397,30 @@ export default function Home() {
</div>
</div>
<div>
<Label>求和字段(已选 {sumFields.length} 个)- 将在表格底部显示求和结果</Label>
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 mt-2 h-[150px] overflow-y-auto border rounded p-2">
{availableFields.filter(f =>
['charge_degree', 'charge_ah', 'receivable_electric_fee', 'receivable_service_fee',
'receivable_total_fee', 'charge_elecfee_amount', 'charge_service_amount',
'actual_pay_amount', 'pay_amount', 'total_amount'].some(numField => f.field.includes(numField))
).map((field) => (
<div key={`sum-${field.field}`} className="flex items-center space-x-2">
<Checkbox
id={`sum-config-${field.field}`}
checked={sumFields.includes(field.field)}
onCheckedChange={(checked) =>
handleFieldChange(field.field, checked as boolean, 'sum')
}
/>
<Label htmlFor={`sum-config-${field.field}`} className="text-sm">
{field.label}
</Label>
</div>
))}
</div>
</div>
<Button
onClick={handleCreateConfig}
disabled={loading}
@@ -346,7 +443,9 @@ export default function Home() {
<TableRow>
<TableHead>配置名称</TableHead>
<TableHead>拆分方式</TableHead>
<TableHead>字段数量</TableHead>
<TableHead>企业/用户</TableHead>
<TableHead>字段数</TableHead>
<TableHead>求和字段</TableHead>
<TableHead>操作</TableHead>
</TableRow>
</TableHeader>
@@ -359,9 +458,13 @@ export default function Home() {
{config.split_type === 'company_id' ? '企业' : '用户'}
</Badge>
</TableCell>
<TableCell>{config.split_name || config.split_value}</TableCell>
<TableCell>
{JSON.parse(config.selected_fields).length} 个
</TableCell>
<TableCell>
{config.sum_fields ? JSON.parse(config.sum_fields).length : 0} 个
</TableCell>
<TableCell>
<Button
variant="destructive"
@@ -386,7 +489,7 @@ export default function Home() {
<CardHeader>
<CardTitle>手动生成日报</CardTitle>
<CardDescription>
选择企业/用户和字段,手动生成指定日期的日报
选择企业/用户和字段,生成指定日期的日报
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
@@ -404,16 +507,21 @@ export default function Home() {
</Select>
{reportSplitType === 'user_id' && (
<p className="text-sm text-muted-foreground mt-1">
注意:用户ID可能对应企业用户或普通用户
注意:用户ID会显示用户类型(企业用户/普通用户)
</p>
)}
</div>
<div>
<Label htmlFor="reportSplitValue">
{reportSplitType === 'company_id' ? '企业ID' : '用户ID'}
{reportSplitType === 'company_id' ? '选择企业' : '选择用户'}
</Label>
<Select value={reportSplitValue} onValueChange={setReportSplitValue}>
<Select value={reportSplitValue} onValueChange={(value) => {
const selectedItem = reportSplitType === 'company_id'
? companies.find(c => String(c.company_id) === value)
: users.find(u => String(u.user_id) === value);
handleReportSplitValueChange(value, selectedItem?.company_name || selectedItem?.user_name || '');
}}>
<SelectTrigger>
<SelectValue placeholder="请选择" />
</SelectTrigger>
@@ -421,12 +529,12 @@ export default function Home() {
{reportSplitType === 'company_id'
? companies.map((company) => (
<SelectItem key={company.company_id} value={String(company.company_id)}>
企业ID: {company.company_id} ({company.order_count} 单)
{company.company_name} ({company.order_count} 单)
</SelectItem>
))
: users.map((user) => (
<SelectItem key={`${user.user_id}-${user.order_type}`} value={String(user.user_id)}>
用户ID: {user.user_id} ({user.user_type_label}, {user.order_count} 单)
{user.user_name || `用户${user.user_id}`} ({user.user_type_label}, {user.order_count} 单)
</SelectItem>
))
}
@@ -465,6 +573,30 @@ export default function Home() {
</div>
</div>
<div>
<Label>求和字段(已选 {reportSumFields.length} 个)- 将在表格底部显示求和结果</Label>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2 mt-2 h-[150px] overflow-y-auto border rounded p-2">
{availableFields.filter(f =>
['charge_degree', 'charge_ah', 'receivable_electric_fee', 'receivable_service_fee',
'receivable_total_fee', 'charge_elecfee_amount', 'charge_service_amount',
'actual_pay_amount', 'pay_amount', 'total_amount'].some(numField => f.field.includes(numField))
).map((field) => (
<div key={`sum-report-${field.field}`} className="flex items-center space-x-2">
<Checkbox
id={`sum-report-${field.field}`}
checked={reportSumFields.includes(field.field)}
onCheckedChange={(checked) =>
handleReportSumFieldChange(field.field, checked as boolean)
}
/>
<Label htmlFor={`sum-report-${field.field}`} className="text-sm">
{field.label}
</Label>
</div>
))}
</div>
</div>
<Button
onClick={handleGenerateReport}
disabled={loading}
@@ -489,9 +621,10 @@ export default function Home() {
<TableRow>
<TableHead>报表日期</TableHead>
<TableHead>拆分方式</TableHead>
<TableHead>拆分值</TableHead>
<TableHead>企业/用户</TableHead>
<TableHead>订单数</TableHead>
<TableHead>总金额</TableHead>
<TableHead>求和结果</TableHead>
<TableHead>状态</TableHead>
<TableHead>生成时间</TableHead>
<TableHead>操作</TableHead>
@@ -506,9 +639,18 @@ export default function Home() {
{record.split_type === 'company_id' ? '企业' : '用户'}
</Badge>
</TableCell>
<TableCell>{record.split_value}</TableCell>
<TableCell>{record.split_name || record.split_value}</TableCell>
<TableCell>{record.total_orders}</TableCell>
<TableCell>¥{record.total_amount}</TableCell>
<TableCell>
{record.sum_results ? (
<div className="text-xs">
{Object.entries(JSON.parse(record.sum_results) as Record<string, string | number>).slice(0, 2).map(([key, value]) => (
<div key={key}>{key}: {value}</div>
))}
</div>
) : '-'}
</TableCell>
<TableCell>
<Badge variant={record.status === 1 ? 'default' : 'destructive'}>
{record.status === 1 ? '成功' : '失败'}

View File

@@ -8,8 +8,10 @@ const dbConfig = {
password: 'DsideaL147258369',
database: 'yltcharge',
waitForConnections: true,
connectionLimit: 10,
connectionLimit: 5,
queueLimit: 0,
enableKeepAlive: true,
keepAliveInitialDelay: 0,
};
// 创建连接池

View File

@@ -7,7 +7,10 @@ CREATE TABLE IF NOT EXISTS t_daily_report_config (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '配置ID',
config_name VARCHAR(100) NOT NULL COMMENT '配置名称',
split_type VARCHAR(20) NOT NULL COMMENT '拆分方式:company_id 或 user_id',
split_value VARCHAR(100) NOT NULL COMMENT '拆分值(企业ID或用户ID)',
split_name VARCHAR(200) COMMENT '拆分显示名称(企业名称或用户姓名)',
selected_fields JSON NOT NULL COMMENT '用户选择的字段列表(JSON数组)',
sum_fields JSON COMMENT '需要求和的字段列表(JSON数组)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
is_active TINYINT DEFAULT 1 COMMENT '是否启用:0禁用 1启用'
@@ -21,9 +24,11 @@ CREATE TABLE IF NOT EXISTS t_daily_report_history (
end_time DATETIME NOT NULL COMMENT '报表结束时间',
split_type VARCHAR(20) NOT NULL COMMENT '拆分方式:company_id 或 user_id',
split_value VARCHAR(100) NOT NULL COMMENT '拆分值(企业ID或用户ID)',
split_name VARCHAR(200) COMMENT '拆分显示名称',
config_id BIGINT COMMENT '使用的配置ID',
total_orders INT DEFAULT 0 COMMENT '订单总数',
total_amount DECIMAL(15,2) DEFAULT 0 COMMENT '总金额',
sum_results JSON COMMENT '求和结果(JSON对象)',
file_path VARCHAR(500) COMMENT '生成的文件路径',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '生成时间',
status TINYINT DEFAULT 1 COMMENT '状态:0失败 1成功'
@@ -39,15 +44,15 @@ export async function initDatabaseTables(): Promise<void> {
const statements = createTablesSQL.split(';').filter(stmt => stmt.trim());
for (const statement of statements) {
if (statement.trim()) {
await connection.execute(statement);
await connection.query(statement);
}
}
connection.release();
console.log('数据库表初始化成功');
console.log('创建了以下表:');
console.log('1. t_daily_report_config - 日报字段配置表');
console.log('2. t_daily_report_history - 日报生成历史表');
console.log('1. t_daily_report_config - 日报字段配置表(包含拆分值和显示名称)');
console.log('2. t_daily_report_history - 日报生成历史表(包含求和结果)');
} catch (error) {
console.error('数据库表初始化失败:', error);
throw error;

View File

@@ -8,7 +8,10 @@ import path from 'path';
interface ReportConfig {
config_id: number;
split_type: string; // 'company_id' 或 'user_id'
split_value: string; // 企业ID或用户ID
split_name?: string; // 显示名称
selected_fields: string[]; // 用户选择的字段
sum_fields?: string[]; // 需要求和的字段
}
// 日报生成结果
@@ -19,8 +22,10 @@ interface ReportResult {
endTime: string;
splitType: string;
splitValue: string;
splitName?: string;
totalOrders: number;
totalAmount: number;
sumResults?: Record<string, number>;
filePath?: string;
error?: string;
}
@@ -29,7 +34,9 @@ interface ReportResult {
export async function generateDailyReport(
splitType: string,
splitValue: string,
splitName: string,
selectedFields: string[],
sumFields: string[] = [],
reportDate?: string // 可选的报表日期,默认为昨天
): Promise<ReportResult> {
try {
@@ -48,7 +55,10 @@ export async function generateDailyReport(
const queryField = splitType === 'company_id' ? 'company_id' : 'user_id';
const fieldList = selectedFields.join(', ');
const [orders] = await pool.execute<any[]>(
// 获取连接
const connection = await pool.getConnection();
const [orders] = await connection.query<any[]>(
`SELECT ${fieldList} FROM t_equipment_charge_order
WHERE state = 3
AND report_time >= ?
@@ -58,6 +68,8 @@ export async function generateDailyReport(
[startTime, endTime, splitValue]
);
connection.release();
if (!Array.isArray(orders) || orders.length === 0) {
return {
success: false,
@@ -66,6 +78,7 @@ export async function generateDailyReport(
endTime: endTime.toISOString(),
splitType,
splitValue,
splitName,
totalOrders: 0,
totalAmount: 0,
error: '该时间段内没有订单数据'
@@ -97,6 +110,30 @@ export async function generateDailyReport(
return processed;
});
// 计算求和
const sumResults: Record<string, number> = {};
if (sumFields && sumFields.length > 0) {
for (const sumField of sumFields) {
const sum = orders.reduce((total, order) => {
const value = parseFloat(order[sumField] || 0);
return total + value;
}, 0);
sumResults[FIELD_MAPPING[sumField] || sumField] = sum;
}
// 添加求和行
const sumRow: Record<string, any> = {};
for (const field of selectedFields) {
const chineseName = FIELD_MAPPING[field] || field;
if (sumResults[chineseName] !== undefined) {
sumRow[chineseName] = sumResults[chineseName];
} else {
sumRow[chineseName] = '';
}
}
processedOrders.push(sumRow);
}
// 生成Excel文件
const workbook = XLSX.utils.book_new();
const worksheet = XLSX.utils.json_to_sheet(processedOrders);
@@ -109,8 +146,9 @@ export async function generateDailyReport(
XLSX.utils.book_append_sheet(workbook, worksheet, '日报');
// 生成文件名
const fileName = `日报_${splitType === 'company_id' ? '企业' : '用户'}_${splitValue}_${targetDate.toISOString().split('T')[0]}.xlsx`;
// 生成文件名(包含显示名称)
const displayName = splitName || splitValue;
const fileName = `日报_${splitType === 'company_id' ? '企业' : '用户'}_${displayName}_${targetDate.toISOString().split('T')[0]}.xlsx`;
const reportsDir = path.join(process.cwd(), 'public', 'reports');
// 确保目录存在
@@ -134,8 +172,10 @@ export async function generateDailyReport(
endTime: endTime.toISOString(),
splitType,
splitValue,
splitName,
totalOrders,
totalAmount,
sumResults: sumFields && sumFields.length > 0 ? sumResults : undefined,
filePath: `/reports/${fileName}`
};
} catch (error) {
@@ -146,6 +186,7 @@ export async function generateDailyReport(
endTime: '',
splitType,
splitValue,
splitName,
totalOrders: 0,
totalAmount: 0,
error: error instanceof Error ? error.message : '未知错误'
@@ -153,101 +194,79 @@ export async function generateDailyReport(
}
}
// 批量生成日报(用于定时任务)
// 批量生成日报(用于定时任务)- 现在只为配置中指定的单个企业/用户生成
export async function generateBatchReports(configId: number): Promise<ReportResult[]> {
try {
// 获取配置
const [configs] = await pool.execute<any[]>(
const connection = await pool.getConnection();
const [configs] = await connection.query<any[]>(
'SELECT * FROM t_daily_report_config WHERE id = ? AND is_active = 1',
[configId]
);
if (!Array.isArray(configs) || configs.length === 0) {
connection.release();
throw new Error('配置不存在或已禁用');
}
const config = configs[0];
connection.release();
const splitType = config.split_type;
const splitValue = config.split_value;
const splitName = config.split_name;
const selectedFields = JSON.parse(config.selected_fields);
const sumFields = config.sum_fields ? JSON.parse(config.sum_fields) : undefined;
// 获取所有需要生成日报的企业/用户列表
const queryField = splitType === 'company_id' ? 'company_id' : 'user_id';
// 只为配置中指定的企业/用户生成日报
const result = await generateDailyReport(splitType, splitValue, splitName, selectedFields, sumFields);
// 计算时间范围
const targetDate = new Date();
targetDate.setDate(targetDate.getDate() - 1);
// 保存到历史记录表
const connection2 = await pool.getConnection();
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 [];
if (result.success) {
await connection2.query(
`INSERT INTO t_daily_report_history
(report_date, start_time, end_time, split_type, split_value, split_name, config_id, total_orders, total_amount, sum_results, file_path, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
result.reportDate,
result.startTime,
result.endTime,
result.splitType,
result.splitValue,
result.splitName || null,
configId,
result.totalOrders,
result.totalAmount,
result.sumResults ? JSON.stringify(result.sumResults) : null,
result.filePath || null,
1
]
);
} else {
await connection2.query(
`INSERT INTO t_daily_report_history
(report_date, start_time, end_time, split_type, split_value, split_name, config_id, total_orders, total_amount, status)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
result.reportDate,
result.startTime,
result.endTime,
result.splitType,
result.splitValue,
result.splitName || null,
configId,
result.totalOrders,
result.totalAmount,
0
]
);
}
// 批量生成日报
const results: ReportResult[] = [];
connection2.release();
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;
return [result];
} catch (error) {
throw error;
}