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:
@@ -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({
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
196
src/app/page.tsx
196
src/app/page.tsx
@@ -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 ? '成功' : '失败'}
|
||||
|
||||
@@ -8,8 +8,10 @@ const dbConfig = {
|
||||
password: 'DsideaL147258369',
|
||||
database: 'yltcharge',
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
connectionLimit: 5,
|
||||
queueLimit: 0,
|
||||
enableKeepAlive: true,
|
||||
keepAliveInitialDelay: 0,
|
||||
};
|
||||
|
||||
// 创建连接池
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user