Commit 3d498bc8 authored by mingyard's avatar mingyard

feat:设备id

parent a6ad4da5
Pipeline #65314 failed with stages
in 57 seconds
......@@ -30,9 +30,11 @@ WORKDIR /usr/src/code
# 复制构建结果和依赖
COPY --from=build-stage /code/node_modules ./node_modules
COPY --from=build-stage /code/migrations-build ./migrations
COPY --from=build-stage /code/dist ./
COPY ./ormconfig.ts ./ormconfig.ts
COPY ./tsconfig.json ./tsconfig.json
COPY ./package.json ./
COPY ./tsconfig.json ./
# 暴露应用运行的端口
EXPOSE 3000
......
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
import { CreatedAt, ID, NOT_NULL, TINYINT, UpdatedAt, VARCHAR } from './utils';
export class CreateTableTranslateDevice1739177747810
implements MigrationInterface
{
TABLE_NAME = 'translatedevice';
public async up(queryRunner: QueryRunner): Promise<void> {
const exist = await queryRunner.hasTable(this.TABLE_NAME);
if (exist) return;
await queryRunner.createTable(
new Table({
name: this.TABLE_NAME,
columns: [
ID,
{
name: 'sn',
...VARCHAR,
length: '255',
comment: '设备序列号',
...NOT_NULL,
},
{
name: 'name',
...VARCHAR,
length: '255',
comment: '设备名称',
...NOT_NULL,
},
{
name: 'type',
...VARCHAR,
length: '255',
comment: '设备类型',
...NOT_NULL,
},
{
name: 'status',
...TINYINT,
comment: '状态:0禁用、1启用',
default: '1',
...NOT_NULL,
},
{
name: 'deleted',
...TINYINT,
comment: '0未删除、1已删除',
default: '0',
...NOT_NULL,
},
CreatedAt,
UpdatedAt,
],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {}
}
export * from './table-checker';
export * from './utils';
import { DatabaseType, getManager, QueryRunner } from 'typeorm';
export const isTableExist = async (
tableName: string,
queryRunner: QueryRunner,
) => {
const table = await queryRunner.getTable(tableName);
if (table) {
return true;
} else {
return false;
}
};
export const isColumnExist = async (
columnName: string,
tableName: string,
queryRunner: QueryRunner,
) => {
const table = await queryRunner.getTable(tableName);
const column = table?.columns.find((c) => c.name === columnName);
if (column) {
return true;
} else {
return false;
}
};
export const isIndexExist = async (
indexName: string,
tableName: string,
queryRunner: QueryRunner,
) => {
const table = await queryRunner.getTable(tableName);
const index = table.indices.find((i) => i.name === indexName);
if (index) {
return true;
} else {
return false;
}
};
export const getDatabaseType = (): DatabaseType => {
const type = getManager().connection.options.type;
return type;
};
export const isMysql = () => getDatabaseType() === 'mysql';
export const isPostgres = () => getDatabaseType() === 'postgres';
import _ from 'lodash';
import * as yaml from 'yaml';
import path from 'path';
import fs from 'fs';
import * as crypto from 'crypto';
import * as minimist from 'minimist';
export const NOT_NULL = {
isNullable: false,
};
export const NULLABLE = {
isNullable: true,
};
export const VARCHAR = {
type: 'varchar',
};
export const BOOL = {
type: 'bool',
};
export const TEXT = {
type: 'text',
};
export const SMALL_INT = {
type: 'smallint',
};
export const INT = {
type: 'int',
};
export const TINYINT = {
type: 'int',
};
export const LONG_INT = {
type: 'bigint',
};
export const TIMESTAMP = {
type: 'timestamp',
};
export const JSON = {
type: 'json',
};
export const ID = {
name: 'id',
type: 'bigint',
isGenerated: true,
generationStrategy: 'increment' as 'uuid' | 'increment' | 'rowid',
isNullable: false,
isPrimary: true,
length: '64',
};
export const STRING_ID = {
name: 'id',
type: 'varchar',
isNullable: false,
isPrimary: true,
length: '32',
};
export const Version = {
name: 'version',
type: 'int',
comment: '版本号,事务乐观锁使用',
default: 0,
...NOT_NULL,
};
export const CreatedAt = {
name: 'created_at',
...LONG_INT,
comment: '创建时间',
default: Date.now(),
...NOT_NULL,
};
export const DeletedAt = {
name: 'deleted_at',
...LONG_INT,
comment: '删除时间',
...NULLABLE,
};
export const UpdatedAt = {
name: 'updated_at',
...LONG_INT,
comment: '修改时间',
default: Date.now(),
...NOT_NULL,
};
export const UserId = {
name: 'user_id',
comment: '用户 ID',
length: '32',
...NOT_NULL,
...VARCHAR,
};
export const TenantId = {
name: 'tenant_id',
comment: '租户 ID',
length: '32',
...NOT_NULL,
...VARCHAR,
};
export const TABLE_DEFINITION = {};
export function chunkArray(arr: any[], len: number) {
const chunks: any[] = [];
let i = 0;
const n = arr.length;
while (i < n) {
chunks.push(arr.slice(i, (i += len)));
}
return chunks;
}
export function generateRandomString(length: number = 30) {
let result = '';
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const charactersLength = characters.length;
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
export const generateObjectId = (
m = Math,
d = Date,
h = 16,
s = (s) => m.floor(s).toString(h),
) => s(d.now() / 1000) + ' '.repeat(h).replace(/./g, () => s(m.random() * h));
export function generateSecret() {
const md5 = crypto.createHash('md5');
return md5.update(generateRandomString()).digest('hex');
}
export const sleep = (minsecond: number) =>
new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, minsecond);
});
/**
* @Description: 初始化配置
*/
export const initConfig = () => {
let configFilePassedInCmd = null;
const argv = minimist(process.argv.slice(2));
if (argv?.c) {
configFilePassedInCmd = argv.c;
}
// 解析配置文件
let rawConfigs: any[] = [];
if (configFilePassedInCmd) {
rawConfigs = [path.resolve(configFilePassedInCmd)];
} else {
rawConfigs = [
path.resolve('/etc/skillUpp/config.yaml'),
path.resolve('./config.yaml'),
];
}
rawConfigs = rawConfigs
.filter(Boolean)
.filter(fs.existsSync)
.map((file) => fs.readFileSync(file, 'utf-8'))
.map((content) => yaml.parse(content));
if (rawConfigs.length === 0) {
throw new Error('缺少配置文件:/etc/skillUpp/config.yaml');
}
// 递归合并
// 优先级 env > config.{NODE_ENV}.yaml > config.yaml > //etc/skillUpp/config.yaml > 默认值
const config = [...rawConfigs].reduce((prev, curr) => {
return _.merge(prev, curr);
});
/**
* 获得配置项的值
* @param key 配置项的 key,可以通过 . 来选择子项,比如 app.port
* @param defaultValue 默认值
*/
const env = (key: string, defaultValue?: any) => {
const envKey = key
.split('.')
.map((x) => x.toUpperCase())
.join('_');
return process.env[envKey] || _.get(config, key, defaultValue);
};
return {
region: env('region', 'cn'),
cost: {
url: env('cost.url'),
tenantNo: env('cost.tenantNo'),
productNo: env('cost.productNo'),
},
};
};
/**
* @Description: 生成arn
*/
export const generateArn2 = (
resource: string,
config: { region: string; arnName: string },
) => {
const arn = `arn:${config.region}:${config.arnName}`;
const index = resource.indexOf(':');
if (index === -1) {
// 说明 resource 只有资源类型,没有资源标识符
return `${arn}:${resource}`;
}
const resourceType = resource.substring(0, index);
const resourceId = resource.substring(index + 1);
return `${arn}:${resourceType}:${resourceId}`;
};
import _ from 'lodash';
import { DataSource } from 'typeorm';
import 'dotenv/config'; // 加载 .env 文件
// 递归合并
// 优先级 env > config.{NODE_ENV}.yaml > 默认值
const ENV = process.env;
const database: any = {
type: ENV.APP_DATABASE_TYPE,
url: ENV.APP_DATABASE_URL,
};
const migrationsDir = process.env.MIGRATIONS_DIR || 'migrations';
// The following line works for me. Try this if the above throws an error.
// const migrationsDir = process.env.MIGRATIONS_DIR || 'migrations-build';
// According to the TypeORM doc at https://github.com/typeorm/typeorm/blob/master/docs/migrations.md#running-and-reverting-migrations
// The `migration:run` and `migration:revert` commands only work on `.js` files.
// TypeScript files need to be compiled before running the commands.
export const dataSource = new DataSource({
...database,
synchronize: false,
logging: 'error',
migrations: [__dirname + `/${migrationsDir}/*{.ts,.js}`],
});
......@@ -9,6 +9,7 @@
"node": ">= 20.0.0"
},
"scripts": {
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli -d ./ormconfig.ts",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
......@@ -20,7 +21,10 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
"test:e2e": "jest --config ./test/jest-e2e.json",
"migration:create": "typeorm migration:create ",
"migration:run": "yarn typeorm migration:run",
"migration:revert": "yarn typeorm migration:revert"
},
"dependencies": {
"@nestjs-modules/ioredis": "^2.0.2",
......@@ -42,7 +46,8 @@
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"sha1": "^1.1.1",
"typeorm": "^0.3.20"
"typeorm": "^0.3.20",
"yaml": "^2.7.0"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
......
......@@ -11,6 +11,8 @@ import { UserModule } from './controller/user/user.module';
import { AuthModule } from './controller/auth/auth.module';
import { RedisModule } from '@nestjs-modules/ioredis';
import { TranslateModule } from './controller/translate/translate.module';
import { DeviceModule } from './controller/device/device.module';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
......@@ -27,9 +29,15 @@ import { TranslateModule } from './controller/translate/translate.module';
type: 'single',
url: config.cache.redis.url,
}),
JwtModule.register({
global: true,
secret: config.jwt.secret,
signOptions: { expiresIn: config.jwt.expiresIn },
}),
UserModule,
AuthModule,
TranslateModule,
DeviceModule,
],
controllers: [SystemController],
})
......
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtModule } from '@nestjs/jwt';
import { config } from '@/config';
import { ActiveRecordsEntity } from '@/entities/activeRecords.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserService } from '../user/user.service';
......@@ -10,15 +8,7 @@ import { AuthGuard } from './auth.guard';
import { UserModule } from '../user/user.module';
@Module({
imports: [
JwtModule.register({
global: true,
secret: config.jwt.secret,
signOptions: { expiresIn: config.jwt.expiresIn },
}),
TypeOrmModule.forFeature([ActiveRecordsEntity]),
UserModule,
],
imports: [TypeOrmModule.forFeature([ActiveRecordsEntity]), UserModule],
controllers: [AuthController],
providers: [AuthService, UserService, AuthGuard],
exports: [AuthService, AuthGuard],
......
import { Test, TestingModule } from '@nestjs/testing';
import { DeviceController } from './device.controller';
import { DeviceService } from './device.service';
describe('DeviceController', () => {
let controller: DeviceController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DeviceController],
providers: [DeviceService],
}).compile();
controller = module.get<DeviceController>(DeviceController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});
import {
Controller,
Post,
Body,
Param,
Delete,
Get,
UseInterceptors,
UseFilters,
} from '@nestjs/common';
import { DeviceService } from './device.service';
import { CreateDeviceDto } from './dto/create-device.dto';
import { DeleteDeviceDto } from './dto/delete-device.dto';
import { BadRequestError } from '@/common/exception/badRequest/BadRequestError';
import { JwtService } from '@nestjs/jwt';
import { UnauthorizedError } from '@/common/exception/unauthorized/UnauthorizedError';
import { ApiResponseInterceptor } from '@/common/interceptor/api.response.interceptor';
import { ApiExceptionsFilter } from '@/common/filter/api.exception.filter';
@Controller('')
@UseInterceptors(ApiResponseInterceptor)
@UseFilters(ApiExceptionsFilter)
export class DeviceController {
constructor(
private readonly deviceService: DeviceService,
private readonly jwtService: JwtService,
) {}
@Post('/device/register')
async create(@Body() dto: CreateDeviceDto) {
const exist = await this.deviceService.findBySn(dto.sn);
if (exist) {
return BadRequestError.default('device is exist');
}
return await this.deviceService.create(dto);
}
@Get('/device/getToken')
async getToken(@Param('sn') sn: string) {
const exist = await this.deviceService.findBySn(sn);
if (exist?.deleted === true) {
throw UnauthorizedError.default();
}
const payload = { sub: sn, username: exist.name };
return await this.jwtService.signAsync(payload);
}
@Delete('/device/delete')
async remove(@Body() dto: DeleteDeviceDto) {
return this.deviceService.remove(dto.sn);
}
}
import { Module } from '@nestjs/common';
import { DeviceService } from './device.service';
import { DeviceController } from './device.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { TranslateDeviceEntity } from '@/entities/translate-device.entity';
@Module({
imports: [TypeOrmModule.forFeature([TranslateDeviceEntity])],
controllers: [DeviceController],
providers: [DeviceService],
})
export class DeviceModule {}
import { Test, TestingModule } from '@nestjs/testing';
import { DeviceService } from './device.service';
describe('DeviceService', () => {
let service: DeviceService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DeviceService],
}).compile();
service = module.get<DeviceService>(DeviceService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
import { Injectable } from '@nestjs/common';
import { CreateDeviceDto } from './dto/create-device.dto';
import { InjectRepository } from '@nestjs/typeorm';
import {
DeviceType,
TranslateDeviceEntity,
} from '@/entities/translate-device.entity';
import { Repository } from 'typeorm';
@Injectable()
export class DeviceService {
constructor(
@InjectRepository(TranslateDeviceEntity)
private deviceRepository: Repository<TranslateDeviceEntity>,
) {}
async create(dto: CreateDeviceDto): Promise<TranslateDeviceEntity> {
const device = new TranslateDeviceEntity();
device.sn = dto.sn;
device.name = dto?.name || `device-${dto.sn}`;
device.type = DeviceType.HEADSET;
device.createdAt = new Date();
device.updatedAt = new Date();
return await this.deviceRepository.save(dto);
}
async findBySn(sn: string) {
return await this.deviceRepository.findOne({
where: { sn, deleted: false },
});
}
async remove(sn: string) {
return await this.deviceRepository.update({ deleted: true }, { sn });
}
}
import { DeviceType } from '@/entities/translate-device.entity';
import { ApiProperty } from '@nestjs/swagger';
import { IsEmpty, IsOptional, IsString } from 'class-validator';
export class CreateDeviceDto {
@ApiProperty({
description: '设备序列号',
type: String,
example: '123456',
})
@IsEmpty()
@IsString()
sn: string;
@ApiProperty({
description: '设备名称',
type: String,
example: '设备1',
})
@IsOptional()
@IsEmpty()
@IsString()
name?: string;
@ApiProperty({
description: '设备类型',
enum: DeviceType,
example: DeviceType.HEADSET,
})
@IsEmpty()
@IsString()
type: DeviceType;
}
import { ApiProperty } from '@nestjs/swagger';
export class DeleteDeviceDto {
@ApiProperty({
description: '设备序列号',
type: String,
example: '123456',
})
sn: string;
}
......@@ -23,7 +23,6 @@ import { FileInterceptor } from '@nestjs/platform-express';
import { TranslateImageReqDto } from './dto/req/translateImageReq.dto';
import { TranslateProgressReqDto } from './dto/req/translateProgressReq.dto';
import { ApiResponseInterceptor } from '@/common/interceptor/api.response.interceptor';
import { Response } from 'express';
import { TextToSpeechReqDto } from './dto/req/textToSpeechReq.dto';
import { TTS_VOICE_LIST } from '@/common/utils/constants';
import { BadRequestError } from '@/common/exception/badRequest/BadRequestError';
......
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
export enum DeviceType {
HEADSET = 'HEADSET',
}
@Entity('translatedevice')
export class TranslateDeviceEntity {
@PrimaryGeneratedColumn()
id: number;
@Column({
nullable: false,
type: 'varchar',
name: 'sn',
comment: '设备序列号',
})
sn: string;
@Column({
nullable: false,
type: 'varchar',
name: 'name',
comment: '设备名称',
})
name: string;
@Column({
nullable: false,
type: 'varchar',
name: 'type',
comment: '设备类型',
})
type: DeviceType;
@Column({
nullable: false,
name: 'status',
default: 1,
comment: '状态:0禁用、1启用',
})
status: number;
@Column({
nullable: false,
name: 'deleted',
default: 0,
comment: '0未删除、1已删除',
})
deleted: boolean;
@Column({
nullable: false,
name: 'created_at',
comment: '创建时间',
default: Date.now(),
})
createdAt: Date;
@Column({
nullable: false,
name: 'updated_at',
comment: '修改时间',
default: Date.now(),
})
updatedAt: Date;
}
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "./migrations-build",
},
"include": [
"migrations/**/*"
],
"exclude": ["node_modules", "test", "dist", "src"]
}
\ No newline at end of file
......@@ -5474,6 +5474,11 @@ yallist@^3.0.2:
resolved "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd"
integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
yaml@^2.7.0:
version "2.7.0"
resolved "https://registry.npmmirror.com/yaml/-/yaml-2.7.0.tgz#aef9bb617a64c937a9a748803786ad8d3ffe1e98"
integrity sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==
yargs-parser@21.1.1, yargs-parser@^21.1.1:
version "21.1.1"
resolved "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment