Files
backend-node/routes/serials.js
2026-02-06 14:29:29 +08:00

407 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require('express');
const QRCode = require('qrcode');
const db = require('../utils/database');
const { authenticateToken, requireAdmin } = require('../middleware/auth');
const router = express.Router();
// 生成序列号
router.post('/generate', authenticateToken, requireAdmin, async (req, res) => {
try {
const { companyName, quantity = 1, validDays = 365 } = req.body;
if (!companyName) {
return res.status(400).json({ error: '企业名称不能为空' });
}
if (quantity < 1 || quantity > 100) {
return res.status(400).json({ error: '生成数量必须在1-100之间' });
}
// 计算有效期
const validUntil = new Date();
validUntil.setDate(validUntil.getDate() + validDays);
// 确保企业存在
const existingCompany = await db.get('SELECT * FROM companies WHERE company_name = ?', [companyName]);
if (!existingCompany) {
await db.run('INSERT INTO companies (company_name, is_active) VALUES (?, 1)', [companyName]);
}
// 生成序列号
const serials = [];
const prefix = 'BF';
const datePart = new Date().getFullYear().toString().substr(2);
// 批量插入序列号
const insertPromises = [];
for (let i = 0; i < quantity; i++) {
// 使用随机数生成序列号,避免重复
const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
const serialNumber = `${prefix}${datePart}${randomPart}`;
insertPromises.push(
db.run(
'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)',
[serialNumber, companyName, validUntil.toISOString().slice(0, 19).replace('T', ' '), req.user.id]
)
);
serials.push({
serialNumber,
companyName,
validUntil: validUntil.toISOString(),
createdAt: new Date().toISOString()
});
}
await Promise.all(insertPromises);
res.json({
message: `成功生成${quantity}个序列号`,
serials
});
} catch (error) {
console.error('生成序列号错误:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 生成二维码
router.post('/:serialNumber/qrcode', authenticateToken, async (req, res) => {
try {
const { serialNumber } = req.params;
let { baseUrl } = req.body;
if (!serialNumber) {
return res.status(400).json({ error: '序列号不能为空' });
}
// 验证序列号是否存在
const serial = await db.get(
'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?',
[serialNumber.toUpperCase()]
);
if (!serial) {
return res.status(404).json({ error: '序列号不存在' });
}
if (!serial.is_active) {
return res.status(400).json({ error: '序列号已被禁用' });
}
// 检查是否过期
if (serial.valid_until && new Date(serial.valid_until) < new Date()) {
return res.status(400).json({ error: '序列号已过期' });
}
// 生成查询URL
if (!baseUrl) {
baseUrl = `${req.protocol}://${req.get('host')}/query.html`;
}
const queryUrl = baseUrl.includes('?')
? `${baseUrl}&serial=${serial.serial_number}`
: `${baseUrl}?serial=${serial.serial_number}`;
// 生成二维码
const qrCodeData = await QRCode.toDataURL(queryUrl, {
width: 200,
color: {
dark: '#165DFF',
light: '#ffffff'
}
});
res.json({
message: '二维码生成成功',
qrCodeData,
queryUrl,
serialNumber: serial.serial_number,
companyName: serial.company_name,
validUntil: serial.valid_until
});
} catch (error) {
console.error('生成二维码错误:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 查询序列号
router.get('/:serialNumber/query', async (req, res) => {
try {
const { serialNumber } = req.params;
if (!serialNumber) {
return res.status(400).json({ error: '序列号不能为空' });
}
// 查询序列号
const serial = await db.get(
'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?',
[serialNumber.toUpperCase()]
);
if (!serial) {
return res.status(404).json({ error: '序列号不存在' });
}
// 检查是否过期
if (serial.valid_until && new Date(serial.valid_until) < new Date()) {
return res.status(400).json({ error: '序列号已过期' });
}
res.json({
message: '查询成功',
serial: {
serialNumber: serial.serial_number,
companyName: serial.company_name,
validUntil: serial.valid_until,
status: serial.is_active ? 'active' : 'disabled',
isActive: serial.is_active,
createdAt: serial.created_at,
createdBy: serial.created_by_name
}
});
} catch (error) {
console.error('查询序列号错误:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 获取序列号列表
router.get('/', authenticateToken, async (req, res) => {
try {
const { page = 1, limit = 20, search = '' } = req.query;
const offset = (page - 1) * limit;
let query = `
SELECT s.*, u.name as created_by_name
FROM serials s
LEFT JOIN users u ON s.created_by = u.id
`;
let countQuery = 'SELECT COUNT(*) as total FROM serials s';
let params = [];
if (search) {
query += ' WHERE s.serial_number LIKE ? OR s.company_name LIKE ?';
countQuery += ' WHERE s.serial_number LIKE ? OR s.company_name LIKE ?';
const searchParam = `%${search}%`;
params.push(searchParam, searchParam);
}
query += ' ORDER BY s.created_at DESC LIMIT ? OFFSET ?';
params.push(parseInt(limit), parseInt(offset));
const [serials, countResult] = await Promise.all([
db.all(query, params),
db.get(countQuery, params.slice(0, -2))
]);
const total = countResult ? countResult.total : 0;
const totalPages = Math.ceil(total / limit);
res.json({
message: '获取序列号列表成功',
data: serials.map(s => ({
serialNumber: s.serial_number,
companyName: s.company_name,
validUntil: s.valid_until,
isActive: s.is_active,
createdAt: s.created_at,
createdBy: s.created_by_name
})),
pagination: {
page: parseInt(page),
limit: parseInt(limit),
total,
totalPages
}
});
} catch (error) {
console.error('获取序列号列表错误:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 更新序列号
router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req, res) => {
try {
const { serialNumber } = req.params;
const { companyName, validUntil, isActive } = req.body;
if (!serialNumber) {
return res.status(400).json({ error: '序列号不能为空' });
}
// 检查序列号是否存在
const existingSerial = await db.get('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
if (!existingSerial) {
return res.status(404).json({ error: '序列号不存在' });
}
// 构建更新字段
const updateFields = [];
const params = [];
if (companyName !== undefined) {
updateFields.push('company_name = ?');
params.push(companyName);
}
if (validUntil !== undefined) {
updateFields.push('valid_until = ?');
params.push(validUntil);
}
if (isActive !== undefined) {
updateFields.push('is_active = ?');
params.push(isActive ? 1 : 0);
}
if (updateFields.length === 0) {
return res.status(400).json({ error: '没有提供更新字段' });
}
updateFields.push('updated_at = CURRENT_TIMESTAMP');
params.push(serialNumber.toUpperCase());
await db.run(
`UPDATE serials SET ${updateFields.join(', ')} WHERE serial_number = ?`,
params
);
// 获取更新后的序列号信息
const updatedSerial = await db.get(
'SELECT s.*, u.name as created_by_name FROM serials s LEFT JOIN users u ON s.created_by = u.id WHERE s.serial_number = ?',
[serialNumber.toUpperCase()]
);
res.json({
message: '序列号更新成功',
serial: {
serialNumber: updatedSerial.serial_number,
companyName: updatedSerial.company_name,
validUntil: updatedSerial.valid_until,
isActive: updatedSerial.is_active,
createdAt: updatedSerial.created_at,
updatedAt: updatedSerial.updated_at,
createdBy: updatedSerial.created_by_name
}
});
} catch (error) {
console.error('更新序列号错误:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 吊销序列号
router.post('/:serialNumber/revoke', authenticateToken, requireAdmin, async (req, res) => {
try {
const { serialNumber } = req.params;
if (!serialNumber) {
return res.status(400).json({ error: '序列号不能为空' });
}
// 检查序列号是否存在
const existingSerial = await db.get(
'SELECT * FROM serials WHERE serial_number = ?',
[serialNumber.toUpperCase()]
);
if (!existingSerial) {
return res.status(404).json({ error: '序列号不存在' });
}
// 如果已经吊销,返回提示
if (!existingSerial.is_active) {
return res.status(400).json({ error: '序列号已被吊销' });
}
// 吊销序列号(将 is_active 设为 0
await db.run(
'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE serial_number = ?',
[serialNumber.toUpperCase()]
);
res.json({
message: '序列号已吊销',
data: {
serialNumber: serialNumber.toUpperCase()
}
});
} catch (error) {
console.error('吊销序列号错误:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
// 自定义前缀生成序列号(管理员权限)
router.post('/generate-with-prefix', authenticateToken, requireAdmin, async (req, res) => {
try {
const { companyName, quantity = 1, validDays = 365, serialPrefix } = req.body;
if (!companyName) {
return res.status(400).json({ error: '企业名称不能为空' });
}
if (!serialPrefix || serialPrefix.length > 10) {
return res.status(400).json({ error: '自定义前缀不能为空且不能超过10个字符' });
}
if (quantity < 1 || quantity > 100) {
return res.status(400).json({ error: '生成数量必须在1-100之间' });
}
// 计算有效期
const validUntil = new Date();
validUntil.setDate(validUntil.getDate() + validDays);
// 生成序列号
const serials = [];
const prefix = serialPrefix.toUpperCase().replace(/[^A-Z0-9]/g, '');
if (!prefix) {
return res.status(400).json({ error: '自定义前缀包含无效字符,只能包含字母和数字' });
}
// 批量插入序列号
const insertPromises = [];
for (let i = 0; i < quantity; i++) {
// 使用随机数生成序列号,避免重复
const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
const serialNumber = `${prefix}${randomPart}`;
insertPromises.push(
db.run(
'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)',
[serialNumber, companyName, validUntil.toISOString(), req.user.id]
)
);
serials.push({
serialNumber,
companyName,
validUntil: validUntil.toISOString(),
createdAt: new Date().toISOString()
});
}
await Promise.all(insertPromises);
res.json({
message: `成功生成${quantity}个序列号`,
serials
});
} catch (error) {
console.error('生成序列号错误:', error);
res.status(500).json({ error: '服务器内部错误' });
}
});
module.exports = router;