feat: 实现ID生成器并重构模型ID生成逻辑

refactor: 统一ID生成方式,使用新的idGenerator模块
refactor: 重构模型ID生成逻辑,允许空ID并在创建前自动生成
fix(roomSchema): 使机房ID字段变为可选并更新前端表单验证
style: 格式化代码并优化导入语句
test: 添加操作日志集成测试文件
docs: 添加错误处理模块文档
This commit is contained in:
zhang1106
2026-04-02 10:34:13 +08:00
parent ea67951dd4
commit 7f1df28858
28 changed files with 451 additions and 91 deletions
+2 -5
View File
@@ -11,13 +11,10 @@ const {
USERNAME_MIN_LENGTH,
USERNAME_MAX_LENGTH,
} = require('../config');
const { generateId } = require('../utils/idGenerator');
const router = express.Router();
const generateId = () => {
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
};
router.post('/register', async (req, res) => {
try {
const { username, password, email, phone, realName } = req.body;
@@ -57,7 +54,7 @@ router.post('/register', async (req, res) => {
const isFirstUser = userCount === 0;
const user = await User.create({
userId: generateId(),
userId: generateId({ prefix: 'USR' }),
username,
password: hashedPassword,
email,
+33 -11
View File
@@ -5,6 +5,7 @@ const { sequelize } = require('../db');
const Consumable = require('../models/Consumable');
const ConsumableLog = require('../models/ConsumableLog');
const { importJobManager } = require('../utils/importJobManager');
const { generateId } = require('../utils/idGenerator');
const SUPPORTED_FIELDS = [
'consumableId',
@@ -54,7 +55,10 @@ const parseSnList = snStr => {
if (!snStr) return [];
if (Array.isArray(snStr)) return snStr;
if (typeof snStr === 'string') {
return snStr.split(/[,;\n]/).map(s => s.trim()).filter(Boolean);
return snStr
.split(/[,;\n]/)
.map(s => s.trim())
.filter(Boolean);
}
return [];
};
@@ -108,8 +112,7 @@ router.post('/consumables/background', async (req, res) => {
}
}
const consumableId =
mappedItem.consumableId || mappedItem.name + `_${Date.now()}`;
const consumableId = mappedItem.consumableId || generateId({ prefix: 'CON' });
const name = mappedItem.name;
const category = mappedItem.category;
@@ -128,9 +131,7 @@ router.post('/consumables/background', async (req, res) => {
category,
unit: mappedItem.unit || '个',
currentStock:
snList.length > 0
? snList.length
: parseInt(mappedItem.currentStock) || 0,
snList.length > 0 ? snList.length : parseInt(mappedItem.currentStock) || 0,
minStock: parseInt(mappedItem.minStock) || 10,
maxStock: parseInt(mappedItem.maxStock) || 0,
unitPrice: parseFloat(mappedItem.unitPrice) || 0,
@@ -210,7 +211,13 @@ router.post('/consumables/background', async (req, res) => {
{ transaction }
);
importJobManager.incrementProgress(jobId, existingConsumable && mode === 'update' ? 0 : 1, 0, 0, existingConsumable ? 1 : 0);
importJobManager.incrementProgress(
jobId,
existingConsumable && mode === 'update' ? 0 : 1,
0,
0,
existingConsumable ? 1 : 0
);
} catch (error) {
results.failed++;
results.errors.push(`${rowNumber} 行: ${error.message}`);
@@ -294,13 +301,28 @@ router.get('/consumables/field-mappings', async (req, res) => {
{ source: '单位', target: 'unit', required: false, description: '计量单位,默认"个"' },
{ source: '当前库存', target: 'currentStock', required: false, description: '当前库存数量' },
{ source: '最小库存', target: 'minStock', required: false, description: '安全库存预警值' },
{ source: '最大库存', target: 'maxStock', required: false, description: '最大库存限制,0表示无限制' },
{
source: '最大库存',
target: 'maxStock',
required: false,
description: '最大库存限制,0表示无限制',
},
{ source: '单价', target: 'unitPrice', required: false, description: '耗材单价' },
{ source: '供应商', target: 'supplier', required: false, description: '供应商名称' },
{ source: '存放位置', target: 'location', required: false, description: '仓库内存放位置' },
{ source: '描述', target: 'description', required: false, description: '耗材详细描述' },
{ source: 'SN序列号', target: 'snList', required: false, description: '序列号列表,用逗号分隔' },
{ source: '状态', target: 'status', required: false, description: '状态:active启用,inactive停用' },
{
source: 'SN序列号',
target: 'snList',
required: false,
description: '序列号列表,用逗号分隔',
},
{
source: '状态',
target: 'status',
required: false,
description: '状态:active启用,inactive停用',
},
];
const systemFields = [
@@ -325,4 +347,4 @@ router.get('/consumables/field-mappings', async (req, res) => {
});
});
module.exports = router;
module.exports = router;
+1 -10
View File
@@ -471,16 +471,7 @@ router.post('/quick-inout', async (req, res) => {
while (attempt < RETRY.MAX_RETRIES) {
const transaction = await sequelize.transaction();
try {
const {
consumableId,
type,
quantity,
operator,
reason,
notes,
snList,
deviceId,
} = req.body;
const { consumableId, type, quantity, operator, reason, notes, snList, deviceId } = req.body;
const consumable = await Consumable.findByPk(consumableId, { transaction });
if (!consumable) {
+4 -1
View File
@@ -171,7 +171,10 @@ router.post('/batch', async (req, res) => {
let device;
if (portData.deviceSn) {
device = await Device.findOne({ where: { serialNumber: portData.deviceSn }, transaction });
device = await Device.findOne({
where: { serialNumber: portData.deviceSn },
transaction,
});
if (!device) {
throw new Error(`设备SN ${portData.deviceSn} 不存在`);
}
+2 -2
View File
@@ -728,12 +728,12 @@ async function generateDeviceId() {
},
},
attributes: [
[sequelize.fn('MAX', sequelize.literal("CAST(SUBSTR(deviceId, 4) AS INTEGER)")), 'maxNum'],
[sequelize.fn('MAX', sequelize.literal('CAST(SUBSTR(deviceId, 4) AS INTEGER)')), 'maxNum'],
],
raw: true,
});
const maxNumber = (result && result.maxNum) ? parseInt(result.maxNum, 10) : 0;
const maxNumber = result && result.maxNum ? parseInt(result.maxNum, 10) : 0;
const newNumber = maxNumber + 1;
return `DEV${String(newNumber).padStart(3, '0')}`;
}
+4 -7
View File
@@ -12,6 +12,7 @@ const User = require('../models/User');
const PendingDevice = require('../models/PendingDevice');
const { authMiddleware, authorize } = require('../middleware/auth');
const { PAGINATION } = require('../config');
const { generateId } = require('../utils/idGenerator');
InventoryTask.belongsTo(InventoryPlan, { foreignKey: 'planId', as: 'Plan' });
InventoryPlan.hasMany(InventoryTask, { foreignKey: 'planId', as: 'Tasks' });
@@ -23,20 +24,16 @@ 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()}`;
return generateId({ prefix: 'PLAN' });
}
function generateTaskId() {
return `TASK${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
return generateId({ prefix: 'TASK' });
}
function generateRecordId() {
return `REC${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 6).toUpperCase()}`;
return generateId({ prefix: 'REC' });
}
router.use(authMiddleware);
+4 -1
View File
@@ -281,7 +281,10 @@ router.post('/batch', async (req, res) => {
let device;
if (cardData.deviceSn) {
device = await Device.findOne({ where: { serialNumber: cardData.deviceSn }, transaction });
device = await Device.findOne({
where: { serialNumber: cardData.deviceSn },
transaction,
});
if (!device) {
throw new Error(`设备SN ${cardData.deviceSn} 不存在`);
}
+2 -5
View File
@@ -5,13 +5,10 @@ const UserRole = require('../models/UserRole');
const User = require('../models/User');
const { authMiddleware } = require('../middleware/auth');
const { logRoleOperation } = require('../utils/operationLogger');
const { generateId } = require('../utils/idGenerator');
const router = express.Router();
const generateId = () => {
return 'role_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
};
const { Op } = require('sequelize');
router.get('/', authMiddleware, async (req, res) => {
@@ -129,7 +126,7 @@ router.post('/', authMiddleware, async (req, res) => {
}
const role = await Role.create({
roleId: generateId(),
roleId: generateId({ prefix: 'ROLE' }),
roleName,
roleCode,
description,
+39 -9
View File
@@ -338,7 +338,15 @@ const TICKET_EXPORT_FIELDS = [
router.get('/export', async (req, res) => {
try {
const { keyword, status, priority, faultCategory, deviceId, format = 'csv', ticketIds } = req.query;
const {
keyword,
status,
priority,
faultCategory,
deviceId,
format = 'csv',
ticketIds,
} = req.query;
const where = {};
@@ -391,9 +399,19 @@ router.get('/export', async (req, res) => {
const priorityMap = { low: '低', medium: '中', high: '高', urgent: '紧急' };
value = priorityMap[value] || value;
} else if (fieldName === 'status') {
const statusMap = { pending: '待处理', in_progress: '处理中', completed: '已完成', closed: '已关闭' };
const statusMap = {
pending: '待处理',
in_progress: '处理中',
completed: '已完成',
closed: '已关闭',
};
value = statusMap[value] || value;
} else if (fieldName === 'expectedCompletionDate' || fieldName === 'completionDate' || fieldName === 'createdAt' || fieldName === 'updatedAt') {
} else if (
fieldName === 'expectedCompletionDate' ||
fieldName === 'completionDate' ||
fieldName === 'createdAt' ||
fieldName === 'updatedAt'
) {
value = value ? new Date(value).toLocaleString('zh-CN') : '';
}
@@ -411,7 +429,8 @@ router.get('/export', async (req, res) => {
});
if (format === 'json') {
return res.setHeader('Content-Type', 'application/json; charset=utf-8')
return res
.setHeader('Content-Type', 'application/json; charset=utf-8')
.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.json`)
.json({ success: true, data: exportData, total: exportData.length });
}
@@ -422,7 +441,10 @@ router.get('/export', async (req, res) => {
XLSX.utils.book_append_sheet(workbook, worksheet, '工单数据');
const xlsxBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'buffer' });
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader(
'Content-Type',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
);
res.setHeader('Content-Disposition', `attachment; filename=tickets_${Date.now()}.xlsx`);
return res.send(xlsxBuffer);
}
@@ -554,7 +576,7 @@ router.post('/', async (req, res) => {
return res.status(400).json({ error: '请选择设备或手动输入设备信息' });
}
const ticketId = `TKT${Date.now().toString(36).toUpperCase()}${Math.random().toString(36).substr(2, 4).toUpperCase()}`;
const ticketId = generateId({ prefix: 'TKT' });
const ticket = await Ticket.create({
ticketId,
@@ -607,9 +629,17 @@ router.put('/:ticketId', async (req, res) => {
// 白名单过滤:只允许更新安全字段,防止覆盖 ticketId/createdAt 等关键字段
const ALLOWED_UPDATE_FIELDS = [
'title', 'description', 'category', 'priority', 'location',
'contactPerson', 'contactPhone', 'contactEmail',
'expectedDate', 'attachments', 'customFields',
'title',
'description',
'category',
'priority',
'location',
'contactPerson',
'contactPhone',
'contactEmail',
'expectedDate',
'attachments',
'customFields',
];
const updateData = {};
ALLOWED_UPDATE_FIELDS.forEach(field => {