2026-02-26 11:24:00 +08:00
|
|
|
const express = require('express');
|
|
|
|
|
const router = express.Router();
|
|
|
|
|
const { Op } = require('sequelize');
|
2026-02-26 11:59:19 +08:00
|
|
|
const { sequelize, dbDialect } = require('../db');
|
2026-02-26 11:24:00 +08:00
|
|
|
const InventoryPlan = require('../models/InventoryPlan');
|
|
|
|
|
const InventoryTask = require('../models/InventoryTask');
|
|
|
|
|
const InventoryRecord = require('../models/InventoryRecord');
|
|
|
|
|
const Device = require('../models/Device');
|
|
|
|
|
const Rack = require('../models/Rack');
|
|
|
|
|
const Room = require('../models/Room');
|
|
|
|
|
const User = require('../models/User');
|
2026-03-10 09:20:31 +08:00
|
|
|
const PendingDevice = require('../models/PendingDevice');
|
2026-02-26 11:24:00 +08:00
|
|
|
const { authMiddleware, authorize } = require('../middleware/auth');
|
2026-03-06 14:39:52 +08:00
|
|
|
const { PAGINATION } = require('../config');
|
2026-02-26 11:24:00 +08:00
|
|
|
|
|
|
|
|
InventoryTask.belongsTo(InventoryPlan, { foreignKey: 'planId', as: 'Plan' });
|
|
|
|
|
InventoryPlan.hasMany(InventoryTask, { foreignKey: 'planId', as: 'Tasks' });
|
|
|
|
|
InventoryTask.belongsTo(User, { foreignKey: 'assignedTo', as: 'Assignee' });
|
|
|
|
|
InventoryRecord.belongsTo(InventoryTask, { foreignKey: 'taskId', as: 'Task' });
|
|
|
|
|
InventoryTask.hasMany(InventoryRecord, { foreignKey: 'taskId', as: 'Records' });
|
|
|
|
|
InventoryRecord.belongsTo(InventoryPlan, { foreignKey: 'planId', as: 'Plan' });
|
|
|
|
|
InventoryPlan.hasMany(InventoryRecord, { foreignKey: 'planId', as: 'Records' });
|
|
|
|
|
InventoryRecord.belongsTo(Device, { foreignKey: 'deviceId', as: 'Device' });
|
|
|
|
|
InventoryRecord.belongsTo(User, { foreignKey: 'checkedBy', as: 'Checker' });
|
|
|
|
|
|
|
|
|
|
function generateId(prefix) {
|
|
|
|
|
return `${prefix}${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generatePlanId() {
|
|
|
|
|
return `PLAN${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generateTaskId() {
|
|
|
|
|
return `TASK${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function generateRecordId() {
|
|
|
|
|
return `REC${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
router.use(authMiddleware);
|
|
|
|
|
|
|
|
|
|
router.get('/plans', async (req, res) => {
|
|
|
|
|
try {
|
2026-03-06 14:39:52 +08:00
|
|
|
const { status, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE, keyword } = req.query;
|
2026-02-26 11:24:00 +08:00
|
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
|
const where = {};
|
|
|
|
|
|
|
|
|
|
if (status) {
|
|
|
|
|
where.status = status;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (keyword) {
|
|
|
|
|
where[Op.or] = [
|
|
|
|
|
{ name: { [Op.like]: `%${keyword}%` } },
|
|
|
|
|
{ description: { [Op.like]: `%${keyword}%` } }
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { count, rows } = await InventoryPlan.findAndCountAll({
|
|
|
|
|
where,
|
|
|
|
|
include: [
|
|
|
|
|
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
|
|
|
|
|
],
|
|
|
|
|
order: [['createdAt', 'DESC']],
|
|
|
|
|
limit: parseInt(pageSize),
|
|
|
|
|
offset: parseInt(offset)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
plans: rows,
|
|
|
|
|
total: count,
|
|
|
|
|
page: parseInt(page),
|
|
|
|
|
pageSize: parseInt(pageSize)
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get('/plans/:planId', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const plan = await InventoryPlan.findByPk(req.params.planId, {
|
|
|
|
|
include: [
|
|
|
|
|
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!plan) {
|
|
|
|
|
return res.status(404).json({ error: '盘点计划不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tasks = await InventoryTask.findAll({
|
|
|
|
|
where: { planId: plan.planId },
|
|
|
|
|
include: [
|
|
|
|
|
{ model: require('../models/User'), as: 'Assignee', attributes: ['userId', 'username', 'realName'] }
|
|
|
|
|
],
|
|
|
|
|
order: [['createdAt', 'ASC']]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({ plan, tasks });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post('/plans', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const { name, type, description, scheduledDate, targetRooms, targetRacks } = req.body;
|
|
|
|
|
|
|
|
|
|
const plan = await InventoryPlan.create({
|
|
|
|
|
planId: generatePlanId(),
|
|
|
|
|
name,
|
|
|
|
|
type: type || 'full',
|
|
|
|
|
description,
|
|
|
|
|
scheduledDate: scheduledDate ? new Date(scheduledDate) : null,
|
|
|
|
|
targetRooms: targetRooms || [],
|
|
|
|
|
targetRacks: targetRacks || [],
|
|
|
|
|
status: 'draft',
|
|
|
|
|
createdBy: req.user?.userId
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.status(201).json(plan);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.put('/plans/:planId', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const plan = await InventoryPlan.findByPk(req.params.planId);
|
|
|
|
|
if (!plan) {
|
|
|
|
|
return res.status(404).json({ error: '盘点计划不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { name, type, description, scheduledDate, targetRooms, targetRacks, status } = req.body;
|
|
|
|
|
|
|
|
|
|
await plan.update({
|
|
|
|
|
name: name || plan.name,
|
|
|
|
|
type: type || plan.type,
|
|
|
|
|
description: description !== undefined ? description : plan.description,
|
|
|
|
|
scheduledDate: scheduledDate ? new Date(scheduledDate) : plan.scheduledDate,
|
|
|
|
|
targetRooms: targetRooms || plan.targetRooms,
|
|
|
|
|
targetRacks: targetRacks || plan.targetRacks,
|
|
|
|
|
status: status || plan.status
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json(plan);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.delete('/plans/:planId', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const plan = await InventoryPlan.findByPk(req.params.planId);
|
|
|
|
|
if (!plan) {
|
|
|
|
|
return res.status(404).json({ error: '盘点计划不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await InventoryRecord.destroy({ where: { planId: plan.planId } });
|
|
|
|
|
await InventoryTask.destroy({ where: { planId: plan.planId } });
|
|
|
|
|
await plan.destroy();
|
|
|
|
|
|
|
|
|
|
res.json({ message: '盘点计划删除成功' });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post('/plans/:planId/start', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const plan = await InventoryPlan.findByPk(req.params.planId);
|
|
|
|
|
|
|
|
|
|
if (!plan) {
|
|
|
|
|
return res.status(404).json({ error: '盘点计划不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (plan.status !== 'draft' && plan.status !== 'pending') {
|
|
|
|
|
return res.status(400).json({ error: '只有草稿或待执行状态的盘点计划可以启动' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const targetRooms = plan.targetRooms || [];
|
|
|
|
|
const targetRacks = plan.targetRacks || [];
|
|
|
|
|
let allDevices = [];
|
|
|
|
|
|
|
|
|
|
if (targetRacks.length > 0) {
|
|
|
|
|
allDevices = await Device.findAll({
|
|
|
|
|
where: { rackId: { [Op.in]: targetRacks } }
|
|
|
|
|
});
|
|
|
|
|
} else if (targetRooms.length > 0) {
|
|
|
|
|
const racksInRooms = await Rack.findAll({
|
|
|
|
|
where: { roomId: { [Op.in]: targetRooms } },
|
|
|
|
|
attributes: ['rackId']
|
|
|
|
|
});
|
|
|
|
|
const rackIds = racksInRooms.map(r => r.rackId);
|
|
|
|
|
allDevices = await Device.findAll({
|
|
|
|
|
where: { rackId: { [Op.in]: rackIds } }
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
allDevices = await Device.findAll();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const tasksToCreate = [];
|
|
|
|
|
const recordsToCreate = [];
|
|
|
|
|
|
|
|
|
|
const taskId = generateTaskId();
|
|
|
|
|
|
|
|
|
|
tasksToCreate.push({
|
|
|
|
|
taskId,
|
|
|
|
|
planId: plan.planId,
|
|
|
|
|
targetType: 'all',
|
|
|
|
|
targetId: 'all',
|
|
|
|
|
targetName: '全部设备',
|
|
|
|
|
status: 'pending',
|
|
|
|
|
totalDevices: allDevices.length
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < allDevices.length; i++) {
|
|
|
|
|
const device = allDevices[i];
|
|
|
|
|
recordsToCreate.push({
|
|
|
|
|
recordId: generateRecordId(),
|
|
|
|
|
taskId,
|
|
|
|
|
planId: plan.planId,
|
|
|
|
|
deviceId: device.deviceId,
|
|
|
|
|
deviceName: device.name,
|
|
|
|
|
deviceType: device.type,
|
|
|
|
|
serialNumber: device.serialNumber,
|
|
|
|
|
rackId: device.rackId,
|
|
|
|
|
position: device.position,
|
|
|
|
|
status: 'pending'
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (tasksToCreate.length > 0) {
|
|
|
|
|
await InventoryTask.bulkCreate(tasksToCreate, { individualHooks: false });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (recordsToCreate.length > 0) {
|
2026-02-26 11:59:19 +08:00
|
|
|
const now = dbDialect === 'mysql'
|
|
|
|
|
? new Date().toISOString().replace('T', ' ').replace('Z', '')
|
|
|
|
|
: new Date().toISOString();
|
2026-02-26 11:24:00 +08:00
|
|
|
const placeholders = recordsToCreate.map(r =>
|
|
|
|
|
`('${r.recordId}', '${r.taskId}', '${r.planId}', '${r.deviceId}', '${r.deviceName}', '${r.deviceType}', '${r.serialNumber || ''}', '${r.rackId}', ${r.position}, 'pending', '${now}', '${now}')`
|
|
|
|
|
).join(',');
|
|
|
|
|
|
|
|
|
|
if (placeholders) {
|
|
|
|
|
await sequelize.query(`
|
|
|
|
|
INSERT INTO inventory_records (recordId, taskId, planId, deviceId, deviceName, deviceType, serialNumber, rackId, position, status, createdAt, updatedAt)
|
|
|
|
|
VALUES ${placeholders}
|
|
|
|
|
`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await plan.update({
|
|
|
|
|
status: 'in_progress',
|
|
|
|
|
totalDevices: allDevices.length,
|
|
|
|
|
checkedDevices: 0,
|
|
|
|
|
normalDevices: 0,
|
|
|
|
|
abnormalDevices: 0,
|
|
|
|
|
missedDevices: allDevices.length
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({ message: '盘点任务已启动', taskCount: tasksToCreate.length, deviceCount: allDevices.length });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('启动盘点错误:', error);
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get('/tasks/:taskId', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const task = await InventoryTask.findByPk(req.params.taskId, {
|
|
|
|
|
include: [
|
|
|
|
|
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
|
|
|
|
{ model: require('../models/User'), as: 'Assignee', attributes: ['userId', 'username', 'realName'] }
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!task) {
|
|
|
|
|
return res.status(404).json({ error: '盘点任务不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const records = await InventoryRecord.findAll({
|
|
|
|
|
where: { taskId: task.taskId },
|
|
|
|
|
include: [
|
|
|
|
|
{ model: Device, as: 'Device', attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'] },
|
|
|
|
|
{ model: require('../models/User'), as: 'Checker', attributes: ['userId', 'username', 'realName'] }
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const rackIds = [...new Set(records.map(r => r.rackId).filter(Boolean))];
|
|
|
|
|
const racks = await Rack.findAll({
|
|
|
|
|
where: { rackId: rackIds },
|
|
|
|
|
include: [{ model: Room, as: 'Room' }]
|
|
|
|
|
});
|
|
|
|
|
const rackMap = {};
|
|
|
|
|
racks.forEach(r => {
|
|
|
|
|
rackMap[r.rackId] = r;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const enrichedRecords = records.map(record => {
|
|
|
|
|
const rackInfo = rackMap[record.rackId];
|
|
|
|
|
const roomName = rackInfo?.Room?.name || '';
|
|
|
|
|
const rackName = rackInfo?.name || record.rackId || '';
|
|
|
|
|
const position = record.position || '';
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...record.toJSON(),
|
|
|
|
|
displayLocation: roomName ? `${roomName} - ${rackName} - U${position}` : `${rackName} - U${position}`
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({ task, records: enrichedRecords });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取盘点任务记录错误:', error);
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.put('/tasks/:taskId', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const task = await InventoryTask.findByPk(req.params.taskId);
|
|
|
|
|
if (!task) {
|
|
|
|
|
return res.status(404).json({ error: '盘点任务不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { assignedTo, status } = req.body;
|
|
|
|
|
|
|
|
|
|
if (assignedTo !== undefined) {
|
|
|
|
|
await task.update({
|
|
|
|
|
assignedTo,
|
|
|
|
|
assignedAt: assignedTo ? new Date() : task.assignedAt
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (status) {
|
|
|
|
|
await task.update({
|
|
|
|
|
status,
|
|
|
|
|
completedAt: status === 'completed' ? new Date() : null
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json(task);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post('/records/:recordId/check', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const record = await InventoryRecord.findByPk(req.params.recordId, {
|
|
|
|
|
include: [{ model: Device, as: 'Device' }]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!record) {
|
|
|
|
|
return res.status(404).json({ error: '盘点记录不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { actualSerialNumber, actualRackId, actualPosition, status, remark, photoUrl } = req.body;
|
|
|
|
|
|
|
|
|
|
let abnormalType = null;
|
|
|
|
|
if (status === 'abnormal') {
|
|
|
|
|
if (actualSerialNumber && actualSerialNumber !== record.serialNumber) {
|
|
|
|
|
abnormalType = 'serial_mismatch';
|
|
|
|
|
} else if (actualRackId && actualRackId !== record.rackId) {
|
|
|
|
|
abnormalType = 'position_mismatch';
|
|
|
|
|
} else if (status === 'not_found') {
|
|
|
|
|
abnormalType = 'device_missing';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await record.update({
|
|
|
|
|
actualSerialNumber: actualSerialNumber || null,
|
|
|
|
|
actualRackId: actualRackId || null,
|
|
|
|
|
actualPosition: actualPosition || null,
|
|
|
|
|
status: status || record.status,
|
|
|
|
|
abnormalType,
|
|
|
|
|
checkedBy: req.user?.userId,
|
|
|
|
|
checkedAt: new Date(),
|
|
|
|
|
remark: remark || null,
|
|
|
|
|
photoUrl: photoUrl || null
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const task = await InventoryTask.findByPk(record.taskId);
|
|
|
|
|
const plan = await InventoryPlan.findByPk(record.planId);
|
|
|
|
|
|
|
|
|
|
const taskRecords = await InventoryRecord.findAll({ where: { taskId: task.taskId } });
|
|
|
|
|
const taskStats = {
|
|
|
|
|
totalDevices: taskRecords.length,
|
|
|
|
|
checkedDevices: taskRecords.filter(r => r.status !== 'pending').length,
|
|
|
|
|
normalDevices: taskRecords.filter(r => r.status === 'normal').length,
|
|
|
|
|
abnormalDevices: taskRecords.filter(r => r.status === 'abnormal').length
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await task.update(taskStats);
|
|
|
|
|
|
|
|
|
|
const planRecords = await InventoryRecord.findAll({ where: { planId: plan.planId } });
|
|
|
|
|
const planStats = {
|
|
|
|
|
totalDevices: planRecords.length,
|
|
|
|
|
checkedDevices: planRecords.filter(r => r.status !== 'pending').length,
|
|
|
|
|
normalDevices: planRecords.filter(r => r.status === 'normal').length,
|
|
|
|
|
abnormalDevices: planRecords.filter(r => r.status === 'abnormal').length,
|
|
|
|
|
missedDevices: planRecords.filter(r => r.status === 'pending').length
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
await plan.update(planStats);
|
|
|
|
|
|
|
|
|
|
res.json({ record, taskStats, planStats });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get('/records', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const { planId, taskId, status, page = 1, pageSize = 20 } = req.query;
|
|
|
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
|
const where = {};
|
|
|
|
|
|
|
|
|
|
if (planId) where.planId = planId;
|
|
|
|
|
if (taskId) where.taskId = taskId;
|
|
|
|
|
if (status) where.status = status;
|
|
|
|
|
|
|
|
|
|
const { count, rows } = await InventoryRecord.findAndCountAll({
|
|
|
|
|
where,
|
|
|
|
|
include: [
|
|
|
|
|
{ model: Device, as: 'Device', attributes: ['deviceId', 'name', 'type', 'serialNumber', 'rackId', 'position'] },
|
|
|
|
|
{ model: require('../models/User'), as: 'Checker', attributes: ['userId', 'username', 'realName'] }
|
|
|
|
|
],
|
|
|
|
|
order: [['checkedAt', 'DESC'], ['createdAt', 'DESC']],
|
|
|
|
|
limit: parseInt(pageSize),
|
|
|
|
|
offset: parseInt(offset)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
records: rows,
|
|
|
|
|
total: count,
|
|
|
|
|
page: parseInt(page),
|
|
|
|
|
pageSize: parseInt(pageSize)
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post('/plans/:planId/complete', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const plan = await InventoryPlan.findByPk(req.params.planId);
|
|
|
|
|
|
|
|
|
|
if (!plan) {
|
|
|
|
|
return res.status(404).json({ error: '盘点计划不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await InventoryRecord.update(
|
|
|
|
|
{ status: 'missed' },
|
|
|
|
|
{ where: { planId: plan.planId, status: 'pending' } }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const finalRecords = await InventoryRecord.findAll({ where: { planId: plan.planId } });
|
|
|
|
|
|
|
|
|
|
await plan.update({
|
|
|
|
|
status: 'completed',
|
|
|
|
|
completedDate: new Date(),
|
|
|
|
|
checkedDevices: finalRecords.filter(r => r.status !== 'pending').length,
|
|
|
|
|
normalDevices: finalRecords.filter(r => r.status === 'normal').length,
|
|
|
|
|
abnormalDevices: finalRecords.filter(r => r.status === 'abnormal').length,
|
|
|
|
|
missedDevices: finalRecords.filter(r => r.status === 'missed').length
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await InventoryTask.update(
|
|
|
|
|
{ status: 'completed' },
|
|
|
|
|
{ where: { planId: plan.planId, status: { [Op.ne]: 'completed' } } }
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
res.json({ message: '盘点已完成', plan });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get('/stats/dashboard', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const totalPlans = await InventoryPlan.count();
|
|
|
|
|
const completedPlans = await InventoryPlan.count({ where: { status: 'completed' } });
|
|
|
|
|
const inProgressPlans = await InventoryPlan.count({ where: { status: 'in_progress' } });
|
|
|
|
|
|
|
|
|
|
const totalRecords = await InventoryRecord.count();
|
|
|
|
|
const normalRecords = await InventoryRecord.count({ where: { status: 'normal' } });
|
|
|
|
|
const abnormalRecords = await InventoryRecord.count({ where: { status: 'abnormal' } });
|
|
|
|
|
const pendingRecords = await InventoryRecord.count({ where: { status: 'pending' } });
|
|
|
|
|
|
|
|
|
|
const recentPlans = await InventoryPlan.findAll({
|
|
|
|
|
limit: 5,
|
|
|
|
|
order: [['createdAt', 'DESC']],
|
|
|
|
|
include: [
|
|
|
|
|
{ model: require('../models/User'), as: 'Creator', attributes: ['userId', 'username', 'realName'] }
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
totalPlans,
|
|
|
|
|
completedPlans,
|
|
|
|
|
inProgressPlans,
|
|
|
|
|
totalRecords,
|
|
|
|
|
normalRecords,
|
|
|
|
|
abnormalRecords,
|
|
|
|
|
pendingRecords,
|
|
|
|
|
completionRate: totalRecords > 0 ? ((normalRecords + abnormalRecords) / totalRecords * 100).toFixed(1) : 0,
|
|
|
|
|
abnormalRate: totalRecords > 0 ? (abnormalRecords / totalRecords * 100).toFixed(1) : 0,
|
|
|
|
|
recentPlans
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-06 15:41:07 +08:00
|
|
|
router.post('/quick-add-device', async (req, res) => {
|
|
|
|
|
try {
|
2026-03-10 09:20:31 +08:00
|
|
|
const {
|
|
|
|
|
taskId,
|
|
|
|
|
planId,
|
|
|
|
|
serialNumber,
|
|
|
|
|
SN,
|
|
|
|
|
deviceName,
|
|
|
|
|
name,
|
|
|
|
|
deviceType,
|
|
|
|
|
type,
|
|
|
|
|
roomId,
|
|
|
|
|
rackId,
|
|
|
|
|
position,
|
|
|
|
|
model,
|
|
|
|
|
brand,
|
|
|
|
|
height,
|
|
|
|
|
powerConsumption,
|
|
|
|
|
ipAddress,
|
|
|
|
|
purchaseDate,
|
|
|
|
|
warrantyExpiry,
|
|
|
|
|
description,
|
|
|
|
|
remark,
|
|
|
|
|
...restFields
|
|
|
|
|
} = req.body;
|
2026-03-06 15:41:07 +08:00
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
// 字段名映射:优先使用数据库字段名,其次使用默认字段名
|
|
|
|
|
const finalSerialNumber = serialNumber || SN;
|
|
|
|
|
const finalDeviceName = name || deviceName;
|
|
|
|
|
const finalDeviceType = type || deviceType;
|
2026-03-06 15:41:07 +08:00
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
if (!planId || !finalSerialNumber) {
|
|
|
|
|
return res.status(400).json({ error: '缺少必要参数:planId, serialNumber' });
|
2026-03-06 15:41:07 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const plan = await InventoryPlan.findByPk(planId);
|
|
|
|
|
if (!plan) {
|
|
|
|
|
return res.status(404).json({ error: '盘点计划不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
const existingDevice = await Device.findOne({ where: { serialNumber: finalSerialNumber } });
|
2026-03-06 15:41:07 +08:00
|
|
|
if (existingDevice) {
|
2026-03-10 09:20:31 +08:00
|
|
|
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中', deviceId: existingDevice.deviceId });
|
2026-03-06 15:41:07 +08:00
|
|
|
}
|
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
const existingPending = await PendingDevice.findOne({
|
|
|
|
|
where: { serialNumber: finalSerialNumber, status: 'pending' }
|
2026-03-06 15:41:07 +08:00
|
|
|
});
|
2026-03-10 09:20:31 +08:00
|
|
|
if (existingPending) {
|
|
|
|
|
return res.status(400).json({ error: '该序列号的设备已在暂存列表中', pendingId: existingPending.pendingId });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pendingId = `PEND${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
|
|
|
|
|
|
|
|
|
|
// 只有当用户没有填写设备名称时,才使用默认名称
|
|
|
|
|
const finalName = finalDeviceName && finalDeviceName.trim() !== ''
|
|
|
|
|
? finalDeviceName.trim()
|
|
|
|
|
: `新设备-${finalSerialNumber.slice(-6)}`;
|
|
|
|
|
|
|
|
|
|
const pendingDevice = await PendingDevice.create({
|
|
|
|
|
pendingId,
|
|
|
|
|
serialNumber: finalSerialNumber,
|
|
|
|
|
deviceName: finalName,
|
|
|
|
|
deviceType: finalDeviceType || 'other',
|
|
|
|
|
roomId: roomId || null,
|
|
|
|
|
rackId: rackId || null,
|
|
|
|
|
position: position || null,
|
|
|
|
|
model: model || null,
|
|
|
|
|
brand: brand || null,
|
|
|
|
|
height: height || 1,
|
|
|
|
|
powerConsumption: powerConsumption || 0,
|
|
|
|
|
ipAddress: ipAddress || null,
|
|
|
|
|
purchaseDate: purchaseDate ? new Date(purchaseDate) : null,
|
|
|
|
|
warrantyExpiry: warrantyExpiry ? new Date(warrantyExpiry) : null,
|
|
|
|
|
description: description || null,
|
|
|
|
|
customFields: Object.keys(restFields).length > 0 ? restFields : null,
|
|
|
|
|
planId,
|
|
|
|
|
taskId: taskId || null,
|
|
|
|
|
createdBy: req.user?.userId,
|
|
|
|
|
status: 'pending',
|
|
|
|
|
remark: remark || '盘点时快速添加'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.status(201).json({
|
|
|
|
|
message: '设备已暂存,请前往暂存设备页面完善信息后同步',
|
|
|
|
|
pendingDevice
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('快速添加设备错误:', error);
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get('/pending-devices', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const { status, planId, roomId, keyword, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE } = req.query;
|
|
|
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
|
const where = {};
|
|
|
|
|
|
|
|
|
|
if (status) {
|
|
|
|
|
where.status = status;
|
|
|
|
|
}
|
|
|
|
|
if (planId) {
|
|
|
|
|
where.planId = planId;
|
|
|
|
|
}
|
|
|
|
|
if (roomId) {
|
|
|
|
|
where.roomId = roomId;
|
|
|
|
|
}
|
|
|
|
|
if (keyword) {
|
|
|
|
|
where[Op.or] = [
|
|
|
|
|
{ serialNumber: { [Op.like]: `%${keyword}%` } },
|
|
|
|
|
{ deviceName: { [Op.like]: `%${keyword}%` } }
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { count, rows } = await PendingDevice.findAndCountAll({
|
|
|
|
|
where,
|
|
|
|
|
include: [
|
|
|
|
|
{ model: User, as: 'Creator', attributes: ['userId', 'username', 'realName'] },
|
|
|
|
|
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
|
|
|
|
|
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
|
|
|
|
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
|
|
|
|
|
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] }
|
|
|
|
|
],
|
|
|
|
|
order: [['createdAt', 'DESC']],
|
|
|
|
|
limit: parseInt(pageSize),
|
|
|
|
|
offset: parseInt(offset)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
pendingDevices: rows,
|
|
|
|
|
total: count,
|
|
|
|
|
page: parseInt(page),
|
|
|
|
|
pageSize: parseInt(pageSize)
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取暂存设备列表错误:', error);
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get('/pending-devices/stats', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const total = await PendingDevice.count();
|
|
|
|
|
const pending = await PendingDevice.count({ where: { status: 'pending' } });
|
|
|
|
|
const synced = await PendingDevice.count({ where: { status: 'synced' } });
|
|
|
|
|
|
|
|
|
|
res.json({ total, pending, synced });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.get('/pending-devices/:pendingId', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const pendingDevice = await PendingDevice.findByPk(req.params.pendingId, {
|
|
|
|
|
include: [
|
|
|
|
|
{ model: User, as: 'Creator', attributes: ['userId', 'username', 'realName'] },
|
|
|
|
|
{ model: User, as: 'Syncer', attributes: ['userId', 'username', 'realName'] },
|
|
|
|
|
{ model: InventoryPlan, as: 'Plan', attributes: ['planId', 'name'] },
|
|
|
|
|
{ model: Room, as: 'Room', attributes: ['roomId', 'name'] },
|
|
|
|
|
{ model: Rack, as: 'Rack', attributes: ['rackId', 'name'] }
|
|
|
|
|
]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!pendingDevice) {
|
|
|
|
|
return res.status(404).json({ error: '暂存设备不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json(pendingDevice);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.put('/pending-devices/:pendingId', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const pendingDevice = await PendingDevice.findByPk(req.params.pendingId);
|
|
|
|
|
if (!pendingDevice) {
|
|
|
|
|
return res.status(404).json({ error: '暂存设备不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pendingDevice.status === 'synced') {
|
|
|
|
|
return res.status(400).json({ error: '已同步的设备无法修改' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { deviceName, deviceType, roomId, rackId, position, model, brand, height, powerConsumption, ipAddress, purchaseDate, warrantyExpiry, description, remark, ...restFields } = req.body;
|
|
|
|
|
|
|
|
|
|
const updateData = {
|
|
|
|
|
deviceName: deviceName !== undefined ? deviceName : pendingDevice.deviceName,
|
|
|
|
|
deviceType: deviceType !== undefined ? deviceType : pendingDevice.deviceType,
|
|
|
|
|
roomId: roomId !== undefined ? roomId : pendingDevice.roomId,
|
|
|
|
|
rackId: rackId !== undefined ? rackId : pendingDevice.rackId,
|
|
|
|
|
position: position !== undefined ? position : pendingDevice.position,
|
|
|
|
|
model: model !== undefined ? model : pendingDevice.model,
|
|
|
|
|
brand: brand !== undefined ? brand : pendingDevice.brand,
|
|
|
|
|
height: height !== undefined ? height : pendingDevice.height,
|
|
|
|
|
powerConsumption: powerConsumption !== undefined ? powerConsumption : pendingDevice.powerConsumption,
|
|
|
|
|
ipAddress: ipAddress !== undefined ? ipAddress : pendingDevice.ipAddress,
|
|
|
|
|
purchaseDate: purchaseDate !== undefined ? (purchaseDate ? new Date(purchaseDate) : null) : pendingDevice.purchaseDate,
|
|
|
|
|
warrantyExpiry: warrantyExpiry !== undefined ? (warrantyExpiry ? new Date(warrantyExpiry) : null) : pendingDevice.warrantyExpiry,
|
|
|
|
|
description: description !== undefined ? description : pendingDevice.description,
|
|
|
|
|
remark: remark !== undefined ? remark : pendingDevice.remark
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (Object.keys(restFields).length > 0) {
|
|
|
|
|
const existingCustomFields = pendingDevice.customFields || {};
|
|
|
|
|
updateData.customFields = { ...existingCustomFields, ...restFields };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await pendingDevice.update(updateData);
|
|
|
|
|
|
|
|
|
|
res.json(pendingDevice);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.delete('/pending-devices/:pendingId', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const pendingDevice = await PendingDevice.findByPk(req.params.pendingId);
|
|
|
|
|
if (!pendingDevice) {
|
|
|
|
|
return res.status(404).json({ error: '暂存设备不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await pendingDevice.destroy();
|
|
|
|
|
res.json({ message: '删除成功' });
|
|
|
|
|
} catch (error) {
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post('/pending-devices/:pendingId/sync', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const pendingDevice = await PendingDevice.findByPk(req.params.pendingId);
|
|
|
|
|
if (!pendingDevice) {
|
|
|
|
|
return res.status(404).json({ error: '暂存设备不存在' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (pendingDevice.status === 'synced') {
|
|
|
|
|
return res.status(400).json({ error: '该设备已同步' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const existingDevice = await Device.findOne({ where: { serialNumber: pendingDevice.serialNumber } });
|
|
|
|
|
if (existingDevice) {
|
|
|
|
|
return res.status(400).json({ error: '该序列号的设备已存在于设备管理中' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const devices = await Device.findAll({ where: { deviceId: { [Op.like]: 'DEV%' } } });
|
2026-03-06 15:41:07 +08:00
|
|
|
let maxNumber = 0;
|
|
|
|
|
devices.forEach(device => {
|
|
|
|
|
const match = device.deviceId.match(/^DEV(\d+)$/);
|
|
|
|
|
if (match) {
|
|
|
|
|
const num = parseInt(match[1], 10);
|
|
|
|
|
if (num > maxNumber) maxNumber = num;
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-03-10 09:20:31 +08:00
|
|
|
const deviceId = `DEV${String(maxNumber + 1).padStart(3, '0')}`;
|
2026-03-06 15:41:07 +08:00
|
|
|
|
|
|
|
|
const newDevice = await Device.create({
|
|
|
|
|
deviceId,
|
2026-03-10 09:20:31 +08:00
|
|
|
name: pendingDevice.deviceName,
|
|
|
|
|
type: pendingDevice.deviceType,
|
|
|
|
|
serialNumber: pendingDevice.serialNumber,
|
|
|
|
|
rackId: pendingDevice.rackId,
|
|
|
|
|
position: pendingDevice.position,
|
|
|
|
|
height: pendingDevice.height || 1,
|
|
|
|
|
powerConsumption: pendingDevice.powerConsumption || 0,
|
|
|
|
|
model: pendingDevice.model,
|
|
|
|
|
ipAddress: pendingDevice.ipAddress,
|
|
|
|
|
description: pendingDevice.description,
|
|
|
|
|
purchaseDate: pendingDevice.purchaseDate,
|
|
|
|
|
warrantyExpiry: pendingDevice.warrantyExpiry,
|
|
|
|
|
customFields: pendingDevice.customFields,
|
2026-03-06 15:41:07 +08:00
|
|
|
status: 'running'
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
await pendingDevice.update({
|
|
|
|
|
status: 'synced',
|
|
|
|
|
syncedAt: new Date(),
|
|
|
|
|
syncedBy: req.user?.userId,
|
|
|
|
|
syncedDeviceId: newDevice.deviceId
|
2026-03-06 15:41:07 +08:00
|
|
|
});
|
|
|
|
|
|
2026-03-10 09:20:31 +08:00
|
|
|
res.json({
|
|
|
|
|
message: '同步成功',
|
2026-03-06 15:41:07 +08:00
|
|
|
device: newDevice,
|
2026-03-10 09:20:31 +08:00
|
|
|
pendingDevice
|
2026-03-06 15:41:07 +08:00
|
|
|
});
|
|
|
|
|
} catch (error) {
|
2026-03-10 09:20:31 +08:00
|
|
|
console.error('同步设备错误:', error);
|
|
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
router.post('/pending-devices/batch-sync', async (req, res) => {
|
|
|
|
|
try {
|
|
|
|
|
const { pendingIds } = req.body;
|
|
|
|
|
if (!pendingIds || pendingIds.length === 0) {
|
|
|
|
|
return res.status(400).json({ error: '请选择要同步的设备' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pendingDevices = await PendingDevice.findAll({
|
|
|
|
|
where: {
|
|
|
|
|
pendingId: { [Op.in]: pendingIds },
|
|
|
|
|
status: 'pending'
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (pendingDevices.length === 0) {
|
|
|
|
|
return res.status(400).json({ error: '没有可同步的设备' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const devices = await Device.findAll({ where: { deviceId: { [Op.like]: 'DEV%' } } });
|
|
|
|
|
let maxNumber = 0;
|
|
|
|
|
devices.forEach(device => {
|
|
|
|
|
const match = device.deviceId.match(/^DEV(\d+)$/);
|
|
|
|
|
if (match) {
|
|
|
|
|
const num = parseInt(match[1], 10);
|
|
|
|
|
if (num > maxNumber) maxNumber = num;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const results = [];
|
|
|
|
|
const errors = [];
|
|
|
|
|
|
|
|
|
|
for (const pending of pendingDevices) {
|
|
|
|
|
try {
|
|
|
|
|
const existingDevice = await Device.findOne({ where: { serialNumber: pending.serialNumber } });
|
|
|
|
|
if (existingDevice) {
|
|
|
|
|
errors.push({ pendingId: pending.pendingId, serialNumber: pending.serialNumber, error: '序列号已存在' });
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
maxNumber++;
|
|
|
|
|
const deviceId = `DEV${String(maxNumber).padStart(3, '0')}`;
|
|
|
|
|
|
|
|
|
|
const newDevice = await Device.create({
|
|
|
|
|
deviceId,
|
|
|
|
|
name: pending.deviceName,
|
|
|
|
|
type: pending.deviceType,
|
|
|
|
|
serialNumber: pending.serialNumber,
|
|
|
|
|
rackId: pending.rackId,
|
|
|
|
|
position: pending.position,
|
|
|
|
|
height: pending.height || 1,
|
|
|
|
|
powerConsumption: pending.powerConsumption || 0,
|
|
|
|
|
model: pending.model,
|
|
|
|
|
ipAddress: pending.ipAddress,
|
|
|
|
|
description: pending.description,
|
|
|
|
|
purchaseDate: pending.purchaseDate,
|
|
|
|
|
warrantyExpiry: pending.warrantyExpiry,
|
|
|
|
|
customFields: pending.customFields,
|
|
|
|
|
status: 'running'
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await pending.update({
|
|
|
|
|
status: 'synced',
|
|
|
|
|
syncedAt: new Date(),
|
|
|
|
|
syncedBy: req.user?.userId,
|
|
|
|
|
syncedDeviceId: newDevice.deviceId
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
results.push({ pendingId: pending.pendingId, deviceId: newDevice.deviceId });
|
|
|
|
|
} catch (err) {
|
|
|
|
|
errors.push({ pendingId: pending.pendingId, error: err.message });
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res.json({
|
|
|
|
|
message: `成功同步 ${results.length} 台设备`,
|
|
|
|
|
successCount: results.length,
|
|
|
|
|
errorCount: errors.length,
|
|
|
|
|
results,
|
|
|
|
|
errors
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('批量同步设备错误:', error);
|
2026-03-06 15:41:07 +08:00
|
|
|
res.status(500).json({ error: error.message });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-26 11:24:00 +08:00
|
|
|
module.exports = router;
|