378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
import express, { Request, Response } from 'express';
|
|
import QRCode from 'qrcode';
|
|
import db from '../utils/database';
|
|
import { authenticateToken, requireAdmin } from '../middleware/auth';
|
|
import { GenerateSerialRequest, GenerateSerialWithPrefixRequest, QRCodeRequest, UpdateSerialRequest, PaginationQuery, SerialListItem } from '../types';
|
|
|
|
const router = express.Router();
|
|
|
|
router.post('/generate', authenticateToken, requireAdmin, async (req: Request<{}, {}, GenerateSerialRequest>, res: Response): Promise<void> => {
|
|
try {
|
|
const { companyName, quantity = 1, validDays = 365 } = req.body;
|
|
|
|
if (!companyName) {
|
|
res.status(400).json({ error: '企业名称不能为空' });
|
|
return;
|
|
}
|
|
|
|
if (quantity < 1 || quantity > 100) {
|
|
res.status(400).json({ error: '生成数量必须在1-100之间' });
|
|
return;
|
|
}
|
|
|
|
const validUntil = new Date();
|
|
validUntil.setDate(validUntil.getDate() + validDays);
|
|
|
|
const existingCompany = await db.get('xSELECT * FROM companies WHERE company_name = ?', [companyName]);
|
|
if (!existingCompany) {
|
|
await db.run('INSERT INTO companies (company_name, is_active) VALUES (?, 1)', [companyName]);
|
|
}
|
|
|
|
const serials: SerialListItem[] = [];
|
|
const prefix = 'BF';
|
|
const datePart = new Date().getFullYear().toString().substr(2);
|
|
|
|
for (let i = 0; i < quantity; i++) {
|
|
const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
|
const serialNumber = `${prefix}${datePart}${randomPart}`;
|
|
|
|
await 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()
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
message: `成功生成${quantity}个序列号`,
|
|
serials
|
|
});
|
|
} catch (error) {
|
|
console.error('生成序列号错误:', error);
|
|
res.status(500).json({ error: '服务器内部错误' });
|
|
}
|
|
});
|
|
|
|
router.post('/:serialNumber/qrcode', authenticateToken, async (req: Request<{ serialNumber: string }, {}, QRCodeRequest>, res: Response): Promise<void> => {
|
|
try {
|
|
const { serialNumber } = req.params;
|
|
let { baseUrl } = req.body;
|
|
|
|
if (!serialNumber) {
|
|
res.status(400).json({ error: '序列号不能为空' });
|
|
return;
|
|
}
|
|
|
|
const serial = await db.get<{ serial_number: string; company_name: string; is_active: number; valid_until: string | null }>(
|
|
'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) {
|
|
res.status(404).json({ error: '序列号不存在' });
|
|
return;
|
|
}
|
|
|
|
if (!serial.is_active) {
|
|
res.status(400).json({ error: '序列号已被禁用' });
|
|
return;
|
|
}
|
|
|
|
if (serial.valid_until && new Date(serial.valid_until) < new Date()) {
|
|
res.status(400).json({ error: '序列号已过期' });
|
|
return;
|
|
}
|
|
|
|
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: Request<{ serialNumber: string }>, res: Response): Promise<void> => {
|
|
try {
|
|
const { serialNumber } = req.params;
|
|
|
|
if (!serialNumber) {
|
|
res.status(400).json({ error: '序列号不能为空' });
|
|
return;
|
|
}
|
|
|
|
const serial = await db.get<{ serial_number: string; company_name: string; valid_until: string | null; is_active: number; created_at: string; created_by_name: string }>(
|
|
'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) {
|
|
res.status(404).json({ error: '序列号不存在' });
|
|
return;
|
|
}
|
|
|
|
if (serial.valid_until && new Date(serial.valid_until) < new Date()) {
|
|
res.status(400).json({ error: '序列号已过期' });
|
|
return;
|
|
}
|
|
|
|
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: Request<{}, {}, {}, PaginationQuery>, res: Response): Promise<void> => {
|
|
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: any[] = [];
|
|
|
|
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.toString()), parseInt(offset.toString()));
|
|
|
|
const [serials, countResult] = await Promise.all([
|
|
db.all(query, params),
|
|
db.get<{ total: number }>(countQuery, params.slice(0, -2))
|
|
]);
|
|
|
|
const total = countResult?.total || 0;
|
|
const totalPages = Math.ceil(total / limit);
|
|
|
|
res.json({
|
|
message: '获取序列号列表成功',
|
|
data: serials.map((s: any) => ({
|
|
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.toString()),
|
|
limit: parseInt(limit.toString()),
|
|
total,
|
|
totalPages
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('获取序列号列表错误:', error);
|
|
res.status(500).json({ error: '服务器内部错误' });
|
|
}
|
|
});
|
|
|
|
router.patch('/:serialNumber', authenticateToken, requireAdmin, async (req: Request<{ serialNumber: string }, {}, UpdateSerialRequest>, res: Response): Promise<void> => {
|
|
try {
|
|
const { serialNumber } = req.params;
|
|
const { companyName, validUntil, isActive } = req.body;
|
|
|
|
if (!serialNumber) {
|
|
res.status(400).json({ error: '序列号不能为空' });
|
|
return;
|
|
}
|
|
|
|
const existingSerial = await db.get<{ is_active: number }>('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
|
|
|
|
if (!existingSerial) {
|
|
res.status(404).json({ error: '序列号不存在' });
|
|
return;
|
|
}
|
|
|
|
const updateFields: string[] = [];
|
|
const params: any[] = [];
|
|
|
|
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) {
|
|
res.status(400).json({ error: '没有提供更新字段' });
|
|
return;
|
|
}
|
|
|
|
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 as any).serial_number,
|
|
companyName: (updatedSerial as any).company_name,
|
|
validUntil: (updatedSerial as any).valid_until,
|
|
isActive: (updatedSerial as any).is_active,
|
|
createdAt: (updatedSerial as any).created_at,
|
|
updatedAt: (updatedSerial as any).updated_at,
|
|
createdBy: (updatedSerial as any).created_by_name
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('更新序列号错误:', error);
|
|
res.status(500).json({ error: '服务器内部错误' });
|
|
}
|
|
});
|
|
|
|
router.post('/:serialNumber/revoke', authenticateToken, requireAdmin, async (req: Request<{ serialNumber: string }>, res: Response): Promise<void> => {
|
|
try {
|
|
const { serialNumber } = req.params;
|
|
|
|
if (!serialNumber) {
|
|
res.status(400).json({ error: '序列号不能为空' });
|
|
return;
|
|
}
|
|
|
|
const existingSerial = await db.get<{ is_active: number }>('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
|
|
|
|
if (!existingSerial) {
|
|
res.status(404).json({ error: '序列号不存在' });
|
|
return;
|
|
}
|
|
|
|
if (!existingSerial.is_active) {
|
|
res.status(400).json({ error: '序列号已被吊销' });
|
|
return;
|
|
}
|
|
|
|
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: Request<{}, {}, GenerateSerialWithPrefixRequest>, res: Response): Promise<void> => {
|
|
try {
|
|
const { companyName, quantity = 1, validDays = 365, serialPrefix } = req.body;
|
|
|
|
if (!companyName) {
|
|
res.status(400).json({ error: '企业名称不能为空' });
|
|
return;
|
|
}
|
|
|
|
if (!serialPrefix || serialPrefix.length > 10) {
|
|
res.status(400).json({ error: '自定义前缀不能为空且不能超过10个字符' });
|
|
return;
|
|
}
|
|
|
|
if (quantity < 1 || quantity > 100) {
|
|
res.status(400).json({ error: '生成数量必须在1-100之间' });
|
|
return;
|
|
}
|
|
|
|
const validUntil = new Date();
|
|
validUntil.setDate(validUntil.getDate() + validDays);
|
|
|
|
const serials: SerialListItem[] = [];
|
|
const prefix = serialPrefix.toUpperCase().replace(/[^A-Z0-9]/g, '');
|
|
|
|
if (!prefix) {
|
|
res.status(400).json({ error: '自定义前缀包含无效字符,只能包含字母和数字' });
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < quantity; i++) {
|
|
const randomPart = Math.floor(Math.random() * 1000000).toString().padStart(6, '0');
|
|
const serialNumber = `${prefix}${randomPart}`;
|
|
|
|
await 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()
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
message: `成功生成${quantity}个序列号`,
|
|
serials
|
|
});
|
|
} catch (error) {
|
|
console.error('生成序列号错误:', error);
|
|
res.status(500).json({ error: '服务器内部错误' });
|
|
}
|
|
});
|
|
|
|
export default router;
|