| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236 |
- import { Router } from 'express';
- import { query } from 'express-validator';
- import { AnalyticsService } from '../services/AnalyticsService.js';
- import { WorkDayStatisticsService } from '../services/WorkDayStatisticsService.js';
- import { authenticate } from '../middleware/auth.js';
- import { asyncHandler } from '../middleware/error.js';
- import { validateRequest } from '../middleware/validate.js';
- const router = Router();
- const analyticsService = new AnalyticsService();
- const workDayStatisticsService = new WorkDayStatisticsService();
- router.use(authenticate);
- // 在 Node 中声明全局 fetch,避免 TypeScript 编译错误(运行时使用 Node 18+ 自带的 fetch)
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- declare const fetch: any;
- /**
- * 调用本地 Python 统计服务的工具函数
- * 默认地址: http://localhost:5005
- */
- async function callPythonAnalyticsApi(pathname: string, params: Record<string, string | number | undefined>) {
- const base = process.env.PYTHON_API_URL || 'http://localhost:5005';
- const url = new URL(base);
- url.pathname = pathname.startsWith('/') ? pathname : `/${pathname}`;
- const search = new URLSearchParams();
- Object.entries(params).forEach(([key, value]) => {
- if (value !== undefined && value !== null) {
- search.append(key, String(value));
- }
- });
- url.search = search.toString();
- try {
- const resp = await fetch(url.toString(), { method: 'GET' });
-
- // 检查响应状态
- if (!resp.ok) {
- // 尝试解析 JSON 错误响应
- let errorData: any;
- try {
- errorData = await resp.json();
- } catch {
- // 如果不是 JSON,读取文本内容
- const text = await resp.text();
- errorData = {
- success: false,
- error: `Python API 返回错误 (${resp.status}): ${text.substring(0, 500)}`,
- };
- }
- throw new Error(errorData.error || `Python API 返回错误: ${resp.status} ${resp.statusText}`);
- }
- // 检查 Content-Type
- const contentType = resp.headers.get('content-type') || '';
- if (!contentType.includes('application/json')) {
- const text = await resp.text();
- throw new Error(`Python API 返回非 JSON 响应: ${text.substring(0, 500)}`);
- }
- const json = await resp.json();
- return json;
- } catch (error: any) {
- // 如果是网络错误(连接失败、超时等)
- if (error.name === 'TypeError' && error.message.includes('fetch')) {
- throw new Error(`无法连接 Python API (${base}): ${error.message}`);
- }
- // 其他错误直接抛出
- throw error;
- }
- }
- // 获取汇总统计(直接走 Node 本地统计)
- router.get(
- '/summary',
- [
- query('startDate').notEmpty().withMessage('开始日期不能为空'),
- query('endDate').notEmpty().withMessage('结束日期不能为空'),
- validateRequest,
- ],
- asyncHandler(async (req, res) => {
- const { startDate, endDate, accountId, platform } = req.query;
- const summary = await analyticsService.getSummary(req.user!.userId, {
- startDate: startDate as string,
- endDate: endDate as string,
- accountId: accountId ? Number(accountId) : undefined,
- platform: platform as string,
- });
- res.json({ success: true, data: summary });
- })
- );
- /**
- * GET /api/analytics/trend-from-python
- * 通过 Node 转发到 Python 的 /work_day_statistics/trend
- * 前端只需要传 startDate / endDate,userId 由 JWT 决定
- */
- router.get(
- '/trend-from-python',
- [
- query('startDate').notEmpty().withMessage('开始日期不能为空'),
- query('endDate').notEmpty().withMessage('结束日期不能为空'),
- validateRequest,
- ],
- asyncHandler(async (req, res) => {
- const { startDate, endDate } = req.query;
- try {
- const pythonResult = await callPythonAnalyticsApi('/work_day_statistics/trend', {
- user_id: req.user!.userId,
- start_date: String(startDate),
- end_date: String(endDate),
- });
- if (!pythonResult || pythonResult.success === false) {
- return res.status(500).json({
- success: false,
- error: pythonResult?.error || '获取数据趋势失败',
- message: pythonResult?.error || '获取数据趋势失败',
- });
- }
- return res.json({
- success: true,
- data: pythonResult.data,
- });
- } catch (error: any) {
- console.error('[trend-from-python] 调用 Python API 失败:', error);
- return res.status(500).json({
- success: false,
- error: error.message || '调用 Python API 失败',
- message: error.message || '调用 Python API 失败',
- });
- }
- })
- );
- // 获取趋势数据(直接走 Node 本地统计)
- router.get(
- '/trend',
- [
- query('startDate').notEmpty().withMessage('开始日期不能为空'),
- query('endDate').notEmpty().withMessage('结束日期不能为空'),
- validateRequest,
- ],
- asyncHandler(async (req, res) => {
- const { startDate, endDate, accountId, platform } = req.query;
- const trend = await analyticsService.getTrend(req.user!.userId, {
- startDate: startDate as string,
- endDate: endDate as string,
- accountId: accountId ? Number(accountId) : undefined,
- platform: platform as string,
- });
- res.json({ success: true, data: trend });
- })
- );
- /**
- * GET /api/analytics/platforms-from-python
- * 通过 Node 转发到 Python 的 /work_day_statistics/platforms
- * 前端只需要传 startDate / endDate,userId 由 JWT 决定
- */
- router.get(
- '/platforms-from-python',
- [
- query('startDate').notEmpty().withMessage('开始日期不能为空'),
- query('endDate').notEmpty().withMessage('结束日期不能为空'),
- validateRequest,
- ],
- asyncHandler(async (req, res) => {
- const { startDate, endDate } = req.query;
- try {
- // 口径切换:平台数据不再转发 Python,直接使用 Node 本地的 user_day_statistics 统计
- const data = await workDayStatisticsService.getStatisticsByPlatform(req.user!.userId, {
- startDate: String(startDate),
- endDate: String(endDate),
- });
- return res.json({ success: true, data });
- } catch (error: any) {
- // 捕获并返回详细的错误信息
- console.error('[platforms-from-python] 调用 Python API 失败:', error);
- return res.status(500).json({
- success: false,
- error: error.message || '调用 Python API 失败',
- message: error.message || '调用 Python API 失败',
- });
- }
- })
- );
- // 获取平台对比(直接走 Node 本地统计)
- router.get(
- '/platforms',
- [
- query('startDate').notEmpty().withMessage('开始日期不能为空'),
- query('endDate').notEmpty().withMessage('结束日期不能为空'),
- validateRequest,
- ],
- asyncHandler(async (req, res) => {
- const { startDate, endDate } = req.query;
- const comparison = await analyticsService.getPlatformComparison(req.user!.userId, {
- startDate: startDate as string,
- endDate: endDate as string,
- });
- res.json({ success: true, data: comparison });
- })
- );
- // 导出报表
- router.get(
- '/export',
- [
- query('startDate').notEmpty().withMessage('开始日期不能为空'),
- query('endDate').notEmpty().withMessage('结束日期不能为空'),
- validateRequest,
- ],
- asyncHandler(async (req, res) => {
- const { startDate, endDate, accountId, format = 'csv' } = req.query;
- const data = await analyticsService.exportData(req.user!.userId, {
- startDate: startDate as string,
- endDate: endDate as string,
- accountId: accountId ? Number(accountId) : undefined,
- format: format as string,
- });
-
- res.setHeader('Content-Type', format === 'csv' ? 'text/csv' : 'application/json');
- res.setHeader('Content-Disposition', `attachment; filename=analytics_${startDate}_${endDate}.${format}`);
- res.send(data);
- })
- );
- export default router;
|