ota 拓展功能-黑名单&灰色定向上线

This commit is contained in:
eafonyang
2026-06-30 18:15:25 +08:00
parent 08da279275
commit 864040f45b
16 changed files with 1872 additions and 9 deletions
+36
View File
@@ -0,0 +1,36 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const Ota = require('./Ota');
const BlackList = sequelize.define('BlackList', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
comment: 'ID',
},
ota_id: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'ota_id',
comment: '关联 OTA 版本 ID',
},
mac: {
type: DataTypes.STRING(100),
allowNull: false,
comment: 'MAC 地址',
},
create_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间',
},
}, {
tableName: 'black_list',
comment: 'OTA 黑名单',
});
BlackList.belongsTo(Ota, { foreignKey: 'ota_id', as: 'ota' });
module.exports = BlackList;
+37
View File
@@ -0,0 +1,37 @@
const { DataTypes } = require('sequelize');
const sequelize = require('../config/database');
const Ota = require('./Ota');
const OtaTargetDevice = sequelize.define('OtaTargetDevice', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
comment: 'ID',
},
ota_id: {
type: DataTypes.INTEGER,
allowNull: false,
field: 'ota_id',
comment: '关联 OTA 版本 ID',
},
mac_addr: {
type: DataTypes.STRING(17),
allowNull: false,
field: 'mac_addr',
comment: 'MAC 地址',
},
create_at: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.NOW,
comment: '创建时间',
},
}, {
tableName: 'ota_target_device',
comment: '定向升级设备',
});
OtaTargetDevice.belongsTo(Ota, { foreignKey: 'ota_id', as: 'ota' });
module.exports = OtaTargetDevice;
+3 -1
View File
@@ -1,7 +1,9 @@
const Brand = require('./Brand');
const Model = require('./Model');
const Ota = require('./Ota');
const BlackList = require('./BlackList');
const OtaTargetDevice = require('./OtaTargetDevice');
const ShareCodeLog = require('./ShareCodeLog');
const DashboardUser = require('./DashboardUser');
module.exports = { Brand, Model, Ota, ShareCodeLog, DashboardUser };
module.exports = { Brand, Model, Ota, BlackList, OtaTargetDevice, ShareCodeLog, DashboardUser };
+222
View File
@@ -0,0 +1,222 @@
/**
* 黑名单路由
*/
const router = require('express').Router();
const { Op } = require('sequelize');
const BlackList = require('../models/BlackList');
const Ota = require('../models/Ota');
const logger = require('../config/logger');
const { ApiResponse } = require('../utils/response');
const { authMiddleware } = require('../middleware/auth');
const { BlackListCreateSchema, BlackListUpdateSchema } = require('../validators/blacklist');
// 所有接口需登录
router.use('/api/blacklist', authMiddleware);
// GET /api/blacklist/
router.get('/api/blacklist/', async (req, res) => {
try {
const skip = parseInt(req.query.skip || '0', 10);
const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000);
const otaId = req.query.ota_id !== undefined ? parseInt(req.query.ota_id, 10) : null;
const model = req.query.model;
const mac = req.query.mac;
const where = {};
if (otaId !== null && !isNaN(otaId)) where.ota_id = otaId;
if (mac) where.mac = { [Op.like]: `%${mac}%` };
// 如果按 model 筛选,需先查出对应 model 的 ota_id 列表
if (model) {
const otaIds = await Ota.findAll({
where: { model: { [Op.like]: `%${model}%` } },
attributes: ['id'],
});
const ids = otaIds.map((o) => o.id);
if (ids.length === 0) {
return res.json(ApiResponse.success({ items: [], total: 0, skip, limit }));
}
if (where.ota_id !== undefined) {
where.ota_id = { [Op.and]: [where.ota_id, { [Op.in]: ids }] };
} else {
where.ota_id = { [Op.in]: ids };
}
}
const total = await BlackList.count({ where });
const rows = await BlackList.findAll({
where,
include: [{
model: Ota,
as: 'ota',
attributes: ['id', 'verCode', 'verName', 'model'],
}],
offset: skip,
limit,
order: [['id', 'DESC']],
});
logger.info(`Found ${rows.length} BlackList records, total=${total}`);
if (!rows.length) {
return res.json(ApiResponse.noData('empty'));
}
const items = rows.map(blacklistToDict);
return res.json(ApiResponse.success({ items, total, skip, limit }));
} catch (e) {
logger.error(`Error getting BlackList: ${e.message}`);
return res.json(ApiResponse.error('error'));
}
});
// GET /api/blacklist/:id
router.get('/api/blacklist/:id', async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
logger.info(`Getting BlackList: id=${id}`);
const item = await BlackList.findByPk(id, {
include: [{
model: Ota,
as: 'ota',
attributes: ['id', 'verCode', 'verName', 'model'],
}],
});
if (!item) {
logger.warn(`BlackList not found: id=${id}`);
return res.json(ApiResponse.noData('empty'));
}
return res.json(ApiResponse.success(blacklistToDict(item)));
} catch (e) {
logger.error(`Error getting BlackList ${req.params.id}: ${e.message}`);
return res.json(ApiResponse.error('error'));
}
});
// POST /api/blacklist/
router.post('/api/blacklist/', async (req, res) => {
try {
const parsed = BlackListCreateSchema.safeParse(req.body);
if (!parsed.success) {
const errors = parsed.error.errors.map((e) => e.message).join('; ');
return res.json(ApiResponse.error(errors));
}
const data = parsed.data;
logger.info(`Creating BlackList: ota_id=${data.ota_id}, mac=${data.mac}`);
// 校验 OTA 是否存在
const ota = await Ota.findByPk(data.ota_id);
if (!ota) {
return res.json(ApiResponse.error('OTA 版本不存在'));
}
const item = await BlackList.create(data);
logger.info(`BlackList created: id=${item.id}`);
// 重新加载关联数据
const created = await BlackList.findByPk(item.id, {
include: [{
model: Ota,
as: 'ota',
attributes: ['id', 'verCode', 'verName', 'model'],
}],
});
return res.json(ApiResponse.success(blacklistToDict(created)));
} catch (e) {
logger.error(`Error creating BlackList: ${e.message}`);
return res.json(ApiResponse.error('error'));
}
});
// PUT /api/blacklist/:id
router.put('/api/blacklist/:id', async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const parsed = BlackListUpdateSchema.safeParse(req.body);
if (!parsed.success) {
const errors = parsed.error.errors.map((e) => e.message).join('; ');
return res.json(ApiResponse.error(errors));
}
const data = parsed.data;
logger.info(`Updating BlackList: id=${id}`);
const item = await BlackList.findByPk(id);
if (!item) {
logger.warn(`BlackList not found: id=${id}`);
return res.json(ApiResponse.noData('empty'));
}
// 如果更新 ota_id,校验 OTA 是否存在
if (data.ota_id !== undefined && data.ota_id !== null && data.ota_id !== item.ota_id) {
const ota = await Ota.findByPk(data.ota_id);
if (!ota) {
return res.json(ApiResponse.error('OTA 版本不存在'));
}
}
const updateFields = Object.entries(data).filter(([_, v]) => v !== undefined && v !== null);
for (const [field, value] of updateFields) {
item[field] = value;
}
await item.save();
logger.info(`BlackList updated: id=${id}`);
// 重新加载关联数据
const updated = await BlackList.findByPk(id, {
include: [{
model: Ota,
as: 'ota',
attributes: ['id', 'verCode', 'verName', 'model'],
}],
});
return res.json(ApiResponse.success(blacklistToDict(updated)));
} catch (e) {
logger.error(`Error updating BlackList ${req.params.id}: ${e.message}`);
return res.json(ApiResponse.error('error'));
}
});
// DELETE /api/blacklist/:id
router.delete('/api/blacklist/:id', async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
logger.info(`Deleting BlackList: id=${id}`);
const item = await BlackList.findByPk(id);
if (!item) {
logger.warn(`BlackList not found: id=${id}`);
return res.json(ApiResponse.noData('empty'));
}
await item.destroy();
logger.info(`BlackList deleted: id=${id}`);
return res.json(ApiResponse.success(null, 'success'));
} catch (e) {
logger.error(`Error deleting BlackList ${req.params.id}: ${e.message}`);
return res.json(ApiResponse.error('error'));
}
});
// BlackList 对象转字典
function blacklistToDict(item) {
return {
id: item.id,
ota_id: item.ota_id,
mac: item.mac,
create_at: item.create_at ? item.create_at.toISOString() : null,
ota: item.ota ? {
id: item.ota.id,
verCode: item.ota.verCode,
verName: item.ota.verName,
model: item.ota.model,
} : null,
};
}
module.exports = router;
+3 -1
View File
@@ -5,8 +5,10 @@ const authRouter = require('./auth');
const brandsRouter = require('./brands');
const modelsRouter = require('./models');
const otaRouter = require('./ota');
const blacklistRouter = require('./blacklist');
const otaTargetDeviceRouter = require('./otaTargetDevice');
const shareCodeLogsRouter = require('./shareCodeLogs');
const usersRouter = require('./users');
const dashboardRouter = require('./dashboard');
module.exports = [authRouter, brandsRouter, modelsRouter, otaRouter, shareCodeLogsRouter, usersRouter, dashboardRouter];
module.exports = [authRouter, brandsRouter, modelsRouter, otaRouter, blacklistRouter, otaTargetDeviceRouter, shareCodeLogsRouter, usersRouter, dashboardRouter];
+247
View File
@@ -0,0 +1,247 @@
/**
* 定向升级路由
*/
const router = require('express').Router();
const { Op } = require('sequelize');
const OtaTargetDevice = require('../models/OtaTargetDevice');
const Ota = require('../models/Ota');
const logger = require('../config/logger');
const { ApiResponse } = require('../utils/response');
const { authMiddleware } = require('../middleware/auth');
const { OtaTargetDeviceCreateSchema, OtaTargetDeviceUpdateSchema } = require('../validators/otaTargetDevice');
// 所有接口需登录
router.use('/api/ota-target-device', authMiddleware);
// GET /api/ota-target-device/
router.get('/api/ota-target-device/', async (req, res) => {
try {
const skip = parseInt(req.query.skip || '0', 10);
const limit = Math.min(parseInt(req.query.limit || '100', 10), 1000);
const otaId = req.query.ota_id !== undefined ? parseInt(req.query.ota_id, 10) : null;
const model = req.query.model;
const macAddr = req.query.mac_addr;
const where = {};
if (otaId !== null && !isNaN(otaId)) where.ota_id = otaId;
if (macAddr) where.mac_addr = { [Op.like]: `%${macAddr}%` };
// 如果按 model 筛选,需先查出对应 model 的 ota_id 列表
if (model) {
const otaIds = await Ota.findAll({
where: { model: { [Op.like]: `%${model}%` } },
attributes: ['id'],
});
const ids = otaIds.map((o) => o.id);
if (ids.length === 0) {
return res.json(ApiResponse.success({ items: [], total: 0, skip, limit }));
}
if (where.ota_id !== undefined) {
where.ota_id = { [Op.and]: [where.ota_id, { [Op.in]: ids }] };
} else {
where.ota_id = { [Op.in]: ids };
}
}
const total = await OtaTargetDevice.count({ where });
const rows = await OtaTargetDevice.findAll({
where,
include: [{
model: Ota,
as: 'ota',
attributes: ['id', 'verCode', 'verName', 'model'],
}],
offset: skip,
limit,
order: [['id', 'DESC']],
});
logger.info(`Found ${rows.length} OtaTargetDevice records, total=${total}`);
if (!rows.length) {
return res.json(ApiResponse.noData('empty'));
}
const items = rows.map(toDict);
return res.json(ApiResponse.success({ items, total, skip, limit }));
} catch (e) {
logger.error(`Error getting OtaTargetDevice: ${e.message}`);
return res.json(ApiResponse.error('error'));
}
});
// GET /api/ota-target-device/:id
router.get('/api/ota-target-device/:id', async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
logger.info(`Getting OtaTargetDevice: id=${id}`);
const item = await OtaTargetDevice.findByPk(id, {
include: [{
model: Ota,
as: 'ota',
attributes: ['id', 'verCode', 'verName', 'model'],
}],
});
if (!item) {
logger.warn(`OtaTargetDevice not found: id=${id}`);
return res.json(ApiResponse.noData('empty'));
}
return res.json(ApiResponse.success(toDict(item)));
} catch (e) {
logger.error(`Error getting OtaTargetDevice ${req.params.id}: ${e.message}`);
return res.json(ApiResponse.error('error'));
}
});
// POST /api/ota-target-device/
router.post('/api/ota-target-device/', async (req, res) => {
try {
const parsed = OtaTargetDeviceCreateSchema.safeParse(req.body);
if (!parsed.success) {
const errors = parsed.error.errors.map((e) => e.message).join('; ');
return res.json(ApiResponse.error(errors));
}
const data = parsed.data;
logger.info(`Creating OtaTargetDevice: ota_id=${data.ota_id}, mac_addr=${data.mac_addr}`);
// 校验 OTA 是否存在
const ota = await Ota.findByPk(data.ota_id);
if (!ota) {
return res.json(ApiResponse.error('OTA 版本不存在'));
}
// 唯一性校验:同一 ota_id 下 mac_addr 不能重复
const existing = await OtaTargetDevice.findOne({
where: { ota_id: data.ota_id, mac_addr: data.mac_addr },
});
if (existing) {
return res.json(ApiResponse.error('该 MAC 地址已在此 OTA 版本中'));
}
const item = await OtaTargetDevice.create(data);
logger.info(`OtaTargetDevice created: id=${item.id}`);
// 重新加载关联数据
const created = await OtaTargetDevice.findByPk(item.id, {
include: [{
model: Ota,
as: 'ota',
attributes: ['id', 'verCode', 'verName', 'model'],
}],
});
return res.json(ApiResponse.success(toDict(created)));
} catch (e) {
logger.error(`Error creating OtaTargetDevice: ${e.message}`);
return res.json(ApiResponse.error('error'));
}
});
// PUT /api/ota-target-device/:id
router.put('/api/ota-target-device/:id', async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
const parsed = OtaTargetDeviceUpdateSchema.safeParse(req.body);
if (!parsed.success) {
const errors = parsed.error.errors.map((e) => e.message).join('; ');
return res.json(ApiResponse.error(errors));
}
const data = parsed.data;
logger.info(`Updating OtaTargetDevice: id=${id}`);
const item = await OtaTargetDevice.findByPk(id);
if (!item) {
logger.warn(`OtaTargetDevice not found: id=${id}`);
return res.json(ApiResponse.noData('empty'));
}
// 如果更新 ota_id,校验 OTA 是否存在
if (data.ota_id !== undefined && data.ota_id !== null && data.ota_id !== item.ota_id) {
const ota = await Ota.findByPk(data.ota_id);
if (!ota) {
return res.json(ApiResponse.error('OTA 版本不存在'));
}
}
// 唯一性校验:如果 ota_id 或 mac_addr 有变更
const newOtaId = data.ota_id !== undefined && data.ota_id !== null ? data.ota_id : item.ota_id;
const newMacAddr = data.mac_addr !== undefined && data.mac_addr !== null ? data.mac_addr : item.mac_addr;
if (newOtaId !== item.ota_id || newMacAddr !== item.mac_addr) {
const existing = await OtaTargetDevice.findOne({
where: {
ota_id: newOtaId,
mac_addr: newMacAddr,
id: { [Op.ne]: id },
},
});
if (existing) {
return res.json(ApiResponse.error('该 MAC 地址已在此 OTA 版本中'));
}
}
const updateFields = Object.entries(data).filter(([_, v]) => v !== undefined && v !== null);
for (const [field, value] of updateFields) {
item[field] = value;
}
await item.save();
logger.info(`OtaTargetDevice updated: id=${id}`);
// 重新加载关联数据
const updated = await OtaTargetDevice.findByPk(id, {
include: [{
model: Ota,
as: 'ota',
attributes: ['id', 'verCode', 'verName', 'model'],
}],
});
return res.json(ApiResponse.success(toDict(updated)));
} catch (e) {
logger.error(`Error updating OtaTargetDevice ${req.params.id}: ${e.message}`);
return res.json(ApiResponse.error('error'));
}
});
// DELETE /api/ota-target-device/:id
router.delete('/api/ota-target-device/:id', async (req, res) => {
try {
const id = parseInt(req.params.id, 10);
logger.info(`Deleting OtaTargetDevice: id=${id}`);
const item = await OtaTargetDevice.findByPk(id);
if (!item) {
logger.warn(`OtaTargetDevice not found: id=${id}`);
return res.json(ApiResponse.noData('empty'));
}
await item.destroy();
logger.info(`OtaTargetDevice deleted: id=${id}`);
return res.json(ApiResponse.success(null, 'success'));
} catch (e) {
logger.error(`Error deleting OtaTargetDevice ${req.params.id}: ${e.message}`);
return res.json(ApiResponse.error('error'));
}
});
// OtaTargetDevice 对象转字典
function toDict(item) {
return {
id: item.id,
ota_id: item.ota_id,
mac_addr: item.mac_addr,
create_at: item.create_at ? item.create_at.toISOString() : null,
ota: item.ota ? {
id: item.ota.id,
verCode: item.ota.verCode,
verName: item.ota.verName,
model: item.ota.model,
} : null,
};
}
module.exports = router;
+13
View File
@@ -0,0 +1,13 @@
const { z } = require('zod');
const BlackListCreateSchema = z.object({
ota_id: z.number().int({ message: 'ota_id 须为整数' }),
mac: z.string().min(1, 'MAC 地址不能为空').max(100, 'MAC 地址最多100字符'),
});
const BlackListUpdateSchema = z.object({
ota_id: z.number().int({ message: 'ota_id 须为整数' }).optional().nullable(),
mac: z.string().min(1).max(100).optional().nullable(),
});
module.exports = { BlackListCreateSchema, BlackListUpdateSchema };
+13
View File
@@ -0,0 +1,13 @@
const { z } = require('zod');
const OtaTargetDeviceCreateSchema = z.object({
ota_id: z.number().int({ message: 'ota_id 须为整数' }),
mac_addr: z.string().min(1, 'MAC 地址不能为空').max(17, 'MAC 地址最多17字符'),
});
const OtaTargetDeviceUpdateSchema = z.object({
ota_id: z.number().int({ message: 'ota_id 须为整数' }).optional().nullable(),
mac_addr: z.string().min(1).max(17).optional().nullable(),
});
module.exports = { OtaTargetDeviceCreateSchema, OtaTargetDeviceUpdateSchema };