首页新增新增&日活报表
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const UserActive = sequelize.define('UserActive', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '自增主键',
|
||||
},
|
||||
mac_addr: {
|
||||
type: DataTypes.STRING(30),
|
||||
allowNull: false,
|
||||
comment: '设备 MAC 地址',
|
||||
},
|
||||
model: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '设备型号(如 Luxsin-X9、Luxsin-X8)',
|
||||
},
|
||||
active_date: {
|
||||
type: DataTypes.DATEONLY,
|
||||
allowNull: false,
|
||||
comment: '活跃日期',
|
||||
},
|
||||
ip_addr: {
|
||||
type: DataTypes.STRING(45),
|
||||
allowNull: false,
|
||||
comment: '活跃时 IP 地址',
|
||||
},
|
||||
create_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '记录创建时间',
|
||||
},
|
||||
}, {
|
||||
tableName: 'user_active',
|
||||
comment: '用户设备日活记录(mac_addr + active_date 唯一)',
|
||||
});
|
||||
|
||||
module.exports = UserActive;
|
||||
@@ -0,0 +1,38 @@
|
||||
const { DataTypes } = require('sequelize');
|
||||
const sequelize = require('../config/database');
|
||||
|
||||
const UserDevice = sequelize.define('UserDevice', {
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true,
|
||||
comment: '自增主键',
|
||||
},
|
||||
mac_addr: {
|
||||
type: DataTypes.STRING(30),
|
||||
allowNull: false,
|
||||
unique: true,
|
||||
comment: '设备 MAC 地址(唯一)',
|
||||
},
|
||||
model: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false,
|
||||
comment: '设备型号(如 Luxsin-X9、Luxsin-X8)',
|
||||
},
|
||||
add_time: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false,
|
||||
defaultValue: DataTypes.NOW,
|
||||
comment: '首次注册时间',
|
||||
},
|
||||
ver: {
|
||||
type: DataTypes.STRING(10),
|
||||
allowNull: true,
|
||||
comment: '固件版本号',
|
||||
},
|
||||
}, {
|
||||
tableName: 'user_device',
|
||||
comment: '用户设备注册记录',
|
||||
});
|
||||
|
||||
module.exports = UserDevice;
|
||||
@@ -2,9 +2,11 @@
|
||||
* Dashboard 路由 — 首页统计数据
|
||||
*/
|
||||
const router = require('express').Router();
|
||||
const { Op } = require('sequelize');
|
||||
const { Op, fn, col } = require('sequelize');
|
||||
const Model = require('../models/Model');
|
||||
const Ota = require('../models/Ota');
|
||||
const UserDevice = require('../models/UserDevice');
|
||||
const UserActive = require('../models/UserActive');
|
||||
const logger = require('../config/logger');
|
||||
const { ApiResponse } = require('../utils/response');
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
@@ -62,4 +64,141 @@ router.get('/api/dashboard/today', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/dashboard/device-daily?days=30
|
||||
* 返回最近 N 天每天各设备型号的新增数量(堆叠柱状图数据)
|
||||
* 返回格式:{ dates: ['2026-06-16', ...], series: [{ name: 'Luxsin-X9', data: [5, 3, ...] }, ...], total: 123 }
|
||||
*/
|
||||
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();
|
||||
const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - days + 1);
|
||||
|
||||
// 按日期 + 型号分组统计
|
||||
const rows = await UserDevice.findAll({
|
||||
attributes: [
|
||||
[fn('DATE', col('add_time')), 'date'],
|
||||
'model',
|
||||
[fn('COUNT', col('id')), 'count'],
|
||||
],
|
||||
where: {
|
||||
add_time: { [Op.gte]: startDate },
|
||||
},
|
||||
group: [fn('DATE', col('add_time')), 'model'],
|
||||
order: [[fn('DATE', col('add_time')), 'ASC']],
|
||||
raw: true,
|
||||
});
|
||||
|
||||
// 构建日期序列(补全没有数据的日期)
|
||||
const dateList = [];
|
||||
const tmp = new Date(startDate);
|
||||
while (tmp <= now) {
|
||||
dateList.push(tmp.toISOString().slice(0, 10));
|
||||
tmp.setDate(tmp.getDate() + 1);
|
||||
}
|
||||
|
||||
// 收集所有型号
|
||||
const modelSet = new Set();
|
||||
const dataMap = new Map(); // date -> { model -> count }
|
||||
for (const row of rows) {
|
||||
const dateStr = row.date instanceof Date ? row.date.toISOString().slice(0, 10) : String(row.date).slice(0, 10);
|
||||
modelSet.add(row.model);
|
||||
if (!dataMap.has(dateStr)) dataMap.set(dateStr, {});
|
||||
dataMap.get(dateStr)[row.model] = parseInt(row.count, 10);
|
||||
}
|
||||
|
||||
// 构建系列数据
|
||||
const models = [...modelSet].sort();
|
||||
const series = models.map((m) => ({
|
||||
name: m,
|
||||
data: dateList.map((d) => dataMap.get(d)?.[m] || 0),
|
||||
}));
|
||||
|
||||
const total = rows.reduce((sum, r) => sum + parseInt(r.count, 10), 0);
|
||||
|
||||
return res.json(
|
||||
ApiResponse.success({
|
||||
dates: dateList,
|
||||
series,
|
||||
total,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(`Error getting device daily stats: ${e.message}`);
|
||||
return res.json(ApiResponse.error(`获取设备统计数据失败:${e.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/dashboard/active-daily?days=30
|
||||
* 返回最近 N 天每天各设备型号的日活数量(折线图数据)
|
||||
* 返回格式:{ dates: ['2026-06-16', ...], series: [{ name: 'Luxsin-X9', data: [5, 3, ...] }, ...], latest: { date, total } }
|
||||
*/
|
||||
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();
|
||||
const startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - days + 1);
|
||||
const startDateStr = startDate.toISOString().slice(0, 10);
|
||||
|
||||
// 按活跃日期 + 型号分组统计(active_date 为 DATE 类型,唯一键保证每设备每天一条)
|
||||
const rows = await UserActive.findAll({
|
||||
attributes: [
|
||||
'active_date',
|
||||
'model',
|
||||
[fn('COUNT', col('id')), 'count'],
|
||||
],
|
||||
where: {
|
||||
active_date: { [Op.gte]: startDateStr },
|
||||
},
|
||||
group: ['active_date', 'model'],
|
||||
order: [['active_date', 'ASC']],
|
||||
raw: true,
|
||||
});
|
||||
|
||||
// 构建日期序列(补全没有数据的日期)
|
||||
const dateList = [];
|
||||
const tmp = new Date(startDate);
|
||||
while (tmp <= now) {
|
||||
dateList.push(tmp.toISOString().slice(0, 10));
|
||||
tmp.setDate(tmp.getDate() + 1);
|
||||
}
|
||||
|
||||
// 收集所有型号
|
||||
const modelSet = new Set();
|
||||
const dataMap = new Map(); // date -> { model -> count }
|
||||
for (const row of rows) {
|
||||
const dateStr = String(row.active_date).slice(0, 10);
|
||||
modelSet.add(row.model);
|
||||
if (!dataMap.has(dateStr)) dataMap.set(dateStr, {});
|
||||
dataMap.get(dateStr)[row.model] = parseInt(row.count, 10);
|
||||
}
|
||||
|
||||
// 构建系列数据
|
||||
const models = [...modelSet].sort();
|
||||
const series = models.map((m) => ({
|
||||
name: m,
|
||||
data: dateList.map((d) => dataMap.get(d)?.[m] || 0),
|
||||
}));
|
||||
|
||||
// 计算每天总日活,取最近一个有数据的日子作为「最新一天总日活」
|
||||
const dailyTotals = dateList.map((d) => models.reduce((sum, m) => sum + (dataMap.get(d)?.[m] || 0), 0));
|
||||
let latestIdx = dailyTotals.length - 1;
|
||||
while (latestIdx > 0 && dailyTotals[latestIdx] === 0) latestIdx--;
|
||||
const latest = { date: dateList[latestIdx], total: dailyTotals[latestIdx] };
|
||||
|
||||
return res.json(
|
||||
ApiResponse.success({
|
||||
dates: dateList,
|
||||
series,
|
||||
latest,
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
logger.error(`Error getting active daily stats: ${e.message}`);
|
||||
return res.json(ApiResponse.error(`获取设备日活统计数据失败:${e.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user