refactor: 集中管理配置常量并优化样式一致性
This commit is contained in:
+67
-1
@@ -56,4 +56,70 @@ JWT_SECRET=your-strong-secret-key-minimum-32-characters-change-in-production
|
|||||||
|
|
||||||
# Token 过期时间(格式:数字+单位,如 24h, 2h, 30m)
|
# Token 过期时间(格式:数字+单位,如 24h, 2h, 30m)
|
||||||
# 建议:开发环境 24h,生产环境 2h 或更短
|
# 建议:开发环境 24h,生产环境 2h 或更短
|
||||||
TOKEN_EXPIRY=24h
|
TOKEN_EXPIRY=24h
|
||||||
|
|
||||||
|
# 密码加密强度(bcrypt salt rounds)
|
||||||
|
# 默认值:10,范围:4-12,值越大越安全但计算越慢
|
||||||
|
SALT_ROUNDS=10
|
||||||
|
|
||||||
|
# 登录失败锁定阈值
|
||||||
|
# 连续登录失败超过此次数后锁定账户
|
||||||
|
MAX_LOGIN_ATTEMPTS=5
|
||||||
|
|
||||||
|
# 账户锁定时间(分钟)
|
||||||
|
# 登录失败锁定后的解锁等待时间
|
||||||
|
LOCK_TIME_MINUTES=30
|
||||||
|
|
||||||
|
# 密码最小长度
|
||||||
|
PASSWORD_MIN_LENGTH=6
|
||||||
|
|
||||||
|
# 用户名长度限制
|
||||||
|
USERNAME_MIN_LENGTH=3
|
||||||
|
USERNAME_MAX_LENGTH=50
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# API 配置
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# API 请求超时时间(毫秒)
|
||||||
|
API_TIMEOUT=30000
|
||||||
|
|
||||||
|
# 数据库查询超时时间(毫秒)
|
||||||
|
DB_QUERY_TIMEOUT=30000
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# 分页配置
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# 默认每页条数
|
||||||
|
DEFAULT_PAGE_SIZE=10
|
||||||
|
|
||||||
|
# 最大每页条数
|
||||||
|
MAX_PAGE_SIZE=1000
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# 文件上传配置
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# 最大文件上传大小(MB)
|
||||||
|
MAX_FILE_SIZE_MB=50
|
||||||
|
|
||||||
|
# 最大头像上传大小(MB)
|
||||||
|
MAX_AVATAR_SIZE_MB=5
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# 重试配置
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# 最大重试次数
|
||||||
|
MAX_RETRIES=3
|
||||||
|
|
||||||
|
# 重试延迟(毫秒)
|
||||||
|
RETRY_DELAY=1000
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# 前端配置
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# 前端默认端口
|
||||||
|
FRONTEND_PORT=3000
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
/**
|
||||||
|
* 通用配置常量
|
||||||
|
* 集中管理分页、文件上传、重试等配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
PAGINATION: {
|
||||||
|
DEFAULT_PAGE_SIZE: parseInt(process.env.DEFAULT_PAGE_SIZE, 10) || 10,
|
||||||
|
MAX_PAGE_SIZE: parseInt(process.env.MAX_PAGE_SIZE, 10) || 1000,
|
||||||
|
PAGE_SIZE_OPTIONS: [10, 20, 30, 50, 100],
|
||||||
|
},
|
||||||
|
|
||||||
|
FILE_UPLOAD: {
|
||||||
|
MAX_FILE_SIZE: (parseInt(process.env.MAX_FILE_SIZE_MB, 10) || 50) * 1024 * 1024,
|
||||||
|
MAX_AVATAR_SIZE: (parseInt(process.env.MAX_AVATAR_SIZE_MB, 10) || 5) * 1024 * 1024,
|
||||||
|
ALLOWED_IMAGE_TYPES: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
|
||||||
|
ALLOWED_DOC_TYPES: [
|
||||||
|
'application/pdf',
|
||||||
|
'application/msword',
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||||
|
'application/vnd.ms-excel',
|
||||||
|
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
RETRY: {
|
||||||
|
MAX_RETRIES: parseInt(process.env.MAX_RETRIES, 10) || 3,
|
||||||
|
RETRY_DELAY: parseInt(process.env.RETRY_DELAY, 10) || 1000,
|
||||||
|
},
|
||||||
|
|
||||||
|
TIMEOUT: {
|
||||||
|
API_TIMEOUT: parseInt(process.env.API_TIMEOUT, 10) || 30000,
|
||||||
|
DB_QUERY_TIMEOUT: parseInt(process.env.DB_QUERY_TIMEOUT, 10) || 30000,
|
||||||
|
},
|
||||||
|
|
||||||
|
FRONTEND: {
|
||||||
|
DEFAULT_PORT: parseInt(process.env.FRONTEND_PORT, 10) || 3000,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* 配置统一导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
const security = require('./security');
|
||||||
|
const constants = require('./constants');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
...security,
|
||||||
|
...constants,
|
||||||
|
security,
|
||||||
|
constants,
|
||||||
|
};
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/**
|
||||||
|
* 安全相关配置常量
|
||||||
|
* 集中管理密码加密、登录限制等安全配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
SALT_ROUNDS: parseInt(process.env.SALT_ROUNDS, 10) || 10,
|
||||||
|
|
||||||
|
MAX_LOGIN_ATTEMPTS: parseInt(process.env.MAX_LOGIN_ATTEMPTS, 10) || 5,
|
||||||
|
|
||||||
|
LOCK_TIME: (parseInt(process.env.LOCK_TIME_MINUTES, 10) || 30) * 60 * 1000,
|
||||||
|
|
||||||
|
TOKEN_EXPIRY: process.env.TOKEN_EXPIRY || '24h',
|
||||||
|
|
||||||
|
PASSWORD_MIN_LENGTH: parseInt(process.env.PASSWORD_MIN_LENGTH, 10) || 6,
|
||||||
|
|
||||||
|
USERNAME_MIN_LENGTH: parseInt(process.env.USERNAME_MIN_LENGTH, 10) || 3,
|
||||||
|
USERNAME_MAX_LENGTH: parseInt(process.env.USERNAME_MAX_LENGTH, 10) || 50,
|
||||||
|
};
|
||||||
@@ -4,11 +4,10 @@ const User = require('../models/User');
|
|||||||
const Role = require('../models/Role');
|
const Role = require('../models/Role');
|
||||||
const UserRole = require('../models/UserRole');
|
const UserRole = require('../models/UserRole');
|
||||||
const { generateToken, authMiddleware } = require('../middleware/auth');
|
const { generateToken, authMiddleware } = require('../middleware/auth');
|
||||||
|
const { SALT_ROUNDS, MAX_LOGIN_ATTEMPTS, PASSWORD_MIN_LENGTH, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH } = require('../config');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const SALT_ROUNDS = 10;
|
|
||||||
|
|
||||||
const generateId = () => {
|
const generateId = () => {
|
||||||
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||||
};
|
};
|
||||||
@@ -24,17 +23,17 @@ router.post('/register', async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (username.length < 3 || username.length > 20) {
|
if (username.length < USERNAME_MIN_LENGTH || username.length > USERNAME_MAX_LENGTH) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '用户名长度必须在3-20个字符之间'
|
message: `用户名长度必须在${USERNAME_MIN_LENGTH}-${USERNAME_MAX_LENGTH}个字符之间`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (password.length < 6) {
|
if (password.length < PASSWORD_MIN_LENGTH) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '密码长度不能少于6个字符'
|
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +174,7 @@ router.post('/login', async (req, res) => {
|
|||||||
const isPasswordValid = await bcrypt.compare(password, user.password);
|
const isPasswordValid = await bcrypt.compare(password, user.password);
|
||||||
if (!isPasswordValid) {
|
if (!isPasswordValid) {
|
||||||
user.loginCount = (user.loginCount || 0) + 1;
|
user.loginCount = (user.loginCount || 0) + 1;
|
||||||
if (user.loginCount >= 5) {
|
if (user.loginCount >= MAX_LOGIN_ATTEMPTS) {
|
||||||
user.status = 'locked';
|
user.status = 'locked';
|
||||||
}
|
}
|
||||||
await user.save();
|
await user.save();
|
||||||
@@ -309,10 +308,10 @@ router.put('/password', authMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword.length < 6) {
|
if (newPassword.length < PASSWORD_MIN_LENGTH) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '新密码长度不能少于6个字符'
|
message: `新密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ const Consumable = require('../models/Consumable');
|
|||||||
const ConsumableRecord = require('../models/ConsumableRecord');
|
const ConsumableRecord = require('../models/ConsumableRecord');
|
||||||
const ConsumableLog = require('../models/ConsumableLog');
|
const ConsumableLog = require('../models/ConsumableLog');
|
||||||
const ConsumableLogArchive = require('../models/ConsumableLogArchive');
|
const ConsumableLogArchive = require('../models/ConsumableLogArchive');
|
||||||
|
const { PAGINATION, RETRY } = require('../config');
|
||||||
|
|
||||||
router.get('/', async (req, res) => {
|
router.get('/', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { keyword, category, status, page = 1, pageSize = 10 } = req.query;
|
const { keyword, category, status, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE } = req.query;
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
|
|
||||||
const where = {};
|
const where = {};
|
||||||
@@ -281,10 +282,9 @@ router.get('/inout/records', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post('/quick-inout', async (req, res) => {
|
router.post('/quick-inout', async (req, res) => {
|
||||||
const MAX_RETRIES = 3;
|
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
|
|
||||||
while (attempt < MAX_RETRIES) {
|
while (attempt < RETRY.MAX_RETRIES) {
|
||||||
const transaction = await sequelize.transaction();
|
const transaction = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const { consumableId, type, quantity, operator, reason, notes, snList } = req.body;
|
const { consumableId, type, quantity, operator, reason, notes, snList } = req.body;
|
||||||
@@ -348,7 +348,7 @@ router.post('/quick-inout', async (req, res) => {
|
|||||||
if (affectedRows === 0) {
|
if (affectedRows === 0) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
attempt++;
|
attempt++;
|
||||||
if (attempt >= MAX_RETRIES) {
|
if (attempt >= RETRY.MAX_RETRIES) {
|
||||||
return res.status(409).json({ error: '并发冲突,请稍后重试' });
|
return res.status(409).json({ error: '并发冲突,请稍后重试' });
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -397,7 +397,7 @@ router.post('/quick-inout', async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
if (attempt >= MAX_RETRIES - 1) {
|
if (attempt >= RETRY.MAX_RETRIES - 1) {
|
||||||
return res.status(500).json({ error: error.message });
|
return res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
attempt++;
|
attempt++;
|
||||||
@@ -406,10 +406,9 @@ router.post('/quick-inout', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post('/inout', async (req, res) => {
|
router.post('/inout', async (req, res) => {
|
||||||
const MAX_RETRIES = 3;
|
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
|
|
||||||
while (attempt < MAX_RETRIES) {
|
while (attempt < RETRY.MAX_RETRIES) {
|
||||||
const transaction = await sequelize.transaction();
|
const transaction = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const { consumableId, type, quantity, operator, reason, recipient, notes, snList } = req.body;
|
const { consumableId, type, quantity, operator, reason, recipient, notes, snList } = req.body;
|
||||||
@@ -470,7 +469,7 @@ router.post('/inout', async (req, res) => {
|
|||||||
if (affectedRows === 0) {
|
if (affectedRows === 0) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
attempt++;
|
attempt++;
|
||||||
if (attempt >= MAX_RETRIES) {
|
if (attempt >= RETRY.MAX_RETRIES) {
|
||||||
return res.status(409).json({ error: '并发冲突,请稍后重试' });
|
return res.status(409).json({ error: '并发冲突,请稍后重试' });
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -520,7 +519,7 @@ router.post('/inout', async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
if (attempt >= MAX_RETRIES - 1) {
|
if (attempt >= RETRY.MAX_RETRIES - 1) {
|
||||||
return res.status(500).json({ error: error.message });
|
return res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
attempt++;
|
attempt++;
|
||||||
@@ -529,10 +528,9 @@ router.post('/inout', async (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
router.post('/adjust', async (req, res) => {
|
router.post('/adjust', async (req, res) => {
|
||||||
const MAX_RETRIES = 3;
|
|
||||||
let attempt = 0;
|
let attempt = 0;
|
||||||
|
|
||||||
while (attempt < MAX_RETRIES) {
|
while (attempt < RETRY.MAX_RETRIES) {
|
||||||
const transaction = await sequelize.transaction();
|
const transaction = await sequelize.transaction();
|
||||||
try {
|
try {
|
||||||
const { consumableId, adjustType, quantity, operator, reason, notes } = req.body;
|
const { consumableId, adjustType, quantity, operator, reason, notes } = req.body;
|
||||||
@@ -582,7 +580,7 @@ router.post('/adjust', async (req, res) => {
|
|||||||
if (affectedRows === 0) {
|
if (affectedRows === 0) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
attempt++;
|
attempt++;
|
||||||
if (attempt >= MAX_RETRIES) {
|
if (attempt >= RETRY.MAX_RETRIES) {
|
||||||
return res.status(409).json({ error: '并发冲突,请稍后重试' });
|
return res.status(409).json({ error: '并发冲突,请稍后重试' });
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
@@ -619,7 +617,7 @@ router.post('/adjust', async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await transaction.rollback();
|
await transaction.rollback();
|
||||||
if (attempt >= MAX_RETRIES - 1) {
|
if (attempt >= RETRY.MAX_RETRIES - 1) {
|
||||||
return res.status(500).json({ error: error.message });
|
return res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
attempt++;
|
attempt++;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const Rack = require('../models/Rack');
|
|||||||
const Room = require('../models/Room');
|
const Room = require('../models/Room');
|
||||||
const User = require('../models/User');
|
const User = require('../models/User');
|
||||||
const { authMiddleware, authorize } = require('../middleware/auth');
|
const { authMiddleware, authorize } = require('../middleware/auth');
|
||||||
|
const { PAGINATION } = require('../config');
|
||||||
|
|
||||||
InventoryTask.belongsTo(InventoryPlan, { foreignKey: 'planId', as: 'Plan' });
|
InventoryTask.belongsTo(InventoryPlan, { foreignKey: 'planId', as: 'Plan' });
|
||||||
InventoryPlan.hasMany(InventoryTask, { foreignKey: 'planId', as: 'Tasks' });
|
InventoryPlan.hasMany(InventoryTask, { foreignKey: 'planId', as: 'Tasks' });
|
||||||
@@ -41,7 +42,7 @@ router.use(authMiddleware);
|
|||||||
|
|
||||||
router.get('/plans', async (req, res) => {
|
router.get('/plans', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { status, page = 1, pageSize = 10, keyword } = req.query;
|
const { status, page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE, keyword } = req.query;
|
||||||
const offset = (page - 1) * pageSize;
|
const offset = (page - 1) * pageSize;
|
||||||
const where = {};
|
const where = {};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ const fs = require('fs');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { Op } = require('sequelize');
|
const { Op } = require('sequelize');
|
||||||
const SystemSetting = require('../models/SystemSetting');
|
const SystemSetting = require('../models/SystemSetting');
|
||||||
|
const { FRONTEND } = require('../config');
|
||||||
|
|
||||||
// 初始化默认系统设置
|
// 初始化默认系统设置
|
||||||
const initDefaultSettings = async () => {
|
const initDefaultSettings = async () => {
|
||||||
@@ -267,7 +268,7 @@ router.post('/reset/:key', async (req, res) => {
|
|||||||
idle_warning_time: 60,
|
idle_warning_time: 60,
|
||||||
max_login_attempts: 5,
|
max_login_attempts: 5,
|
||||||
maintenance_mode: false,
|
maintenance_mode: false,
|
||||||
frontend_port: 3000,
|
frontend_port: FRONTEND.DEFAULT_PORT,
|
||||||
primary_color: '#667eea',
|
primary_color: '#667eea',
|
||||||
secondary_color: '#764ba2',
|
secondary_color: '#764ba2',
|
||||||
compact_mode: false,
|
compact_mode: false,
|
||||||
@@ -563,7 +564,7 @@ router.get('/system/info', async (req, res) => {
|
|||||||
router.get('/frontend/port', async (req, res) => {
|
router.get('/frontend/port', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const portSetting = await SystemSetting.findByPk('frontend_port');
|
const portSetting = await SystemSetting.findByPk('frontend_port');
|
||||||
const port = portSetting ? JSON.parse(portSetting.settingValue) : 3000;
|
const port = portSetting ? JSON.parse(portSetting.settingValue) : FRONTEND.DEFAULT_PORT;
|
||||||
res.json({ port });
|
res.json({ port });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
@@ -577,7 +578,7 @@ router.post('/frontend/port/sync', async (req, res) => {
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
const portSetting = await SystemSetting.findByPk('frontend_port');
|
const portSetting = await SystemSetting.findByPk('frontend_port');
|
||||||
const port = portSetting ? JSON.parse(portSetting.settingValue) : 3000;
|
const port = portSetting ? JSON.parse(portSetting.settingValue) : FRONTEND.DEFAULT_PORT;
|
||||||
|
|
||||||
// 写入前端配置文件
|
// 写入前端配置文件
|
||||||
const frontendDir = path.join(__dirname, '../../frontend');
|
const frontendDir = path.join(__dirname, '../../frontend');
|
||||||
|
|||||||
@@ -6,11 +6,10 @@ const User = require('../models/User');
|
|||||||
const Role = require('../models/Role');
|
const Role = require('../models/Role');
|
||||||
const UserRole = require('../models/UserRole');
|
const UserRole = require('../models/UserRole');
|
||||||
const { authMiddleware } = require('../middleware/auth');
|
const { authMiddleware } = require('../middleware/auth');
|
||||||
|
const { SALT_ROUNDS, PASSWORD_MIN_LENGTH, FILE_UPLOAD, PAGINATION } = require('../config');
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
const SALT_ROUNDS = 10;
|
|
||||||
|
|
||||||
const generateId = () => {
|
const generateId = () => {
|
||||||
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
return 'user_' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
||||||
};
|
};
|
||||||
@@ -37,9 +36,9 @@ const { Op } = require('sequelize');
|
|||||||
|
|
||||||
router.get('/', authMiddleware, async (req, res) => {
|
router.get('/', authMiddleware, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { page = 1, pageSize = 10, username, status, realName } = req.query;
|
const { page = 1, pageSize = PAGINATION.DEFAULT_PAGE_SIZE, username, status, realName } = req.query;
|
||||||
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
const offset = (parseInt(page) - 1) * parseInt(pageSize);
|
||||||
const limit = parseInt(pageSize);
|
const limit = Math.min(parseInt(pageSize), PAGINATION.MAX_PAGE_SIZE);
|
||||||
|
|
||||||
const where = getWhereClause({ username, status, realName });
|
const where = getWhereClause({ username, status, realName });
|
||||||
|
|
||||||
@@ -256,7 +255,7 @@ router.put('/:userId', authMiddleware, async (req, res) => {
|
|||||||
if (status !== undefined) user.status = status;
|
if (status !== undefined) user.status = status;
|
||||||
if (remark !== undefined) user.remark = remark;
|
if (remark !== undefined) user.remark = remark;
|
||||||
|
|
||||||
if (newPassword && newPassword.length >= 6) {
|
if (newPassword && newPassword.length >= PASSWORD_MIN_LENGTH) {
|
||||||
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
user.password = await bcrypt.hash(newPassword, SALT_ROUNDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,10 +302,10 @@ router.put('/:userId/password', authMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPassword || newPassword.length < 6) {
|
if (!newPassword || newPassword.length < PASSWORD_MIN_LENGTH) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '密码长度不能少于6个字符'
|
message: `密码长度不能少于${PASSWORD_MIN_LENGTH}个字符`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,10 +387,10 @@ router.post('/:userId/avatar', authMiddleware, async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avatarFile.size > 5 * 1024 * 1024) {
|
if (avatarFile.size > FILE_UPLOAD.MAX_AVATAR_SIZE) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
message: '图片大小不能超过 5MB'
|
message: `图片大小不能超过 ${FILE_UPLOAD.MAX_AVATAR_SIZE / 1024 / 1024}MB`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -3,15 +3,14 @@ const express = require('express');
|
|||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const fileUpload = require('express-fileupload');
|
const fileUpload = require('express-fileupload');
|
||||||
const { sequelize } = require('./db');
|
const { sequelize } = require('./db');
|
||||||
|
const { FILE_UPLOAD } = require('./config');
|
||||||
|
|
||||||
// 创建Express应用
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 8000;
|
const PORT = process.env.PORT || 8000;
|
||||||
|
|
||||||
// 中间件
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(fileUpload({ limits: { fileSize: 50 * 1024 * 1024 } }));
|
app.use(fileUpload({ limits: { fileSize: FILE_UPLOAD.MAX_FILE_SIZE } }));
|
||||||
app.use('/temp', express.static('temp'));
|
app.use('/temp', express.static('temp'));
|
||||||
|
|
||||||
// 数据库连接已从db.js导入
|
// 数据库连接已从db.js导入
|
||||||
|
|||||||
+15
-1
@@ -1,4 +1,18 @@
|
|||||||
# 用户空闲超时配置(毫秒)
|
# ==============================================
|
||||||
|
# API 配置
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# API 请求超时时间(毫秒)
|
||||||
|
VITE_API_TIMEOUT=30000
|
||||||
|
|
||||||
|
# API 基础路径
|
||||||
|
VITE_API_BASE_URL=/api
|
||||||
|
|
||||||
|
# ==============================================
|
||||||
|
# 用户空闲超时配置
|
||||||
|
# ==============================================
|
||||||
|
|
||||||
|
# 用户空闲超时时间(毫秒)
|
||||||
# 默认30分钟 = 30 * 60 * 1000 = 1800000
|
# 默认30分钟 = 30 * 60 * 1000 = 1800000
|
||||||
VITE_IDLE_TIMEOUT=1800000
|
VITE_IDLE_TIMEOUT=1800000
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { API_CONFIG } from '../config/api';
|
||||||
const API_BASE_URL = '/api';
|
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: API_BASE_URL,
|
baseURL: API_CONFIG.baseURL,
|
||||||
timeout: 30000,
|
timeout: API_CONFIG.timeout,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* API 相关配置
|
||||||
|
* 集中管理 API 超时、分页、防抖等配置
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const API_CONFIG = {
|
||||||
|
timeout: parseInt(import.meta.env.VITE_API_TIMEOUT, 10) || 30000,
|
||||||
|
|
||||||
|
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||||
|
|
||||||
|
pagination: {
|
||||||
|
defaultPageSize: 10,
|
||||||
|
maxPageSize: 1000,
|
||||||
|
pageSizeOptions: [10, 20, 30, 50, 100],
|
||||||
|
},
|
||||||
|
|
||||||
|
debounceDelay: 300,
|
||||||
|
|
||||||
|
retry: {
|
||||||
|
maxRetries: 3,
|
||||||
|
retryDelay: 1000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default API_CONFIG;
|
||||||
@@ -89,6 +89,26 @@ export const designTokens = {
|
|||||||
lg: '24px',
|
lg: '24px',
|
||||||
xl: '32px',
|
xl: '32px',
|
||||||
},
|
},
|
||||||
|
typography: {
|
||||||
|
xs: '12px',
|
||||||
|
sm: '13px',
|
||||||
|
base: '14px',
|
||||||
|
md: '16px',
|
||||||
|
lg: '18px',
|
||||||
|
xl: '20px',
|
||||||
|
'2xl': '24px',
|
||||||
|
'3xl': '32px',
|
||||||
|
'4xl': '40px',
|
||||||
|
},
|
||||||
|
zIndex: {
|
||||||
|
dropdown: 1000,
|
||||||
|
sticky: 1020,
|
||||||
|
fixed: 1030,
|
||||||
|
modalBackdrop: 1040,
|
||||||
|
modal: 1050,
|
||||||
|
popover: 1060,
|
||||||
|
tooltip: 1070,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default designTokens;
|
export default designTokens;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import { API_CONFIG } from '../config/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 防抖 Hook
|
* 防抖 Hook
|
||||||
@@ -15,7 +16,7 @@ import { useState, useEffect } from 'react';
|
|||||||
* fetchData(debouncedKeyword);
|
* fetchData(debouncedKeyword);
|
||||||
* }, [debouncedKeyword]);
|
* }, [debouncedKeyword]);
|
||||||
*/
|
*/
|
||||||
export function useDebounce(value, delay = 300) {
|
export function useDebounce(value, delay = API_CONFIG.debounceDelay) {
|
||||||
const [debouncedValue, setDebouncedValue] = useState(value);
|
const [debouncedValue, setDebouncedValue] = useState(value);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -44,7 +45,7 @@ export function useDebounce(value, delay = 300) {
|
|||||||
*
|
*
|
||||||
* <Input onChange={e => debouncedSearch(e.target.value)} />
|
* <Input onChange={e => debouncedSearch(e.target.value)} />
|
||||||
*/
|
*/
|
||||||
export function useDebouncedCallback(callback, delay = 300) {
|
export function useDebouncedCallback(callback, delay = API_CONFIG.debounceDelay) {
|
||||||
const [timeoutId, setTimeoutId] = useState(null);
|
const [timeoutId, setTimeoutId] = useState(null);
|
||||||
|
|
||||||
const debouncedFn = (...args) => {
|
const debouncedFn = (...args) => {
|
||||||
|
|||||||
@@ -1620,7 +1620,7 @@ function DeviceManagement() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={modalHeaderStyle}>
|
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||||
{editingDevice ? (
|
{editingDevice ? (
|
||||||
<EditOutlined style={{ color: '#667eea' }} />
|
<EditOutlined style={{ color: '#667eea' }} />
|
||||||
) : (
|
) : (
|
||||||
@@ -1635,7 +1635,11 @@ function DeviceManagement() {
|
|||||||
width={700}
|
width={700}
|
||||||
style={{ borderRadius: '16px' }}
|
style={{ borderRadius: '16px' }}
|
||||||
styles={{
|
styles={{
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 50px 16px 24px' },
|
header: {
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
padding: '16px 24px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
}}
|
}}
|
||||||
className="device-modal"
|
className="device-modal"
|
||||||
@@ -1941,7 +1945,7 @@ function DeviceManagement() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={modalHeaderStyle}>
|
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||||
<SettingOutlined style={{ color: '#667eea' }} />
|
<SettingOutlined style={{ color: '#667eea' }} />
|
||||||
字段配置
|
字段配置
|
||||||
</div>
|
</div>
|
||||||
@@ -1951,7 +1955,11 @@ function DeviceManagement() {
|
|||||||
footer={null}
|
footer={null}
|
||||||
width={600}
|
width={600}
|
||||||
styles={{
|
styles={{
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
header: {
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
padding: '16px 24px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -2036,7 +2044,7 @@ function DeviceManagement() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={modalHeaderStyle}>
|
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||||
<UploadOutlined style={{ color: '#667eea' }} />
|
<UploadOutlined style={{ color: '#667eea' }} />
|
||||||
导入设备
|
导入设备
|
||||||
</div>
|
</div>
|
||||||
@@ -2053,7 +2061,11 @@ function DeviceManagement() {
|
|||||||
width={650}
|
width={650}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
header: {
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
padding: '16px 24px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -2369,7 +2381,7 @@ function DeviceManagement() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '18px', fontWeight: 600 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '18px', fontWeight: 600, paddingRight: '32px' }}>
|
||||||
<AppstoreOutlined style={{ color: '#667eea' }} />
|
<AppstoreOutlined style={{ color: '#667eea' }} />
|
||||||
设备详情
|
设备详情
|
||||||
</div>
|
</div>
|
||||||
@@ -2434,7 +2446,11 @@ function DeviceManagement() {
|
|||||||
width={700}
|
width={700}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
header: {
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
padding: '16px 24px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
body: { padding: '0', overflow: 'auto' },
|
body: { padding: '0', overflow: 'auto' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -2599,7 +2615,7 @@ function DeviceManagement() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={modalHeaderStyle}>
|
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||||
<ReloadOutlined style={{ color: '#52c41a' }} />
|
<ReloadOutlined style={{ color: '#52c41a' }} />
|
||||||
批量状态变更
|
批量状态变更
|
||||||
</div>
|
</div>
|
||||||
@@ -2637,7 +2653,11 @@ function DeviceManagement() {
|
|||||||
]}
|
]}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
header: {
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
padding: '16px 24px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -2664,7 +2684,7 @@ function DeviceManagement() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={modalHeaderStyle}>
|
<div style={{ ...modalHeaderStyle, paddingRight: '32px' }}>
|
||||||
<ExportOutlined style={{ color: '#fa8c16' }} />
|
<ExportOutlined style={{ color: '#fa8c16' }} />
|
||||||
导出设备数据
|
导出设备数据
|
||||||
</div>
|
</div>
|
||||||
@@ -2702,7 +2722,11 @@ function DeviceManagement() {
|
|||||||
]}
|
]}
|
||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
header: {
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
padding: '16px 24px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
}}
|
}}
|
||||||
width={600}
|
width={600}
|
||||||
|
|||||||
@@ -755,6 +755,23 @@ function RackManagement() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
|
<style>{`
|
||||||
|
.ant-modal-close {
|
||||||
|
top: 16px !important;
|
||||||
|
right: 16px !important;
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
line-height: 32px !important;
|
||||||
|
}
|
||||||
|
.ant-modal-close-x {
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
line-height: 32px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
<div style={headerStyle}>
|
<div style={headerStyle}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -969,7 +986,7 @@ function RackManagement() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', paddingRight: '32px' }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '4px',
|
width: '4px',
|
||||||
@@ -988,9 +1005,16 @@ function RackManagement() {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 50px 16px 24px' },
|
header: {
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
padding: '16px 24px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
style={{ borderRadius: '16px' }}
|
style={{ borderRadius: '16px' }}
|
||||||
|
classNames={{
|
||||||
|
header: 'modal-header-fix',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||||
<Row gutter={16}>
|
<Row gutter={16}>
|
||||||
@@ -1231,7 +1255,7 @@ function RackManagement() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', paddingRight: '32px' }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '4px',
|
width: '4px',
|
||||||
@@ -1256,7 +1280,11 @@ function RackManagement() {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
header: {
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
padding: '16px 24px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
style={{ borderRadius: '16px' }}
|
style={{ borderRadius: '16px' }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -591,6 +591,23 @@ function RoomManagement() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={containerStyle}>
|
<div style={containerStyle}>
|
||||||
|
<style>{`
|
||||||
|
.ant-modal-close {
|
||||||
|
top: 16px !important;
|
||||||
|
right: 16px !important;
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
line-height: 32px !important;
|
||||||
|
}
|
||||||
|
.ant-modal-close-x {
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
line-height: 32px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
<div style={headerStyle}>
|
<div style={headerStyle}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -765,7 +782,7 @@ function RoomManagement() {
|
|||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title={
|
title={
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: '10px', paddingRight: '32px' }}>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
width: '4px',
|
width: '4px',
|
||||||
@@ -784,7 +801,11 @@ function RoomManagement() {
|
|||||||
destroyOnHidden
|
destroyOnHidden
|
||||||
styles={{
|
styles={{
|
||||||
body: { padding: '24px' },
|
body: { padding: '24px' },
|
||||||
header: { borderBottom: '1px solid #f0f0f0', padding: '16px 24px' },
|
header: {
|
||||||
|
borderBottom: '1px solid #f0f0f0',
|
||||||
|
padding: '16px 24px',
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
style={{ borderRadius: '16px' }}
|
style={{ borderRadius: '16px' }}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -815,6 +815,24 @@ export const inputPlaceholders = {
|
|||||||
|
|
||||||
// CSS-in-JS 样式字符串生成函数
|
// CSS-in-JS 样式字符串生成函数
|
||||||
export const generateGlobalStyles = tokens => `
|
export const generateGlobalStyles = tokens => `
|
||||||
|
/* Modal 关闭按钮通用修复 */
|
||||||
|
.ant-modal-close {
|
||||||
|
top: 16px !important;
|
||||||
|
right: 16px !important;
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
line-height: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-modal-close-x {
|
||||||
|
width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
line-height: 32px !important;
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
.device-modal .ant-modal-close {
|
.device-modal .ant-modal-close {
|
||||||
top: 16px;
|
top: 16px;
|
||||||
right: 24px;
|
right: 24px;
|
||||||
|
|||||||
Reference in New Issue
Block a user