优化首页报表
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
# 统一时区为东八区
|
||||
ENV TZ=Asia/Shanghai
|
||||
RUN apk add --no-cache tzdata && \
|
||||
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
|
||||
echo "Asia/Shanghai" > /etc/timezone
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Create logs directory
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
/**
|
||||
* 入口文件 — 对应 Python: main.py
|
||||
*/
|
||||
// 统一时区为东八区(服务器可能在其他时区,但业务面向中国用户)
|
||||
process.env.TZ = 'Asia/Shanghai';
|
||||
|
||||
require('./config/loadEnv');
|
||||
|
||||
const express = require('express');
|
||||
|
||||
@@ -73,6 +73,45 @@ router.get('/api/dashboard/device-daily', async (req, res) => {
|
||||
try {
|
||||
const days = Math.min(Math.max(parseInt(req.query.days || '30', 10) || 30, 1), 365);
|
||||
const now = new Date();
|
||||
|
||||
// ===== 今天:按小时分组 =====
|
||||
if (days === 1) {
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
const rows = await UserDevice.findAll({
|
||||
attributes: [
|
||||
[fn('HOUR', col('add_time')), 'hour'],
|
||||
'model',
|
||||
[fn('COUNT', col('id')), 'count'],
|
||||
],
|
||||
where: { add_time: { [Op.gte]: todayStart } },
|
||||
group: [fn('HOUR', col('add_time')), 'model'],
|
||||
order: [[fn('HOUR', col('add_time')), 'ASC']],
|
||||
raw: true,
|
||||
});
|
||||
|
||||
const currentHour = now.getHours();
|
||||
const hourList = Array.from({ length: currentHour + 1 }, (_, h) => `${String(h).padStart(2, '0')}:00`);
|
||||
const modelSet = new Set();
|
||||
const dataMap = new Map();
|
||||
for (const row of rows) {
|
||||
const hourStr = `${String(parseInt(row.hour, 10)).padStart(2, '0')}:00`;
|
||||
modelSet.add(row.model);
|
||||
if (!dataMap.has(hourStr)) dataMap.set(hourStr, {});
|
||||
dataMap.get(hourStr)[row.model] = parseInt(row.count, 10);
|
||||
}
|
||||
|
||||
const models = [...modelSet].sort();
|
||||
const series = models.map((m) => ({
|
||||
name: m,
|
||||
data: hourList.map((h) => dataMap.get(h)?.[m] || 0),
|
||||
}));
|
||||
const total = rows.reduce((sum, r) => sum + parseInt(r.count, 10), 0);
|
||||
|
||||
return res.json(ApiResponse.success({ dates: hourList, series, total }));
|
||||
}
|
||||
|
||||
// ===== 多天:按日期分组 =====
|
||||
const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - days + 1);
|
||||
|
||||
// 按日期 + 型号分组统计
|
||||
@@ -139,6 +178,47 @@ router.get('/api/dashboard/active-daily', async (req, res) => {
|
||||
try {
|
||||
const days = Math.min(Math.max(parseInt(req.query.days || '30', 10) || 30, 1), 365);
|
||||
const now = new Date();
|
||||
|
||||
// ===== 今天:按小时分组(用 create_at 的时间维度)=====
|
||||
if (days === 1) {
|
||||
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
|
||||
const rows = await UserActive.findAll({
|
||||
attributes: [
|
||||
[fn('HOUR', col('create_at')), 'hour'],
|
||||
'model',
|
||||
[fn('COUNT', col('id')), 'count'],
|
||||
],
|
||||
where: { create_at: { [Op.gte]: todayStart } },
|
||||
group: [fn('HOUR', col('create_at')), 'model'],
|
||||
order: [[fn('HOUR', col('create_at')), 'ASC']],
|
||||
raw: true,
|
||||
});
|
||||
|
||||
const currentHour = now.getHours();
|
||||
const hourList = Array.from({ length: currentHour + 1 }, (_, h) => `${String(h).padStart(2, '0')}:00`);
|
||||
const modelSet = new Set();
|
||||
const dataMap = new Map();
|
||||
for (const row of rows) {
|
||||
const hourStr = `${String(parseInt(row.hour, 10)).padStart(2, '0')}:00`;
|
||||
modelSet.add(row.model);
|
||||
if (!dataMap.has(hourStr)) dataMap.set(hourStr, {});
|
||||
dataMap.get(hourStr)[row.model] = parseInt(row.count, 10);
|
||||
}
|
||||
|
||||
const models = [...modelSet].sort();
|
||||
const series = models.map((m) => ({
|
||||
name: m,
|
||||
data: hourList.map((h) => dataMap.get(h)?.[m] || 0),
|
||||
}));
|
||||
|
||||
const todayTotal = rows.reduce((sum, r) => sum + parseInt(r.count, 10), 0);
|
||||
const latest = { date: '今日', total: todayTotal };
|
||||
|
||||
return res.json(ApiResponse.success({ dates: hourList, series, latest }));
|
||||
}
|
||||
|
||||
// ===== 多天:按日期分组 =====
|
||||
const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - days + 1);
|
||||
const startDateStr = startDate.toISOString().slice(0, 10);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ const multer = require('multer');
|
||||
const path = require('path');
|
||||
const axios = require('axios');
|
||||
const Model = require('../models/Model');
|
||||
const Brand = require('../models/Brand');
|
||||
const logger = require('../config/logger');
|
||||
const { ApiResponse } = require('../utils/response');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
@@ -580,6 +581,83 @@ router.delete('/api/models/:model_id', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH /api/models/batch-update — 批量修改型号字段
|
||||
router.patch('/api/models/batch-update', async (req, res) => {
|
||||
try {
|
||||
const { ids, brand_name, form, source, rig } = req.body || {};
|
||||
if (!Array.isArray(ids) || !ids.length) {
|
||||
return res.json(ApiResponse.error('请选择要修改的型号'));
|
||||
}
|
||||
|
||||
// 至少指定一个要修改的字段
|
||||
const hasUpdate = [brand_name, form, source, rig].some(
|
||||
(v) => v !== undefined && v !== null
|
||||
);
|
||||
if (!hasUpdate) {
|
||||
return res.json(ApiResponse.error('请至少指定一个要修改的字段'));
|
||||
}
|
||||
|
||||
// 若修改品牌名称,确保品牌存在
|
||||
if (brand_name) {
|
||||
const bn = String(brand_name).trim();
|
||||
const existing = await Brand.findOne({ where: { name: bn } });
|
||||
if (!existing) {
|
||||
await Brand.create({ name: bn });
|
||||
logger.info(`Brand created during batch update: ${bn}`);
|
||||
}
|
||||
}
|
||||
|
||||
const models = await Model.findAll({ where: { id: { [Op.in]: ids } } });
|
||||
if (!models.length) {
|
||||
return res.json(ApiResponse.error('未找到选中的型号数据'));
|
||||
}
|
||||
|
||||
const updatedIds = [];
|
||||
const errors = [];
|
||||
|
||||
for (const m of models) {
|
||||
try {
|
||||
const oldParams = {
|
||||
source: m.source,
|
||||
form: m.form,
|
||||
brandName: m.brand_name,
|
||||
modelName: m.name,
|
||||
};
|
||||
|
||||
if (brand_name !== undefined && brand_name !== null) m.brand_name = String(brand_name).trim();
|
||||
if (form !== undefined && form !== null) m.form = form || null;
|
||||
if (source !== undefined && source !== null) m.source = source || null;
|
||||
if (rig !== undefined && rig !== null) m.rig = rig || null;
|
||||
|
||||
// S3 CSV 路径迁移(brand_name 或 form 变更时)
|
||||
const newParams = {
|
||||
source: m.source,
|
||||
form: m.form,
|
||||
brandName: m.brand_name,
|
||||
modelName: m.name,
|
||||
};
|
||||
await moveMeasurementOnS3(oldParams, newParams);
|
||||
|
||||
await m.save();
|
||||
updatedIds.push(m.id);
|
||||
} catch (e) {
|
||||
logger.error(`Batch update failed for model id=${m.id}: ${e.message}`);
|
||||
errors.push({ id: m.id, brand_name: m.brand_name, name: m.name, error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Batch update: ${updatedIds.length} succeeded, ${errors.length} failed`);
|
||||
return res.json(ApiResponse.success({
|
||||
updated_count: updatedIds.length,
|
||||
updated_ids: updatedIds,
|
||||
errors,
|
||||
}));
|
||||
} catch (e) {
|
||||
logger.error(`Batch update error: ${e.message}`);
|
||||
return res.json(ApiResponse.error('error'));
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/models/push-to-search/validate
|
||||
router.post('/api/models/push-to-search/validate', async (req, res) => {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user