refactor: migrate backend framework from Express to NestJS
This commit is contained in:
63
src/serials/dto/index.ts
Normal file
63
src/serials/dto/index.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { IsString, IsNotEmpty, IsNumber, IsOptional, IsBoolean, Min, Max } from 'class-validator';
|
||||
|
||||
export class GenerateSerialDto {
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '企业名称不能为空' })
|
||||
companyName: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@Min(1)
|
||||
@Max(100)
|
||||
quantity?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
validDays?: number;
|
||||
}
|
||||
|
||||
export class GenerateWithPrefixDto extends GenerateSerialDto {
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '自定义前缀不能为空' })
|
||||
serialPrefix: string;
|
||||
}
|
||||
|
||||
export class QRCodeDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export class UpdateSerialDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
companyName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
validUntil?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export interface Serial {
|
||||
id: number;
|
||||
serial_number: string;
|
||||
company_name: string;
|
||||
valid_until: string | null;
|
||||
is_active: boolean;
|
||||
created_by: number | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface SerialListItem {
|
||||
serialNumber: string;
|
||||
companyName: string;
|
||||
validUntil: string | null;
|
||||
isActive: boolean;
|
||||
createdAt: string;
|
||||
createdBy?: string;
|
||||
}
|
||||
97
src/serials/serials.controller.ts
Normal file
97
src/serials/serials.controller.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Controller, Get, Post, Patch, Body, Param, Query, UseGuards, Req, HttpCode, HttpStatus } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { SerialsService } from './serials.service';
|
||||
import { AuthGuard } from '../auth/auth.guard';
|
||||
import { AdminGuard } from '../auth/admin.guard';
|
||||
import { GenerateSerialDto, GenerateWithPrefixDto, QRCodeDto, UpdateSerialDto } from './dto';
|
||||
|
||||
@Controller('serials')
|
||||
export class SerialsController {
|
||||
constructor(private readonly serialsService: SerialsService) {}
|
||||
|
||||
@Post('generate')
|
||||
@UseGuards(AuthGuard, AdminGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async generate(@Body() generateDto: GenerateSerialDto, @Req() req: Request) {
|
||||
const serials = await this.serialsService.generate(
|
||||
generateDto.companyName,
|
||||
generateDto.quantity || 1,
|
||||
generateDto.validDays || 365,
|
||||
(req as any).user.id,
|
||||
);
|
||||
return {
|
||||
message: `成功生成${serials.length}个序列号`,
|
||||
serials,
|
||||
};
|
||||
}
|
||||
|
||||
@Post('generate-with-prefix')
|
||||
@UseGuards(AuthGuard, AdminGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async generateWithPrefix(@Body() generateDto: GenerateWithPrefixDto, @Req() req: Request) {
|
||||
const serials = await this.serialsService.generate(
|
||||
generateDto.companyName,
|
||||
generateDto.quantity || 1,
|
||||
generateDto.validDays || 365,
|
||||
(req as any).user.id,
|
||||
generateDto.serialPrefix,
|
||||
);
|
||||
return {
|
||||
message: `成功生成${serials.length}个序列号`,
|
||||
serials,
|
||||
};
|
||||
}
|
||||
|
||||
@Post(':serialNumber/qrcode')
|
||||
@UseGuards(AuthGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async generateQRCode(
|
||||
@Param('serialNumber') serialNumber: string,
|
||||
@Body() qrCodeDto: QRCodeDto,
|
||||
@Req() req: Request,
|
||||
) {
|
||||
return this.serialsService.generateQRCode(
|
||||
serialNumber,
|
||||
qrCodeDto.baseUrl,
|
||||
req.get('host'),
|
||||
req.protocol,
|
||||
);
|
||||
}
|
||||
|
||||
@Get(':serialNumber/query')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async query(@Param('serialNumber') serialNumber: string) {
|
||||
return this.serialsService.query(serialNumber);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@UseGuards(AuthGuard)
|
||||
async findAll(
|
||||
@Query('page') page: string = '1',
|
||||
@Query('limit') limit: string = '20',
|
||||
@Query('search') search: string = '',
|
||||
) {
|
||||
return this.serialsService.findAll(parseInt(page), parseInt(limit), search);
|
||||
}
|
||||
|
||||
@Patch(':serialNumber')
|
||||
@UseGuards(AuthGuard, AdminGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async update(
|
||||
@Param('serialNumber') serialNumber: string,
|
||||
@Body() updateDto: UpdateSerialDto,
|
||||
) {
|
||||
return this.serialsService.update(serialNumber, {
|
||||
companyName: updateDto.companyName,
|
||||
validUntil: updateDto.validUntil,
|
||||
isActive: updateDto.isActive,
|
||||
});
|
||||
}
|
||||
|
||||
@Post(':serialNumber/revoke')
|
||||
@UseGuards(AuthGuard, AdminGuard)
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async revoke(@Param('serialNumber') serialNumber: string) {
|
||||
return this.serialsService.revoke(serialNumber);
|
||||
}
|
||||
}
|
||||
9
src/serials/serials.module.ts
Normal file
9
src/serials/serials.module.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { SerialsService } from './serials.service';
|
||||
import { SerialsController } from './serials.controller';
|
||||
|
||||
@Module({
|
||||
controllers: [SerialsController],
|
||||
providers: [SerialsService],
|
||||
})
|
||||
export class SerialsModule {}
|
||||
235
src/serials/serials.service.ts
Normal file
235
src/serials/serials.service.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { DatabaseService } from '../database/database.service';
|
||||
import * as QRCode from 'qrcode';
|
||||
import { Serial, SerialListItem } from './dto';
|
||||
|
||||
@Injectable()
|
||||
export class SerialsService {
|
||||
constructor(private dbService: DatabaseService) {}
|
||||
|
||||
async generate(companyName: string, quantity: number, validDays: number, userId: number, serialPrefix?: string): Promise<SerialListItem[]> {
|
||||
const validUntil = new Date();
|
||||
validUntil.setDate(validUntil.getDate() + validDays);
|
||||
|
||||
const existingCompany = await this.dbService.get('SELECT * FROM companies WHERE company_name = ?', [companyName]);
|
||||
if (!existingCompany) {
|
||||
await this.dbService.run('INSERT INTO companies (company_name, is_active) VALUES (?, 1)', [companyName]);
|
||||
}
|
||||
|
||||
const serials: SerialListItem[] = [];
|
||||
const prefix = serialPrefix ? serialPrefix.toUpperCase().replace(/[^A-Z0-9]/g, '') : 'BF' + 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}${randomPart}`;
|
||||
|
||||
await this.dbService.run(
|
||||
'INSERT INTO serials (serial_number, company_name, valid_until, created_by) VALUES (?, ?, ?, ?)',
|
||||
[serialNumber, companyName, validUntil.toISOString().slice(0, 19).replace('T', ' '), userId]
|
||||
);
|
||||
|
||||
serials.push({
|
||||
serialNumber,
|
||||
companyName,
|
||||
validUntil: validUntil.toISOString(),
|
||||
isActive: true,
|
||||
createdAt: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
return serials;
|
||||
}
|
||||
|
||||
async generateQRCode(serialNumber: string, baseUrl?: string, requestHost?: string, protocol?: string) {
|
||||
const serial = await this.dbService.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) {
|
||||
throw new Error('序列号不存在');
|
||||
}
|
||||
|
||||
if (!serial.is_active) {
|
||||
throw new Error('序列号已被禁用');
|
||||
}
|
||||
|
||||
if (serial.valid_until && new Date(serial.valid_until) < new Date()) {
|
||||
throw new Error('序列号已过期');
|
||||
}
|
||||
|
||||
if (!baseUrl) {
|
||||
baseUrl = `${protocol}://${requestHost}/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'
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
message: '二维码生成成功',
|
||||
qrCodeData,
|
||||
queryUrl,
|
||||
serialNumber: serial.serial_number,
|
||||
companyName: serial.company_name,
|
||||
validUntil: serial.valid_until
|
||||
};
|
||||
}
|
||||
|
||||
async query(serialNumber: string) {
|
||||
const serial = await this.dbService.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) {
|
||||
throw new Error('序列号不存在');
|
||||
}
|
||||
|
||||
if (serial.valid_until && new Date(serial.valid_until) < new Date()) {
|
||||
throw new Error('序列号已过期');
|
||||
}
|
||||
|
||||
return {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async findAll(page: number, limit: number, search: string) {
|
||||
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([
|
||||
this.dbService.all(query, params),
|
||||
this.dbService.get<{ total: number }>(countQuery, params.slice(0, -2))
|
||||
]);
|
||||
|
||||
const total = countResult?.total || 0;
|
||||
const totalPages = Math.ceil(total / limit);
|
||||
|
||||
return {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async update(serialNumber: string, updateData: { companyName?: string; validUntil?: string; isActive?: boolean }) {
|
||||
const existingSerial = await this.dbService.get<{ is_active: number }>('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
|
||||
|
||||
if (!existingSerial) {
|
||||
throw new Error('序列号不存在');
|
||||
}
|
||||
|
||||
const updateFields: string[] = [];
|
||||
const params: any[] = [];
|
||||
|
||||
if (updateData.companyName !== undefined) {
|
||||
updateFields.push('company_name = ?');
|
||||
params.push(updateData.companyName);
|
||||
}
|
||||
|
||||
if (updateData.validUntil !== undefined) {
|
||||
updateFields.push('valid_until = ?');
|
||||
params.push(updateData.validUntil);
|
||||
}
|
||||
|
||||
if (updateData.isActive !== undefined) {
|
||||
updateFields.push('is_active = ?');
|
||||
params.push(updateData.isActive ? 1 : 0);
|
||||
}
|
||||
|
||||
if (updateFields.length === 0) {
|
||||
throw new Error('没有提供更新字段');
|
||||
}
|
||||
|
||||
updateFields.push('updated_at = CURRENT_TIMESTAMP');
|
||||
params.push(serialNumber.toUpperCase());
|
||||
|
||||
await this.dbService.run(
|
||||
`UPDATE serials SET ${updateFields.join(', ')} WHERE serial_number = ?`,
|
||||
params
|
||||
);
|
||||
|
||||
const updatedSerial = await this.dbService.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()]);
|
||||
|
||||
return {
|
||||
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async revoke(serialNumber: string) {
|
||||
const existingSerial = await this.dbService.get<{ is_active: number }>('SELECT * FROM serials WHERE serial_number = ?', [serialNumber.toUpperCase()]);
|
||||
|
||||
if (!existingSerial) {
|
||||
throw new Error('序列号不存在');
|
||||
}
|
||||
|
||||
if (!existingSerial.is_active) {
|
||||
throw new Error('序列号已被吊销');
|
||||
}
|
||||
|
||||
await this.dbService.run(
|
||||
'UPDATE serials SET is_active = 0, updated_at = CURRENT_TIMESTAMP WHERE serial_number = ?',
|
||||
[serialNumber.toUpperCase()]
|
||||
);
|
||||
|
||||
return {
|
||||
message: '序列号已吊销',
|
||||
data: {
|
||||
serialNumber: serialNumber.toUpperCase()
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user