初始化

This commit is contained in:
xiaoyi
2024-01-27 19:53:17 +08:00
commit 07dbe71c31
840 changed files with 119152 additions and 0 deletions

BIN
service/src/modules/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,154 @@
import { AppService } from './app.service';
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreateCatsDto } from './dto/createCats.dto';
import { UpdateCatsDto } from './dto/updateCats.dto';
import { DeleteCatsDto } from './dto/deleteCats.dto';
import { QuerCatsDto } from './dto/queryCats.dto';
import { CreateAppDto } from './dto/createApp.dto';
import { UpdateAppDto } from './dto/updateApp.dto';
import { OperateAppDto } from './dto/deleteApp.dto';
import { QuerAppDto } from './dto/queryApp.dto';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { CollectAppDto } from './dto/collectApp.dto';
import { Request } from 'express';
import { CustomAppDto } from './dto/custonApp.dto';
@ApiTags('App')
@Controller('app')
export class AppController {
constructor(private readonly appService: AppService) {}
@Get('queryAppCats')
@ApiOperation({ summary: '获取App分类列表' })
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
appCatsList(@Query() query: QuerCatsDto) {
return this.appService.appCatsList(query);
}
@Get('queryCats')
@ApiOperation({ summary: '用户端获取App分类列表' })
catsList() {
const params: QuerCatsDto = { status: 1, page: 1, size: 1000, name: '' };
return this.appService.appCatsList(params);
}
@Get('queryOneCat')
@ApiOperation({ summary: '用户端获取App分类列表' })
queryOneCats(@Query() query) {
return this.appService.queryOneCat(query);
}
@Post('createAppCats')
@ApiOperation({ summary: '添加App分类' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
createAppCat(@Body() body: CreateCatsDto) {
return this.appService.createAppCat(body);
}
@Post('updateAppCats')
@ApiOperation({ summary: '修改App分类' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
updateAppCats(@Body() body: UpdateCatsDto) {
return this.appService.updateAppCats(body);
}
@Post('delAppCats')
@ApiOperation({ summary: '删除App分类' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
delAppCat(@Body() body: DeleteCatsDto) {
return this.appService.delAppCat(body);
}
@Get('queryApp')
@ApiOperation({ summary: '获取App列表' })
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
appList(@Req() req: Request, @Query() query: QuerAppDto) {
return this.appService.appList(req, query);
}
@Get('list')
@ApiOperation({ summary: '客户端获取App' })
list(@Req() req: Request, @Query() query: QuerAppDto) {
return this.appService.frontAppList(req, query);
}
@Post('createApp')
@ApiOperation({ summary: '添加App' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
createApp(@Body() body: CreateAppDto) {
return this.appService.createApp(body);
}
@Post('customApp')
@ApiOperation({ summary: '添加自定义App' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
customApp(@Body() body: CustomAppDto, @Req() req: Request) {
return this.appService.customApp(body, req);
}
@Post('updateApp')
@ApiOperation({ summary: '修改App' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
updateApp(@Body() body: UpdateAppDto) {
return this.appService.updateApp(body);
}
@Post('delApp')
@ApiOperation({ summary: '删除App' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
delApp(@Body() body: OperateAppDto) {
return this.appService.delApp(body);
}
@Post('auditPass')
@ApiOperation({ summary: '审核通过App' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
auditPass(@Body() body: OperateAppDto) {
return this.appService.auditPass(body);
}
@Post('auditFail')
@ApiOperation({ summary: '审核拒绝App' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
auditFail(@Body() body: OperateAppDto) {
return this.appService.auditFail(body);
}
@Post('delMineApp')
@ApiOperation({ summary: '删除个人App' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
delMineApp(@Body() body: OperateAppDto, @Req() req: Request) {
return this.appService.delMineApp(body, req);
}
@Post('collect')
@ApiOperation({ summary: '收藏/取消收藏App' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
collect(@Body() body: CollectAppDto, @Req() req: Request) {
return this.appService.collect(body, req);
}
@Get('mineApps')
@ApiOperation({ summary: '我的收藏' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
mineApps(@Req() req: Request) {
return this.appService.mineApps(req);
}
}

View File

@@ -0,0 +1,39 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'app' })
export class AppEntity extends BaseEntity {
@Column({ unique: true, comment: 'App应用名称' })
name: string;
@Column({ comment: 'App分类Id' })
catId: number;
@Column({ comment: 'App应用描述信息' })
des: string;
@Column({ comment: 'App应用预设场景信息', type: 'text' })
preset: string;
@Column({ comment: 'App应用封面图片', nullable: true })
coverImg: string;
@Column({ comment: 'App应用排序、数字越大越靠前', default: 100 })
order: number;
@Column({ comment: 'App应用是否启用中 0禁用 1启用', default: 1 })
status: number;
@Column({ comment: 'App示例数据', nullable: true, type: 'text' })
demoData: string;
@Column({ comment: 'App应用角色 system user', default: 'system' })
role: string;
@Column({ comment: 'App是否共享到应用广场', default: false })
public: boolean;
@Column({ comment: '用户Id', nullable: true })
userId: number;
}

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppCatsEntity } from './appCats.entity';
import { AppEntity } from './app.entity';
import { UserAppsEntity } from './userApps.entity';
@Module({
imports: [TypeOrmModule.forFeature([AppCatsEntity, AppEntity, UserAppsEntity])],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

View File

@@ -0,0 +1,318 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { AppCatsEntity } from './appCats.entity';
import { In, IsNull, Like, MoreThan, Not, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { CreateCatsDto } from './dto/createCats.dto';
import { DeleteCatsDto } from './dto/deleteCats.dto';
import { UpdateCatsDto } from './dto/updateCats.dto';
import { QuerCatsDto } from './dto/queryCats.dto';
import { CreateAppDto } from './dto/createApp.dto';
import { UpdateAppDto } from './dto/updateApp.dto';
import { OperateAppDto } from './dto/deleteApp.dto';
import { QuerAppDto } from './dto/queryApp.dto';
import { AppEntity } from './app.entity';
import { CollectAppDto } from './dto/collectApp.dto';
import { UserAppsEntity } from './userApps.entity';
import { Request } from 'express';
import { CustomAppDto } from './dto/custonApp.dto';
@Injectable()
export class AppService {
constructor(
@InjectRepository(AppCatsEntity)
private readonly appCatsEntity: Repository<AppCatsEntity>,
@InjectRepository(AppEntity)
private readonly appEntity: Repository<AppEntity>,
@InjectRepository(UserAppsEntity)
private readonly userAppsEntity: Repository<UserAppsEntity>,
) {}
async createAppCat(body: CreateCatsDto) {
const { name } = body;
const c = await this.appCatsEntity.findOne({ where: { name } });
if (c) {
throw new HttpException('该分类名称已存在!', HttpStatus.BAD_REQUEST);
}
return await this.appCatsEntity.save(body);
}
async delAppCat(body: DeleteCatsDto) {
const { id } = body;
const c = await this.appCatsEntity.findOne({ where: { id } });
if (!c) {
throw new HttpException('该分类不存在!', HttpStatus.BAD_REQUEST);
}
const count = await this.appEntity.count({ where: { catId: id } });
if (count > 0) {
throw new HttpException('该分类下存在App不可删除', HttpStatus.BAD_REQUEST);
}
const res = await this.appCatsEntity.delete(id);
if (res.affected > 0) return '删除成功';
throw new HttpException('删除失败!', HttpStatus.BAD_REQUEST);
}
async updateAppCats(body: UpdateCatsDto) {
const { id, name } = body;
const c = await this.appCatsEntity.findOne({ where: { name, id: Not(id) } });
if (c) {
throw new HttpException('该分类名称已存在!', HttpStatus.BAD_REQUEST);
}
const res = await this.appCatsEntity.update({ id }, body);
if (res.affected > 0) return '修改成功';
throw new HttpException('修改失败!', HttpStatus.BAD_REQUEST);
}
async queryOneCat(params){
const {id} = params
if(!id){
throw new HttpException('缺失必要参数!', HttpStatus.BAD_REQUEST);
}
const app = await this.appEntity.findOne({where: {id}})
const { demoData: demo, coverImg, des, name } = app
return {
demoData: demo ? demo.split('\n') : [],
coverImg,
des,
name
}
}
async appCatsList(query: QuerCatsDto) {
const { page = 1, size = 10, name, status } = query;
const where: any = {};
name && (where.name = Like(`%${name}%`));
[0, 1, '0', '1'].includes(status) && (where.status = status);
const [rows, count] = await this.appCatsEntity.findAndCount({
where,
order: { order: 'DESC' },
skip: (page - 1) * size,
take: size,
});
// 查出所有分类下对应的App数量
const catIds = rows.map((item) => item.id);
const apps = await this.appEntity.find({ where: { catId: In(catIds) } });
const appCountMap = {};
apps.forEach((item) => {
if (appCountMap[item.catId]) {
appCountMap[item.catId] += 1;
} else {
appCountMap[item.catId] = 1;
}
});
rows.forEach((item: any) => (item.appCount = appCountMap[item.id] || 0));
return { rows, count };
}
async appList(req: Request, query: QuerAppDto, orderKey = 'id') {
const { page = 1, size = 10, name, status, catId, role } = query;
const where: any = {};
name && (where.name = Like(`%${name}%`));
catId && (where.catId = catId);
role && (where.role = role);
status && (where.status = status);
const [rows, count] = await this.appEntity.findAndCount({
where,
order: { [orderKey]: 'DESC' },
skip: (page - 1) * size,
take: size,
});
const catIds = rows.map((item) => item.catId);
const cats = await this.appCatsEntity.find({ where: { id: In(catIds) } });
rows.forEach((item: any) => {
const cat = cats.find((c) => c.id === item.catId);
item.catName = cat ? cat.name : '';
});
if (req?.user?.role !== 'super') {
rows.forEach((item: any) => {
delete item.preset;
});
}
return { rows, count };
}
async frontAppList(req: Request, query: QuerAppDto, orderKey = 'id') {
const { page = 1, size = 1000, name, catId, role } = query;
const where: any = [
{ status: In([1, 4]), userId: IsNull(), public: false },
{ userId: MoreThan(0), public: true },
];
const [rows, count] = await this.appEntity.findAndCount({
where,
order: { order: 'DESC' },
skip: (page - 1) * size,
take: size,
});
const catIds = rows.map((item) => item.catId);
const cats = await this.appCatsEntity.find({ where: { id: In(catIds) } });
rows.forEach((item: any) => {
const cat = cats.find((c) => c.id === item.catId);
item.catName = cat ? cat.name : '';
});
if (req?.user?.role !== 'super') {
rows.forEach((item: any) => {
delete item.preset;
});
}
return { rows, count };
}
async createApp(body: CreateAppDto) {
const { name, catId } = body;
body.role = 'system';
const a = await this.appEntity.findOne({ where: { name } });
if (a) {
throw new HttpException('该应用名称已存在!', HttpStatus.BAD_REQUEST);
}
const c = await this.appCatsEntity.findOne({ where: { id: catId } });
if (!c) {
throw new HttpException('该分类不存在!', HttpStatus.BAD_REQUEST);
}
return await this.appEntity.save(body);
}
async customApp(body: CustomAppDto, req: Request) {
const { id } = req.user;
const { name, catId, des, preset, coverImg, demoData, public: isPublic, appId } = body;
if (appId) {
const a = await this.appEntity.findOne({ where: { id: appId, userId: id } });
if (!a) {
throw new HttpException('您正在编辑一个不存在的应用!', HttpStatus.BAD_REQUEST);
}
const data = { name, catId, des, preset, coverImg, demoData, public: isPublic, status: isPublic ? 3 : 1 };
const res = await this.appEntity.update({ id: appId, userId: id }, data);
if (res.affected) {
return '修改成功';
} else {
throw new HttpException('修改失败!', HttpStatus.BAD_REQUEST);
}
}
if (!appId) {
const c = await this.appCatsEntity.findOne({ where: { id: catId } });
if (!c) {
throw new HttpException('该分类不存在!', HttpStatus.BAD_REQUEST);
}
const a = await this.appEntity.findOne({ where: { name } });
if (a) {
throw new HttpException('该应用名称已存在!', HttpStatus.BAD_REQUEST);
}
const data = { name, catId, des, preset, coverImg, status: isPublic ? 3 : 1, demoData, public: isPublic, role: 'user', userId: id };
const res = await this.appEntity.save(data);
const params = { appId: res.id, userId: id, appType: 'user', public: isPublic, status: isPublic ? 3 : 1, catId };
return this.userAppsEntity.save(params);
}
}
async updateApp(body: UpdateAppDto) {
const { id, name, catId, status } = body;
const a = await this.appEntity.findOne({ where: { name, id: Not(id) } });
if (a) {
throw new HttpException('该应用名称已存在!', HttpStatus.BAD_REQUEST);
}
const c = await this.appCatsEntity.findOne({ where: { id: catId } });
if (!c) {
throw new HttpException('该分类不存在!', HttpStatus.BAD_REQUEST);
}
const curApp = await this.appEntity.findOne({ where: { id } });
if (curApp.status !== body.status) {
await this.userAppsEntity.update({ appId: id }, { status });
}
const res = await this.appEntity.update({ id }, body);
if (res.affected > 0) return '修改App信息成功';
throw new HttpException('修改App信息失败', HttpStatus.BAD_REQUEST);
}
async delApp(body: OperateAppDto) {
const { id } = body;
const a = await this.appEntity.findOne({ where: { id } });
if (!a) {
throw new HttpException('该应用不存在!', HttpStatus.BAD_REQUEST);
}
const useApp = await this.userAppsEntity.count({ where: { appId: id } });
if (useApp > 0) {
throw new HttpException('该应用已被用户关联使用中,不可删除!', HttpStatus.BAD_REQUEST);
}
const res = await this.appEntity.delete(id);
if (res.affected > 0) return '删除App成功';
throw new HttpException('删除App失败', HttpStatus.BAD_REQUEST);
}
async auditPass(body: OperateAppDto) {
const { id } = body;
const a = await this.appEntity.findOne({ where: { id, status: 3 } });
if (!a) {
throw new HttpException('该应用不存在!', HttpStatus.BAD_REQUEST);
}
await this.appEntity.update({ id }, { status: 4 });
/* 同步变更useApp status */
await this.userAppsEntity.update({ appId: id }, { status: 4 });
return '应用审核通过';
}
async auditFail(body: OperateAppDto) {
const { id } = body;
const a = await this.appEntity.findOne({ where: { id, status: 3 } });
if (!a) {
throw new HttpException('该应用不存在!', HttpStatus.BAD_REQUEST);
}
await this.appEntity.update({ id }, { status: 5 });
/* 同步变更useApp status */
await this.userAppsEntity.update({ appId: id }, { status: 5 });
return '应用审核拒绝完成';
}
async delMineApp(body: OperateAppDto, req: Request) {
const { id } = body;
const a = await this.appEntity.findOne({ where: { id, userId: req.user.id } });
if (!a) {
throw new HttpException('您正在操作一个不存在的资源!', HttpStatus.BAD_REQUEST);
}
/* 删除app */
await this.appEntity.delete(id);
/* 删除关联的useApp */
await this.userAppsEntity.delete({ appId: id, userId: req.user.id });
return '删除应用成功!';
}
async collect(body: CollectAppDto, req: Request) {
const { appId } = body;
const { id: userId } = req.user;
const historyApp = await this.userAppsEntity.findOne({ where: { appId, userId } });
if (historyApp) {
const r = await this.userAppsEntity.delete({ appId, userId });
if (r.affected > 0) {
return '取消收藏成功!';
} else {
throw new HttpException('取消收藏失败!', HttpStatus.BAD_REQUEST);
}
}
const app = await this.appEntity.findOne({ where: { id: appId } });
const { id, role: appRole, catId } = app;
const collectInfo = { userId, appId: id, catId, appRole, public: true, status: 1 };
await this.userAppsEntity.save(collectInfo);
return '已将应用加入到我的个人工作台!';
}
async mineApps(req: Request, query = { page: 1, size: 30 }) {
const { id } = req.user;
const { page = 1, size = 30 } = query;
const [rows, count] = await this.userAppsEntity.findAndCount({
where: { userId: id, status: In([1, 3, 4, 5]) },
order: { id: 'DESC' },
skip: (page - 1) * size,
take: size,
});
const appIds = rows.map((item) => item.appId);
const appsInfo = await this.appEntity.find({ where: { id: In(appIds) } });
rows.forEach((item: any) => {
const app = appsInfo.find((c) => c.id === item.appId);
item.appName = app ? app.name : '';
item.appRole = app ? app.role : '';
item.appDes = app ? app.des : '';
item.coverImg = app ? app.coverImg : '';
item.demoData = app ? app.demoData : '';
item.preset = app.userId === id ? app.preset : '******';
});
return { rows, count };
}
}

View File

@@ -0,0 +1,21 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'app_cats' })
export class AppCatsEntity extends BaseEntity {
@Column({ unique: true, comment: 'App分类名称' })
name: string;
@Column({ comment: 'App分类描述信息' })
des: string;
@Column({ comment: 'App分类封面图片', nullable: true })
coverImg: string;
@Column({ comment: 'App分类排序、数字越大越靠前', default: 100 })
order: number;
@Column({ comment: 'App分类是否启用中 0禁用 1启用', default: 1 })
status: number;
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class CollectAppDto {
@ApiProperty({ example: 1, description: '要收藏的appId', required: true })
@IsNumber({}, { message: 'ID必须是Number' })
appId: number;
}

View File

@@ -0,0 +1,44 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class CreateAppDto {
@ApiProperty({ example: '前端助手', description: 'app名称', required: true })
@IsDefined({ message: 'app名称是必传参数' })
name: string;
@ApiProperty({ example: 1, description: 'app分类Id', required: true })
@IsDefined({ message: 'app分类Id必传参数' })
catId: number;
@ApiProperty({
example: '适用于编程编码、期望成为您的编程助手',
description: 'app名称详情描述',
required: false,
})
@IsDefined({ message: 'app名称描述是必传参数' })
des: string;
@ApiProperty({ example: '你现在是一个翻译官。接下来我说的所有话帮我翻译成中文', description: '预设的prompt', required: true })
@IsOptional()
preset: string;
@ApiProperty({ example: 'https://xxxx.png', description: '套餐封面图片', required: false })
@IsOptional()
coverImg: string;
@ApiProperty({ example: 100, description: '套餐排序、数字越大越靠前', required: false })
@IsOptional()
order: number;
@ApiProperty({ example: 1, description: '套餐状态 0禁用 1启用', required: true })
@IsNumber({}, { message: '套餐状态必须是Number' })
@IsIn([0, 1, 3, 4, 5], { message: '套餐状态错误' })
status: number;
@ApiProperty({ example: '这是一句示例数据', description: 'app示例数据', required: false })
demoData: string;
@ApiProperty({ example: 'system', description: '创建的角色', required: false })
role: string;
}

View File

@@ -0,0 +1,30 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class CreateCatsDto {
@ApiProperty({ example: '编程助手', description: 'app分类名称', required: true })
@IsDefined({ message: 'app分类名称是必传参数' })
name: string;
@ApiProperty({
example: '适用于编程编码、期望成为您的编程助手',
description: 'app分类名称详情描述',
required: false,
})
@IsDefined({ message: 'app分类名称描述是必传参数' })
des: string;
@ApiProperty({ example: 'https://xxxx.png', description: '套餐封面图片' })
@IsOptional()
coverImg: string;
@ApiProperty({ example: 100, description: '套餐排序、数字越大越靠前', required: false })
@IsOptional()
order: number;
@ApiProperty({ example: 1, description: '套餐状态 0禁用 1启用', required: true })
@IsNumber({}, { message: '套餐状态必须是Number' })
@IsIn([0, 1, 3, 4, 5], { message: '套餐状态错误' })
status: number;
}

View File

@@ -0,0 +1,35 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class CustomAppDto {
@ApiProperty({ example: '前端助手', description: 'app名称', required: true })
name: string;
@ApiProperty({ example: 1, description: 'app分类Id', required: true })
catId: number;
@ApiProperty({
example: '适用于编程编码、期望成为您的编程助手',
description: 'app名称详情描述',
required: false,
})
@IsDefined({ message: 'app名称描述是必传参数' })
des: string;
@ApiProperty({ example: '你现在是一个翻译官。接下来我说的所有话帮我翻译成中文', description: '预设的prompt', required: true })
preset: string;
@ApiProperty({ example: 'https://xxxx.png', description: '套餐封面图片', required: false })
coverImg: string;
@ApiProperty({ example: '这是一句示例数据', description: 'app示例数据', required: false })
demoData: string;
@ApiProperty({ example: false, description: '是否共享到所有人', required: false })
public: boolean;
@ApiProperty({ example: 1, description: '应用ID', required: false })
@IsOptional()
appId: number;
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class OperateAppDto {
@ApiProperty({ example: 1, description: '要删除的appId', required: true })
@IsNumber({}, { message: 'ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class DeleteCatsDto {
@ApiProperty({ example: 1, description: '要删除app分类Id', required: true })
@IsNumber({}, { message: 'ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,30 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class QuerAppDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 'name', description: 'app名称', required: false })
@IsOptional()
name: string;
@ApiProperty({ example: 1, description: 'app状态 0禁用 1启用 3:审核加入广场中 4已拒绝加入广场', required: false })
@IsOptional()
status: number;
@ApiProperty({ example: 2, description: 'app分类Id', required: false })
@IsOptional()
catId: number;
@ApiProperty({ example: 'role', description: 'app角色', required: false })
@IsOptional()
role: string;
}

View File

@@ -0,0 +1,22 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class QuerCatsDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 'name', description: '分类名称', required: false })
@IsOptional()
name: string;
@ApiProperty({ example: 1, description: '分类状态 0禁用 1启用', required: false })
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,10 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { CreateAppDto } from './createApp.dto';
export class UpdateAppDto extends CreateAppDto {
@ApiProperty({ example: 1, description: '要修改的分类Id', required: true })
@IsNumber({}, { message: '分类ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,10 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { CreateCatsDto } from './createCats.dto';
export class UpdateCatsDto extends CreateCatsDto {
@ApiProperty({ example: 1, description: '要修改的分类Id', required: true })
@IsNumber({}, { message: '分类ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,27 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'user_apps' })
export class UserAppsEntity extends BaseEntity {
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '应用ID' })
appId: number;
@Column({ comment: '应用分类ID' })
catId: number;
@Column({ comment: 'app类型 system/user', default: 'user' })
appType: string;
@Column({ comment: '是否公开到公告菜单', default: false })
public: boolean;
@Column({ comment: 'app状态 1正常 2审核 3违规', default: 1 })
status: number;
@Column({ comment: 'App应用排序、数字越大越靠前', default: 100 })
order: number;
}

View File

@@ -0,0 +1,102 @@
import { VerifyCodeDto } from '../verification/dto/verifyCode.dto';
import { UserLoginDto } from './dto/authLogin.dto';
import { Controller, Post, UseGuards, Body, Get, Query, Render, Res, Req } from '@nestjs/common';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { AuthService } from './auth.service';
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
import { UserRegisterDto } from './dto/authRegister.dto';
import { Request, Response } from 'express';
import { UpdatePasswordDto } from './dto/updatePassword.dto';
import { UpdatePassByOtherDto } from './dto/updatePassByOther.dto';
import { SendPhoneCodeDto } from './dto/sendPhoneCode.dto';
import { UserRegisterByPhoneDto } from './dto/userRegisterByPhone.dto';
import { LoginByPhoneDto } from './dto/loginByPhone.dt';
import { AdminLoginDto } from './dto/adminLogin.dto';
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@Post('register')
@ApiOperation({ summary: '用户注册' })
async register(@Body() body: UserRegisterDto, @Req() req: Request) {
return await this.authService.register(body, req);
}
@Post('registerByPhone')
@ApiOperation({ summary: '用户通过手机号注册' })
async registerByPhone(@Body() body: UserRegisterByPhoneDto, @Req() req: Request) {
return await this.authService.registerByPhone(body, req);
}
@Post('login')
@ApiOperation({ summary: '用户登录' })
async login(@Body() body: UserLoginDto, @Req() req: Request) {
return this.authService.login(body, req);
}
@Post('loginByPhone')
@ApiOperation({ summary: '用户手机号登录' })
async loginByPhone(@Body() body: LoginByPhoneDto, @Req() req: Request) {
return this.authService.loginByPhone(body, req);
}
@Post('updatePassword')
@ApiOperation({ summary: '用户更改密码' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async updatePassword(@Req() req: Request, @Body() body: UpdatePasswordDto) {
return this.authService.updatePassword(req, body);
}
@Post('updatePassByOther')
@ApiOperation({ summary: '用户更改密码' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async updatePassByOther(@Req() req: Request, @Body() body: UpdatePassByOtherDto) {
return this.authService.updatePassByOther(req, body);
}
@Get('getInfo')
@ApiOperation({ summary: '获取用户个人信息' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async getInfo(@Req() req: Request) {
return this.authService.getInfo(req);
}
@Get('activateAccount')
@ApiOperation({ summary: '账户激活' })
async activateAccount(@Query() parmas: VerifyCodeDto, @Res() res: Response) {
return this.authService.activateAccount(parmas, res);
}
@Get('registerSuccess')
@ApiOperation({ summary: '注册成功页面' })
@Render('registerSuccess')
async registerSuccess(@Query() parmas) {
const { username, id, email, teamName, registerSuccessEmailTitle, registerSuccessEmailTeamName, registerSuccessEmaileAppend } = parmas;
return { username, id, email, teamName, registerSuccessEmailTitle, registerSuccessEmailTeamName, registerSuccessEmaileAppend };
}
@Get('registerError')
@ApiOperation({ summary: '注册失败页面' })
@Render('registerError')
async registerError(@Query() parmas) {
const { message, teamName, registerFailEmailTitle, registerFailEmailTeamName } = parmas;
return { message, teamName, registerFailEmailTitle, registerFailEmailTeamName };
}
@Post('captcha')
@ApiOperation({ summary: '获取一个图形验证码' })
async captcha(@Body() parmas) {
return this.authService.captcha(parmas);
}
@Post('sendPhoneCode')
@ApiOperation({ summary: '发送手机验证码' })
async sendPhoneCode(@Body() parmas: SendPhoneCodeDto) {
return this.authService.sendPhoneCode(parmas);
}
}

View File

@@ -0,0 +1,60 @@
import { VerifycationEntity } from '../verification/verifycation.entity';
import { TypeOrmModule } from '@nestjs/typeorm';
import { VerificationService } from '../verification/verification.service';
import { MailerService } from '../mailer/mailer.service';
import { ConfigService, ConfigModule } from 'nestjs-config';
import { AuthController } from './auth.controller';
import { Global, Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { UserModule } from '../user/user.module';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { JwtStrategy } from '@/common/auth/jwt.strategy';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { BalanceEntity } from '../userBalance/balance.entity';
import { AccountLogEntity } from '../userBalance/accountLog.entity';
import { ConfigEntity } from '../globalConfig/config.entity';
import { CramiPackageEntity } from '../crami/cramiPackage.entity';
import { RedisCacheService } from '../redisCache/redisCache.service';
import { RedisCacheModule } from '../redisCache/redisCache.module';
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
import { SalesUsersEntity } from '../sales/salesUsers.entity';
import { UserEntity } from '../user/user.entity';
import { WhiteListEntity } from '../chatgpt/whiteList.entity';
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
import { ChatLogEntity } from '../chatLog/chatLog.entity';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { MidjourneyEntity } from '../midjourney/midjourney.entity';
@Global()
@Module({
imports: [
UserModule,
PassportModule.register({ defaultStrategy: 'jwt' }),
JwtModule.registerAsync({
useFactory: async (configService: ConfigService) => configService.get('jwt'),
inject: [ConfigService],
}),
TypeOrmModule.forFeature([
VerifycationEntity,
BalanceEntity,
AccountLogEntity,
ConfigEntity,
CramiPackageEntity,
RedisCacheModule,
UserBalanceEntity,
SalesUsersEntity,
UserEntity,
WhiteListEntity,
FingerprintLogEntity,
ChatLogEntity,
ChatGroupEntity,
MidjourneyEntity
]),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, JwtAuthGuard, MailerService, VerificationService, UserBalanceService, RedisCacheService],
exports: [AuthService],
})
export class AuthModule {}

View File

@@ -0,0 +1,274 @@
import { LoginByPhoneDto } from './dto/loginByPhone.dt';
import { GlobalConfigService } from '@/modules/globalConfig/globalConfig.service';
import { VerifycationEntity } from '../verification/verifycation.entity';
import { VerificationEnum } from '@/common/constants/verification.constant';
import { VerificationService } from '../verification/verification.service';
import { VerifyCodeDto } from '../verification/dto/verifyCode.dto';
import { UserLoginDto } from './dto/authLogin.dto';
import { UserEntity } from '../user/user.entity';
import { Injectable, HttpException, HttpStatus, Logger, OnModuleInit } from '@nestjs/common';
import { Request, Response } from 'express';
import { JwtService } from '@nestjs/jwt';
import { compareSync } from 'bcryptjs';
import { UserService } from '../user/user.service';
import { UserRegisterDto } from './dto/authRegister.dto';
import { MailerService } from '../mailer/mailer.service';
import { SentMessageInfo } from 'nodemailer';
import { UserStatusEnum, UserStatusErrMsg } from '@/common/constants/user.constant';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { UpdatePasswordDto } from './dto/updatePassword.dto';
import { ConfigEntity } from '../globalConfig/config.entity';
import { In, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { createRandomCode, createRandomUid, getClientIp, isExpired } from '@/common/utils';
import { VerificationUseStatusEnum } from '@/common/constants/status.constant';
import * as os from 'os';
import * as fetch from 'isomorphic-fetch';
import { RedisCacheService } from '../redisCache/redisCache.service';
import { UpdatePassByOtherDto } from './dto/updatePassByOther.dto';
import * as svgCaptcha from 'svg-captcha';
import { SendPhoneCodeDto } from './dto/sendPhoneCode.dto';
import { UserRegisterByPhoneDto } from './dto/userRegisterByPhone.dto';
import * as bcrypt from 'bcryptjs';
import { AdminLoginDto } from './dto/adminLogin.dto';
@Injectable()
export class AuthService {
private ipAddress: string;
constructor(
@InjectRepository(ConfigEntity)
private readonly configEntity: Repository<ConfigEntity>,
private userService: UserService,
private jwtService: JwtService,
private mailerService: MailerService,
private readonly verificationService: VerificationService,
private readonly userBalanceService: UserBalanceService,
private readonly redisCacheService: RedisCacheService,
private readonly globalConfigService: GlobalConfigService,
) {}
async onModuleInit() {
this.getIp();
}
async register(body: UserRegisterDto, req: Request) {
await this.verificationService.verifyCaptcha(body);
const user: UserEntity = await this.userService.createUserAndVerifycation(body, req);
const { username, email, client, id } = user;
const res: any = { username, email, id };
client && (res.client = client);
return res;
}
// TODO 通过手机号注册
async registerByPhone(body: UserRegisterByPhoneDto, req: Request) {
const { username, password, phone, phoneCode, invitedBy } = body;
/* 校验账号是否重复 */
await this.userService.verifyUserRegisterByPhone(body);
/* 创建mock email 由于初期简历的email为unqie 必须给用户一个默认的邮箱作为唯一身份 */
/* 校验验证码是否过期 */
const nameSpace = await this.globalConfigService.getNamespace();
const key = `${nameSpace}:PHONECODE:${phone}`;
const redisPhoneCode = await this.redisCacheService.get({ key });
if (!redisPhoneCode) {
throw new HttpException('验证码已过期、请重新发送!', HttpStatus.BAD_REQUEST);
}
if (phoneCode !== redisPhoneCode) {
throw new HttpException('验证码填写错误、请重新输入!', HttpStatus.BAD_REQUEST);
}
/* 创建用户 */
const email = `${createRandomUid()}@nine.com`;
const newUser: any = { username, password, phone, invitedBy, email, status: UserStatusEnum.ACTIVE };
const userDefautlAvatar = await this.globalConfigService.getConfigs(['userDefautlAvatar']);
newUser.avatar = userDefautlAvatar;
const hashedPassword = bcrypt.hashSync(password, 10);
newUser.password = hashedPassword;
const u = await this.userService.createUser(newUser);
/* 如果有邀请人 给与充值奖励 */
let inviteUser: UserEntity;
if (invitedBy) {
inviteUser = await this.userService.qureyUserInfoByInviteCode(invitedBy);
}
await this.userBalanceService.addBalanceToNewUser(u.id, inviteUser?.id);
return;
}
async login(user: UserLoginDto, req: Request): Promise<string> {
const u: UserEntity = await this.userService.verifyUserCredentials(user);
const { username, id, email, role, openId, client } = u;
const ip = getClientIp(req);
await this.userService.savaLoginIp(id, ip);
const token = await this.jwtService.sign({ username, id, email, role, openId, client });
await this.redisCacheService.saveToken(id, token);
return token;
}
async loginByPhone(body: LoginByPhoneDto, req: Request): Promise<string> {
const u: UserEntity = await this.userService.verifyUserCredentials(body);
const { username, id, email, role, openId, client } = u;
const ip = getClientIp(req);
await this.userService.savaLoginIp(id, ip);
const { phone } = body;
const token = await this.jwtService.sign({ username, id, email, role, openId, client, phone });
await this.redisCacheService.saveToken(id, token);
return token;
}
async loginByOpenId(user: UserEntity, req: Request): Promise<string> {
const { status } = user;
if (status !== UserStatusEnum.ACTIVE) {
throw new HttpException(UserStatusErrMsg[status], HttpStatus.BAD_REQUEST);
}
const { username, id, email, role, openId, client } = user;
const ip = getClientIp(req);
await this.userService.savaLoginIp(id, ip);
const token = await this.jwtService.sign({ username, id, email, role, openId, client });
await this.redisCacheService.saveToken(id, token);
return token;
}
async getInfo(req: Request) {
const { id } = req.user;
return await this.userService.getUserInfo(id);
}
async activateAccount(params: VerifyCodeDto, res: Response) {
const emailConfigs = await this.configEntity.find({
where: {
configKey: In([
'registerSuccessEmailTitle',
'registerSuccessEmailTeamName',
'registerSuccessEmaileAppend',
'registerFailEmailTitle',
'registerFailEmailTeamName',
]),
},
});
const configMap: any = emailConfigs.reduce((pre, cur: any) => {
pre[cur.configKey] = cur.configVal;
return pre;
}, {});
try {
const v: VerifycationEntity = await this.verificationService.verifyCode(params, VerificationEnum.Registration);
const { type, userId } = v;
if (type !== VerificationEnum.Registration) {
throw new HttpException('验证码类型错误', HttpStatus.BAD_REQUEST);
}
const status: number = await this.userService.getUserStatus(userId);
if (status === UserStatusEnum.ACTIVE) {
throw new HttpException('账户已被激活过', HttpStatus.BAD_REQUEST);
}
await this.userService.updateUserStatus(v.userId, UserStatusEnum.ACTIVE);
const u: UserEntity = await this.userService.queryUserInfoById(v.userId);
const { username, email, id, invitedBy } = u;
/* 如果用户填写了 invitedBy 邀请码 查到邀请人信息 */
let inviteUser: UserEntity;
if (invitedBy) {
inviteUser = await this.userService.qureyUserInfoByInviteCode(invitedBy);
}
await this.userBalanceService.addBalanceToNewUser(id, inviteUser?.id);
res.redirect(
`/api/auth/registerSuccess?id=${id.toString().padStart(4, '0')}&username=${username}&email=${email}&registerSuccessEmailTitle=${
configMap.registerSuccessEmailTitle
}&registerSuccessEmailTeamName=${configMap.registerSuccessEmailTeamName}&registerSuccessEmaileAppend=${
configMap.registerSuccessEmaileAppend
}`,
);
} catch (error) {
console.log('error: ', error);
const message = error.response;
res.redirect(
`/api/auth/registerError?message=${message}&registerFailEmailTitle=${configMap.registerFailEmailTitle}&registerFailEmailTeamName=${configMap.registerFailEmailTeamName}`,
);
}
}
async updatePassword(req: Request, body: UpdatePasswordDto) {
const { id, client, role } = req.user;
if (client && Number(client) > 0) {
throw new HttpException('无权此操作、请联系管理员!', HttpStatus.BAD_REQUEST);
}
if (role === 'admin') {
throw new HttpException('非法操作、请联系管理员!', HttpStatus.BAD_REQUEST);
}
const bool = await this.userService.verifyUserPassword(id, body.oldPassword);
if (!bool) {
throw new HttpException('旧密码错误、请检查提交', HttpStatus.BAD_REQUEST);
}
this.userService.updateUserPassword(id, body.password);
return '密码修改成功';
}
async updatePassByOther(req: Request, body: UpdatePassByOtherDto) {
const { id, client } = req.user;
if (!client) {
throw new HttpException('无权此操作!', HttpStatus.BAD_REQUEST);
}
this.userService.updateUserPassword(id, body.password);
return '密码修改成功';
}
getIp() {
let ipAddress: string;
const interfaces = os.networkInterfaces();
Object.keys(interfaces).forEach((interfaceName) => {
const interfaceInfo = interfaces[interfaceName];
for (let i = 0; i < interfaceInfo.length; i++) {
const alias = interfaceInfo[i];
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
ipAddress = alias.address;
break;
}
}
});
this.ipAddress = ipAddress;
}
async captcha(parmas) {
const nameSpace = await this.globalConfigService.getNamespace();
const { color = '#fff' } = parmas;
const captcha = svgCaptcha.createMathExpr({ background: color, height: 34, width: 120, noise: 3 });
const text = captcha.text;
const randomId = createRandomUid();
const key = `${nameSpace}:CAPTCHA:${randomId}`;
await this.redisCacheService.set({ key, val: captcha.text }, 5 * 60);
return {
svgCode: captcha.data,
code: randomId,
};
}
/* 发送验证码 */
async sendPhoneCode(body: SendPhoneCodeDto) {
await this.verificationService.verifyCaptcha(body);
const { phone } = body;
const nameSpace = await this.globalConfigService.getNamespace();
const key = `${nameSpace}:PHONECODE:${phone}`;
const ttl = await this.redisCacheService.ttl(key);
if (ttl && ttl > 0) {
throw new HttpException(`${ttl}秒内不得重复发送短信!`, HttpStatus.BAD_REQUEST);
}
const code = createRandomCode();
const messageInfo = { phone, code };
await this.verificationService.sendPhoneCode(messageInfo);
/* 记录发送的验证码是什么 */
await this.redisCacheService.set({ key, val: code }, 1 * 60);
return '验证码发送成功、请填写验证码完成注册!';
}
/* create token */
createTokenFromFingerprint(fingerprint) {
const token = this.jwtService.sign({
username: `游客${fingerprint}`,
id: fingerprint,
email: `${fingerprint}@nine.com`,
role: 'visitor',
openId: null,
client: null,
});
return token;
}
}

View File

@@ -0,0 +1,18 @@
import { IsNotEmpty, MinLength, MaxLength, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class AdminLoginDto {
@ApiProperty({ example: 'super', description: '邮箱' })
@IsNotEmpty({ message: '用户名不能为空!' })
@MinLength(2, { message: '用户名最短是两位数!' })
@MaxLength(30, { message: '用户名最长不得超过30位' })
@IsOptional()
username?: string;
@ApiProperty({ example: '999999', description: '密码' })
@IsNotEmpty({ message: '用户密码不能为空!' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
}

View File

@@ -0,0 +1,22 @@
import { IsNotEmpty, MinLength, MaxLength, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class UserLoginDto {
@ApiProperty({ example: 'super', description: '邮箱' })
@IsNotEmpty({ message: '用户名不能为空!' })
@MinLength(2, { message: '用户名最短是两位数!' })
@MaxLength(30, { message: '用户名最长不得超过30位' })
@IsOptional()
username?: string;
@ApiProperty({ example: 1, description: '用户ID' })
@IsOptional()
uid?: number;
@ApiProperty({ example: '999999', description: '密码' })
@IsNotEmpty({ message: '用户密码不能为空!' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
}

View File

@@ -0,0 +1,46 @@
import { IsNotEmpty, MinLength, MaxLength, IsEmail, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class UserRegisterDto {
@ApiProperty({ example: 'cooper', description: '用户名称' })
@IsNotEmpty({ message: '用户名不能为空!' })
@MinLength(2, { message: '用户名最低需要大于2位数' })
@MaxLength(12, { message: '用户名不得超过12位' })
username?: string;
@ApiProperty({ example: '123456', description: '用户密码' })
@IsNotEmpty({ message: '用户密码不能为空' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
@ApiProperty({ example: 'J_longyan@163.com', description: '用户邮箱' })
@IsEmail({}, { message: '请填写正确格式的邮箱!' })
@IsNotEmpty({ message: '邮箱不能为空!' })
email: string;
@ApiProperty({ example: '5k3n', description: '图形验证码' })
@IsNotEmpty({ message: '验证码为空!' })
captchaCode: string;
@ApiProperty({ example: '2313ko423ko', description: '图形验证码KEY' })
@IsNotEmpty({ message: '验证ID不能为空' })
captchaId: string;
@ApiProperty({ example: 'FRJDLJHFNV', description: '用户填写的别人邀请码', required: false })
@IsOptional()
invitedBy: string;
@ApiProperty({
example: 'https://public-1300678944.cos.ap-shanghai.myqcloud.com/blog/1682571295452image.png',
description: '用户头像',
required: false,
})
@IsOptional()
avatar: string;
@ApiProperty({ example: 'default', description: '用户注册来源', required: false })
@IsOptional()
client: string;
}

View File

@@ -0,0 +1,16 @@
import { IsNotEmpty, MinLength, MaxLength, IsOptional, IsPhoneNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class LoginByPhoneDto {
@ApiProperty({ example: '19999999', description: '手机号' })
@IsNotEmpty({ message: '手机号不能为空!' })
@IsPhoneNumber('CN', { message: '手机号格式不正确!' })
phone?: string;
@ApiProperty({ example: '999999', description: '密码' })
@IsNotEmpty({ message: '用户密码不能为空!' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
}

View File

@@ -0,0 +1,19 @@
import { IsNotEmpty, MinLength, MaxLength, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class SendPhoneCodeDto {
@ApiProperty({ example: '199999999', description: '手机号' })
@IsNotEmpty({ message: '手机号不能为空' })
@MinLength(11, { message: '手机号长度为11位' })
@MaxLength(11, { message: '手机号长度为11位' })
phone?: string;
@ApiProperty({ example: '2b4i1b4', description: '图形验证码KEY' })
@IsNotEmpty({ message: '验证码KEY不能为空' })
captchaId?: string;
@ApiProperty({ example: '1g4d', description: '图形验证码' })
@IsNotEmpty({ message: '验证码不能为空' })
captchaCode?: string;
}

View File

@@ -0,0 +1,10 @@
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UpdatePassByOtherDto {
@ApiProperty({ example: '666666', description: '三方用户更新新密码' })
@IsNotEmpty({ message: '用户密码不能为空!' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
}

View File

@@ -0,0 +1,16 @@
import { IsNotEmpty, MinLength, MaxLength } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UpdatePasswordDto {
@ApiProperty({ example: '123456', description: '用户旧密码' })
@IsNotEmpty({ message: '用户密码不能为空!' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
oldPassword: string;
@ApiProperty({ example: '666666', description: '用户更新新密码' })
@IsNotEmpty({ message: '用户密码不能为空!' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
}

View File

@@ -0,0 +1,30 @@
import { IsNotEmpty, MinLength, MaxLength, IsEmail, IsOptional, IsPhoneNumber } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class UserRegisterByPhoneDto {
@ApiProperty({ example: 'cooper', description: '用户名称' })
@IsNotEmpty({ message: '用户名不能为空!' })
@MinLength(2, { message: '用户名最低需要大于2位数' })
@MaxLength(12, { message: '用户名不得超过12位' })
username?: string;
@ApiProperty({ example: '123456', description: '用户密码' })
@IsNotEmpty({ message: '用户密码不能为空' })
@MinLength(6, { message: '用户密码最低需要大于6位数' })
@MaxLength(30, { message: '用户密码最长不能超过30位数' })
password: string;
@ApiProperty({ example: '19999999999', description: '用户手机号码' })
@IsPhoneNumber('CN', { message: '手机号码格式不正确!' })
@IsNotEmpty({ message: '手机号码不能为空!' })
phone: string;
@ApiProperty({ example: '152546', description: '手机验证码' })
@IsNotEmpty({ message: '手机验证码不能为空!' })
phoneCode: string;
@ApiProperty({ example: 'SNINE', description: '用户邀请码', required: true })
@IsOptional()
invitedBy: string;
}

View File

@@ -0,0 +1,14 @@
import { Check, Column, Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'auto_reply' })
export class AutoReplyEntity extends BaseEntity {
@Column({ comment: '提问的问题', type: 'text' })
prompt: string;
@Column({ comment: '定义的答案', type: 'text' })
answer: string;
@Column({ default: 1, comment: '启用当前自动回复状态, 0关闭 1启用' })
status: number;
}

View File

@@ -0,0 +1,47 @@
import { AutoreplyService } from './autoreply.service';
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { QueryAutoReplyDto } from './dto/queryAutoReply.dto';
import { AddAutoReplyDto } from './dto/addAutoReply.dto';
import { UpdateAutpReplyDto } from './dto/updateAutoReply.dto';
import { DelAutoReplyDto } from './dto/delBadWords.dto';
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
@ApiTags('autoreply')
@Controller('autoreply')
export class AutoreplyController {
constructor(private readonly autoreplyService: AutoreplyService) {}
@Get('query')
@ApiOperation({ summary: '查询自动回复' })
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
queryAutoreply(@Query() query: QueryAutoReplyDto) {
return this.autoreplyService.queryAutoreply(query);
}
@Post('add')
@ApiOperation({ summary: '添加自动回复' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
addAutoreply(@Body() body: AddAutoReplyDto) {
return this.autoreplyService.addAutoreply(body);
}
@Post('update')
@ApiOperation({ summary: '修改自动回复' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
updateAutoreply(@Body() body: UpdateAutpReplyDto) {
return this.autoreplyService.updateAutoreply(body);
}
@Post('del')
@ApiOperation({ summary: '删除自动回复' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
delAutoreply(@Body() body: DelAutoReplyDto) {
return this.autoreplyService.delAutoreply(body);
}
}

View File

@@ -0,0 +1,14 @@
import { Global, Module } from '@nestjs/common';
import { AutoreplyController } from './autoreply.controller';
import { AutoreplyService } from './autoreply.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AutoReplyEntity } from './autoreplay.entity';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([AutoReplyEntity])],
controllers: [AutoreplyController],
providers: [AutoreplyService],
exports: [AutoreplyService],
})
export class AutoreplyModule {}

View File

@@ -0,0 +1,87 @@
import { HttpException, HttpStatus, Injectable, OnModuleInit } from '@nestjs/common';
import { QueryAutoReplyDto } from './dto/queryAutoReply.dto';
import { AutoReplyEntity } from './autoreplay.entity';
import { Like, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { AddAutoReplyDto } from './dto/addAutoReply.dto';
import { UpdateAutpReplyDto } from './dto/updateAutoReply.dto';
import { DelAutoReplyDto } from './dto/delBadWords.dto';
@Injectable()
export class AutoreplyService implements OnModuleInit {
private autoReplyKes: string[] = [];
private autoReplyMap = {};
private autoReplyFuzzyMatch = true;
constructor(
@InjectRepository(AutoReplyEntity)
private readonly autoReplyEntity: Repository<AutoReplyEntity>,
) {}
async onModuleInit() {
this.loadAutoReplyList();
}
async loadAutoReplyList() {
const res = await this.autoReplyEntity.find({ where: { status: 1 }, select: ['prompt', 'answer'] });
this.autoReplyMap = {};
res.forEach((t) => (this.autoReplyMap[t.prompt] = t.answer));
this.autoReplyKes = Object.keys(this.autoReplyMap);
}
async checkAutoReply(prompt: string) {
let question = prompt;
if (this.autoReplyFuzzyMatch) {
question = this.autoReplyKes.find((item) => item.includes(prompt));
}
return question ? this.autoReplyMap[question] : '';
}
async queryAutoreply(query: QueryAutoReplyDto) {
const { page = 1, size = 10, prompt, status } = query;
const where: any = {};
[0, 1, '0', '1'].includes(status) && (where.status = status);
prompt && (where.prompt = Like(`%${prompt}%`));
const [rows, count] = await this.autoReplyEntity.findAndCount({
where,
skip: (page - 1) * size,
take: size,
order: { id: 'DESC' },
});
return { rows, count };
}
async addAutoreply(body: AddAutoReplyDto) {
const { prompt } = body;
const a = await this.autoReplyEntity.findOne({ where: { prompt } });
if (a) {
throw new HttpException('该问题已存在,请检查您的提交信息', HttpStatus.BAD_REQUEST);
}
await this.autoReplyEntity.save(body);
await this.loadAutoReplyList();
return '添加问题成功!';
}
async updateAutoreply(body: UpdateAutpReplyDto) {
const { id } = body;
const res = await this.autoReplyEntity.update({ id }, body);
if (res.affected > 0) {
await this.loadAutoReplyList();
return '更新问题成功';
}
throw new HttpException('更新失败', HttpStatus.BAD_REQUEST);
}
async delAutoreply(body: DelAutoReplyDto) {
const { id } = body;
const z = await this.autoReplyEntity.findOne({ where: { id } });
if (!z) {
throw new HttpException('该问题不存在,请检查您的提交信息', HttpStatus.BAD_REQUEST);
}
const res = await this.autoReplyEntity.delete({ id });
if (res.affected > 0) {
await this.loadAutoReplyList();
return '删除问题成功';
}
throw new HttpException('删除失败', HttpStatus.BAD_REQUEST);
}
}

View File

@@ -0,0 +1,10 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class AddAutoReplyDto {
@ApiProperty({ example: '你是谁', description: '提问的问题', required: true })
prompt: string;
@ApiProperty({ example: '我是NineAi提供的Ai服务机器人', description: '回答的答案', required: true })
answer: string;
}

View File

@@ -0,0 +1,7 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class DelAutoReplyDto {
@ApiProperty({ example: 1, description: '自动回复id', required: true })
id: number;
}

View File

@@ -0,0 +1,20 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QueryAutoReplyDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: '你是谁', description: '提问问题', required: false })
@IsOptional()
prompt: string;
@ApiProperty({ example: 1, description: '问题状态', required: false })
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,20 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UpdateAutpReplyDto {
@ApiProperty({ example: 1, description: '自动回复id', required: true })
@IsOptional()
id: number;
@ApiProperty({ example: '你可以干嘛', description: '问题', required: false })
@IsOptional()
prompt: string;
@ApiProperty({ example: '我可以干很多事情.......', description: '答案', required: false })
@IsOptional()
answer: string;
@ApiProperty({ example: 0, description: '状态', required: false })
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,56 @@
import { BadwordsService } from './badwords.service';
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { QueryBadWordsDto } from './dto/queryBadWords.dto';
import { QueryViolationDto } from './dto/queryViolation.dto';
import { UpdateBadWordsDto } from './dto/updateBadWords.dto';
import { DelBadWordsDto } from './dto/delBadWords.dto';
import { AddBadWordDto } from './dto/addBadWords.dto';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { Admin } from 'typeorm';
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { Request } from 'express';
@ApiTags('badWords')
@Controller('badwords')
export class BadwordsController {
constructor(private readonly badwordsService: BadwordsService) {}
@Get('query')
@ApiOperation({ summary: '查询所有敏感词' })
queryBadWords(@Query() query: QueryBadWordsDto) {
return this.badwordsService.queryBadWords(query);
}
@Post('del')
@ApiOperation({ summary: '删除敏感词' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
delBadWords(@Body() body: DelBadWordsDto) {
return this.badwordsService.delBadWords(body);
}
@Post('update')
@ApiOperation({ summary: '更新敏感词' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
updateBadWords(@Body() body: UpdateBadWordsDto) {
return this.badwordsService.updateBadWords(body);
}
@Post('add')
@ApiOperation({ summary: '新增敏感词' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
addBadWord(@Body() body: AddBadWordDto) {
return this.badwordsService.addBadWord(body);
}
@Get('violation')
@ApiOperation({ summary: '查询违规记录' })
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
violation(@Req() req: Request, @Query() query: QueryViolationDto) {
return this.badwordsService.violation(req, query);
}
}

View File

@@ -0,0 +1,14 @@
import { Check, Column, Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'bad_words' })
export class BadWordsEntity extends BaseEntity {
@Column({ length: 20, comment: '敏感词' })
word: string;
@Column({ default: 1, comment: '敏感词开启状态' })
status: number;
@Column({ default: 0, comment: '敏感词触发次数' })
count: number;
}

View File

@@ -0,0 +1,16 @@
import { Global, Module } from '@nestjs/common';
import { BadwordsService } from './badwords.service';
import { BadwordsController } from './badwords.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BadWordsEntity } from './badwords.entity';
import { ViolationLogEntity } from './violationLog.entity';
import { UserEntity } from '../user/user.entity';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([BadWordsEntity, ViolationLogEntity, UserEntity])],
providers: [BadwordsService],
controllers: [BadwordsController],
exports: [BadwordsService],
})
export class BadwordsModule {}

View File

@@ -0,0 +1,239 @@
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
import { HttpException, HttpStatus, Injectable, OnModuleInit } from '@nestjs/common';
import { BadWordsEntity } from './badwords.entity';
import { In, Like, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { QueryBadWordsDto } from './dto/queryBadWords.dto';
import { UpdateBadWordsDto } from './dto/updateBadWords.dto';
import { DelBadWordsDto } from './dto/delBadWords.dto';
import { AddBadWordDto } from './dto/addBadWords.dto';
import axios from 'axios';
import { ViolationLogEntity } from './violationLog.entity';
import { UserEntity } from '../user/user.entity';
import { hideString } from '@/common/utils';
@Injectable()
export class BadwordsService implements OnModuleInit {
private badWords: string[];
constructor(
@InjectRepository(BadWordsEntity)
private readonly badWordsEntity: Repository<BadWordsEntity>,
@InjectRepository(ViolationLogEntity)
private readonly violationLogEntity: Repository<ViolationLogEntity>,
@InjectRepository(UserEntity)
private readonly userEntity: Repository<UserEntity>,
private readonly globalConfigService: GlobalConfigService,
) {
this.badWords = [];
}
async onModuleInit() {
this.loadBadWords();
}
/* 敏感词匹配 */
async customSensitiveWords(content, userId) {
const triggeredWords = [];
for (let i = 0; i < this.badWords.length; i++) {
const word = this.badWords[i];
if (content.includes(word)) {
triggeredWords.push(word);
}
}
if (triggeredWords.length) {
await this.recordUserBadWords(userId, content, triggeredWords, ['自定义'], '自定义检测');
const tips = `您提交的信息中包含违规的内容、我们已对您的账户进行标记、请合规使用!`;
throw new HttpException(tips, HttpStatus.BAD_REQUEST);
}
}
/* 敏感词检测 先检测百度敏感词 后检测自定义的 */
async checkBadWords(content: string, userId: number) {
const config = await this.globalConfigService.getSensitiveConfig();
/* 如果有则启动配置检测 没有则跳过 */
if (config) {
await this.checkBadWordsByConfig(content, config, userId);
}
/* 自定义敏感词检测 */
await this.customSensitiveWords(content, userId);
}
/* 通过配置信息去检测敏感词 */
async checkBadWordsByConfig(content: string, config: any, userId) {
const { useType } = config;
useType === 'baidu' && (await this.baiduCheckBadWords(content, config.baiduTextAccessToken, userId));
useType === 'nineai' && (await this.nineaiCheckBadWords(content, config, userId));
}
/* 提取百度云敏感词违规类型 */
extractContent(str) {
const pattern = /存在(.*?)不合规/;
const match = str.match(pattern);
return match ? match[1] : '';
}
/* 通过百度云敏感词检测 */
async baiduCheckBadWords(content: string, accessToken: string, userId: number) {
if (!accessToken) return;
const url = `https://aip.baidubce.com/rest/2.0/solution/v1/text_censor/v2/user_defined?access_token=${accessToken}}`;
const headers = {
'Content-Type': 'application/x-www-form-urlencoded',
Accept: 'application/json',
};
const response = await axios.post(url, { text: content }, { headers });
const { conclusion, error_code, error_msg, conclusionType, data } = response.data;
if (error_code) {
console.log('百度文本检测出现错误、请查看配置信息: ', error_msg);
}
// conclusion 审核结果,可取值:合规、不合规、疑似、审核失败
// conclusionType 1.合规2.不合规3.疑似4.审核失败
if (conclusionType !== 1) {
const types = [...new Set(data.map((item) => this.extractContent(item.msg)))];
await this.recordUserBadWords(userId, content, ['***'], types, '百度云检测');
const tips = `您提交的信息中包含${types.join(',')}的内容、我们已对您的账户进行标记、请合规使用!`;
throw new HttpException(tips, HttpStatus.BAD_REQUEST);
}
}
/* 通过nineai提供的敏感词检测 */
async nineaiCheckBadWords(content: string, config: any, userId) {
const { nineaiBuiltInSensitiveApiBase, nineaiBuiltInSensitiveAuthKey } = config;
if (!nineaiBuiltInSensitiveApiBase || !nineaiBuiltInSensitiveAuthKey) return;
const res = await axios.post(
nineaiBuiltInSensitiveApiBase,
{ content },
{ headers: { 'Content-Type': 'application/json', Authorization: nineaiBuiltInSensitiveAuthKey } },
);
if (!res.data) return;
if (res.data.code !== '0') {
const { msg = '检测失败' } = res.data;
throw new HttpException(`敏感词检测 | ${msg}`, HttpStatus.BAD_REQUEST);
}
if (res.data.word_list && res.data.word_list?.length) {
const words = [...new Set(res.data.word_list.map((t) => t.keyword))];
const types = [...new Set(res.data.word_list.map((t) => t.category))];
await this.recordUserBadWords(userId, content, words, types, 'NineAi检测');
const tips = this.formarTips(res.data.word_list);
throw new HttpException(tips, HttpStatus.BAD_REQUEST);
}
}
/* formarTips */
formarTips(wordList) {
const categorys = wordList.map((t) => t.category);
const unSet = [...new Set(categorys)];
return `您提交的内容中包含${unSet.join(',')}的信息、我们已对您账号进行标记、请合规使用!`;
}
/* 加载自定义的敏感词 */
async loadBadWords() {
const data = await this.badWordsEntity.find({ where: { status: 1 }, select: ['word'] });
this.badWords = data.map((t) => t.word);
}
/* 查询自定义的敏感词 */
async queryBadWords(query: QueryBadWordsDto) {
const { page = 1, size = 500, word, status } = query;
const where: any = {};
[0, 1, '0', '1'].includes(status) && (where.status = status);
word && (where.word = Like(`%${word}%`));
const [rows, count] = await this.badWordsEntity.findAndCount({
where,
skip: (page - 1) * size,
take: size,
order: { id: 'ASC' },
});
return { rows, count };
}
/* 删除自定义敏感词 */
async delBadWords(body: DelBadWordsDto) {
const b = await this.badWordsEntity.findOne({ where: { id: body.id } });
if (!b) {
throw new HttpException('敏感词不存在,请检查您的提交信息', HttpStatus.BAD_REQUEST);
}
const res = await this.badWordsEntity.delete({ id: body.id });
if (res.affected > 0) {
await this.loadBadWords();
return '删除敏感词成功';
} else {
throw new HttpException('删除敏感词失败', HttpStatus.BAD_REQUEST);
}
}
/* 修改自定义敏感词 */
async updateBadWords(body: UpdateBadWordsDto) {
const { id, word, status } = body;
const b = await this.badWordsEntity.findOne({ where: { word } });
if (b) {
throw new HttpException('敏感词已经存在了、请勿重复添加', HttpStatus.BAD_REQUEST);
}
const res = await this.badWordsEntity.update({ id }, { word, status });
if (res.affected > 0) {
await this.loadBadWords();
return '更新敏感词成功';
} else {
throw new HttpException('更新敏感词失败', HttpStatus.BAD_REQUEST);
}
}
async addBadWord(body: AddBadWordDto) {
const { word } = body;
const b = await this.badWordsEntity.findOne({ where: { word } });
if (b) {
throw new HttpException('敏感词已存在,请检查您的提交信息', HttpStatus.BAD_REQUEST);
}
await this.badWordsEntity.save({ word });
await this.loadBadWords();
return '添加敏感词成功';
}
/* 记录用户违规次数内容 */
async recordUserBadWords(userId, content, words, typeCn, typeOriginCn) {
const data = {
userId,
content,
words: JSON.stringify(words),
typeCn: JSON.stringify(typeCn),
typeOriginCn,
};
try {
await this.userEntity
.createQueryBuilder()
.update(UserEntity)
.set({ violationCount: () => 'violationCount + 1' })
.where('id = :userId', { userId })
.execute();
await this.violationLogEntity.save(data);
} catch (error) {
console.log('error: ', error);
}
}
/* 违规记录 */
async violation(req, query) {
const { role } = req.user;
const { page = 1, size = 10, userId, typeOriginCn } = query;
const where = {};
userId && (where['userId'] = userId);
typeOriginCn && (where['typeOriginCn'] = typeOriginCn);
const [rows, count] = await this.violationLogEntity.findAndCount({
where,
skip: (page - 1) * size,
take: size,
order: { id: 'DESC' },
});
const userIds = [...new Set(rows.map((t) => t.userId))];
const usersInfo = await this.userEntity.find({
where: { id: In(userIds) },
select: ['id', 'avatar', 'username', 'email', 'violationCount', 'status'],
});
rows.forEach((t: any) => {
const user: any = usersInfo.find((u) => u.id === t.userId);
role !== 'super' && (user.email = hideString(user.email));
t.userInfo = user;
});
return { rows, count };
}
}

View File

@@ -0,0 +1,7 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class AddBadWordDto {
@ApiProperty({ example: 'test', description: '敏感词', required: true })
word: string;
}

View File

@@ -0,0 +1,7 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class DelBadWordsDto {
@ApiProperty({ example: 1, description: '敏感词id', required: true })
id: number;
}

View File

@@ -0,0 +1,20 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QueryBadWordsDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 'test', description: '敏感词内容', required: false })
@IsOptional()
word: string;
@ApiProperty({ example: 1, description: '关键词状态', required: false })
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,20 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class QueryViolationDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 1, description: '用户ID', required: false })
@IsOptional()
userId: number;
@ApiProperty({ example: '百度云检测', description: '检测平台来源', required: false })
@IsOptional()
typeOriginCn: string;
}

View File

@@ -0,0 +1,16 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UpdateBadWordsDto {
@ApiProperty({ example: 1, description: '敏感词id', required: true })
@IsOptional()
id: number;
@ApiProperty({ example: 'test', description: '敏感词内容', required: false })
@IsOptional()
word: string;
@ApiProperty({ example: 1, description: '关键词状态', required: false })
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,20 @@
import { Check, Column, Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'violation_log' })
export class ViolationLogEntity extends BaseEntity {
@Column({ comment: '用户id' })
userId: number;
@Column({ comment: '违规内容', type: 'text' })
content: string;
@Column({ comment: '敏感词', type: 'text' })
words: string;
@Column({ comment: '违规类型' })
typeCn: string;
@Column({ comment: '违规检测失败于哪个平台' })
typeOriginCn: string;
}

View File

@@ -0,0 +1,54 @@
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ChatGroupService } from './chatGroup.service';
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
import { CreateGroupDto } from './dto/createGroup.dto';
import { Request } from 'express';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { DelGroupDto } from './dto/delGroup.dto';
import { UpdateGroupDto } from './dto/updateGroup.dto';
@ApiTags('group')
@Controller('group')
export class ChatGroupController {
constructor(private readonly chatGroupService: ChatGroupService) {}
@Post('create')
@ApiOperation({ summary: '创建对话组' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
create(@Body() body: CreateGroupDto, @Req() req: Request) {
return this.chatGroupService.create(body, req);
}
@Get('query')
@ApiOperation({ summary: '查询对话组' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
query(@Req() req: Request) {
return this.chatGroupService.query(req);
}
@Post('update')
@ApiOperation({ summary: '更新对话组' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
update(@Body() body: UpdateGroupDto, @Req() req: Request) {
return this.chatGroupService.update(body, req);
}
@Post('del')
@ApiOperation({ summary: '删除对话组' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
del(@Body() body: DelGroupDto, @Req() req: Request) {
return this.chatGroupService.del(body, req);
}
@Post('delAll')
@ApiOperation({ summary: '删除对话组' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
delAll(@Req() req: Request) {
return this.chatGroupService.delAll(req);
}
}

View File

@@ -0,0 +1,24 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Check, Column, Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'chat_group' })
export class ChatGroupEntity extends BaseEntity {
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '是否置顶聊天', type: 'boolean', default: false })
isSticky: boolean;
@Column({ comment: '分组名称', nullable: true })
title: string;
@Column({ comment: '应用ID', nullable: true })
appId: number;
@Column({ comment: '是否删除了', default: false })
isDelete: boolean;
@Column({ comment: '配置', nullable: true, default: null, type: 'text' })
config: string;
}

View File

@@ -0,0 +1,17 @@
import { Global, Module } from '@nestjs/common';
import { ChatGroupController } from './chatGroup.controller';
import { ChatGroupService } from './chatGroup.service';
import { Type } from 'class-transformer';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ChatGroupEntity } from './chatGroup.entity';
import { AppEntity } from '../app/app.entity';
import { ModelsEntity } from '../models/models.entity';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([ChatGroupEntity, AppEntity])],
controllers: [ChatGroupController],
providers: [ChatGroupService],
exports: [ChatGroupService]
})
export class ChatGroupModule {}

View File

@@ -0,0 +1,129 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CreateGroupDto } from './dto/createGroup.dto';
import { Request } from 'express';
import { DelGroupDto } from './dto/delGroup.dto';
import { ChatGroupEntity } from './chatGroup.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Repository } from 'typeorm';
import { UpdateGroupDto } from './dto/updateGroup.dto';
import { AppEntity } from '../app/app.entity';
import { ModelsService } from '../models/models.service'
@Injectable()
export class ChatGroupService {
constructor(
@InjectRepository(ChatGroupEntity)
private readonly chatGroupEntity: Repository<ChatGroupEntity>,
@InjectRepository(AppEntity)
private readonly appEntity: Repository<AppEntity>,
private readonly modelsService: ModelsService
) {}
async create(body: CreateGroupDto, req: Request) {
const { id } = req.user;
const { appId } = body;
const params = { title: '新对话', userId: id };
if (appId) {
const appInfo = await this.appEntity.findOne({ where: { id: appId } });
if (!appInfo) {
throw new HttpException('非法操作、您在使用一个不存在的应用!', HttpStatus.BAD_REQUEST);
} else {
const { status, name } = appInfo;
const g = await this.chatGroupEntity.count({ where: { userId: id, appId, isDelete: false } });
if (g > 0) {
throw new HttpException('当前应用已经开启了一个对话无需新建了!', HttpStatus.BAD_REQUEST);
}
if (![1, 3, 4, 5].includes(status)) {
throw new HttpException('非法操作、您在使用一个未启用的应用!', HttpStatus.BAD_REQUEST);
}
name && (params['title'] = name);
appId && (params['appId'] = appId);
}
}
const modelConfig: any = await this.modelsService.getBaseConfig(appId)
appId && (modelConfig.appId = appId)
if(!modelConfig){
throw new HttpException('管理员未配置任何AI模型、请先联系管理员开通聊天模型配置', HttpStatus.BAD_REQUEST)
}
return await this.chatGroupEntity.save({...params, config: JSON.stringify(modelConfig)});
}
async query(req: Request) {
try {
const { id } = req.user;
const params = { userId: id, isDelete: false };
const res = await this.chatGroupEntity.find({ where: params, order: { isSticky: 'DESC', id: 'DESC' } });
const appIds = res.filter( t => t.appId).map( t => t.appId)
const appInfos = await this.appEntity.find({where: { id: In(appIds)}})
return res.map( (item: any) => {
item.appLogo = appInfos.find( t => t.id === item.appId)?.coverImg
return item
})
} catch (error) {
console.log('error: ', error);
}
}
async update(body: UpdateGroupDto, req: Request) {
const { title, isSticky, groupId, config } = body;
const { id } = req.user;
const g = await this.chatGroupEntity.findOne({ where: { id: groupId, userId: id } });
if (!g) {
throw new HttpException('请先选择一个对话或者新加一个对话再操作!', HttpStatus.BAD_REQUEST);
}
const { appId } = g;
if (appId && !title) {
try {
const parseData = JSON.parse(config)
if(Number(parseData.keyType) !== 1){
throw new HttpException('应用对话名称不能修改哟!', HttpStatus.BAD_REQUEST);
}
} catch (error) {
// ignore
}
}
const data = {};
title && (data['title'] = title);
typeof isSticky !== 'undefined' && (data['isSticky'] = isSticky);
config && (data['config'] = config)
const u = await this.chatGroupEntity.update({ id: groupId }, data);
if (u.affected) {
return true;
} else {
throw new HttpException('更新对话失败!', HttpStatus.BAD_REQUEST);
}
}
async del(body: DelGroupDto, req: Request) {
const { groupId } = body;
const { id } = req.user;
const g = await this.chatGroupEntity.findOne({ where: { id: groupId, userId: id } });
if (!g) {
throw new HttpException('非法操作、您在删除一个非法资源!', HttpStatus.BAD_REQUEST);
}
const r = await this.chatGroupEntity.update({ id: groupId }, { isDelete: true });
if (r.affected) {
return '删除成功';
} else {
throw new HttpException('删除失败!', HttpStatus.BAD_REQUEST);
}
}
/* 删除非置顶开启的所有对话记录 */
async delAll(req: Request) {
const { id } = req.user;
const r = await this.chatGroupEntity.update({ userId: id, isSticky: false, isDelete: false }, { isDelete: true });
if (r.affected) {
return '删除成功';
} else {
throw new HttpException('删除失败!', HttpStatus.BAD_REQUEST);
}
}
/* 通过groupId查询当前对话组的详细信息 */
async getGroupInfoFromId(id){
if(!id) return;
return await this.chatGroupEntity.findOne({where: {id}})
}
}

View File

@@ -0,0 +1,10 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class CreateGroupDto {
@ApiProperty({ example: 10, description: '应用ID', required: false })
@IsOptional()
appId: number;
}

View File

@@ -0,0 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { BaseEntity } from 'typeorm';
export class DelGroupDto {
@ApiProperty({ example: 1, description: '对话分组ID', required: true })
groupId: number;
}

View File

@@ -0,0 +1,21 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class UpdateGroupDto {
@ApiProperty({ example: 1, description: '修改的对话ID', required: false })
@IsOptional()
groupId: number;
@ApiProperty({ example: 10, description: '对话组title', required: false })
@IsOptional()
title: string;
@ApiProperty({ example: 10, description: '对话组是否置顶', required: false })
@IsOptional()
isSticky: boolean;
@ApiProperty({ example: "", description: '对话模型配置项序列化的字符串', required: false })
config: string;
}

BIN
service/src/modules/chatLog/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,91 @@
import { Body, Controller, Get, Post, Query, Req, Res, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { Request, Response } from 'express';
import { ChatLogService } from './chatLog.service';
import { QuerAllDrawLogDto } from './dto/queryAllDrawLog.dto';
import { QuerAllChatLogDto } from './dto/queryAllChatLog.dto';
import { recDrawImgDto } from './dto/recDrawImg.dto';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { QuerMyChatLogDto } from './dto/queryMyChatLog.dto';
import { ExportExcelChatlogDto } from './dto/exportExcelChatlog.dto';
import { ChatListDto } from './dto/chatList.dto';
import { DelDto } from './dto/del.dto';
import { DelByGroupDto } from './dto/delByGroup.dto';
import { QueryByAppIdDto } from './dto/queryByAppId.dto';
@Controller('chatLog')
@ApiTags('ChatLog')
export class ChatLogController {
constructor(private readonly chatLogService: ChatLogService) {}
@Get('draw')
@ApiOperation({ summary: '查询我的绘制记录' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
querDrawLog(@Query() query: QuerMyChatLogDto, @Req() req: Request) {
return this.chatLogService.querDrawLog(req, query);
}
@Post('recDrawImg')
@ApiOperation({ summary: '推荐此图片对外展示' })
@ApiBearerAuth()
@UseGuards(SuperAuthGuard)
recDrawImg(@Body() body: recDrawImgDto) {
return this.chatLogService.recDrawImg(body);
}
@Get('drawAll')
@ApiOperation({ summary: '查询所有的绘制记录' })
querAllDrawLog(@Query() params: QuerAllDrawLogDto) {
return this.chatLogService.querAllDrawLog(params);
}
@Get('chatAll')
@ApiOperation({ summary: '查询所有的问答记录' })
@ApiBearerAuth()
@UseGuards(AdminAuthGuard)
queryAllChatLog(@Query() params: QuerAllChatLogDto, @Req() req: Request) {
return this.chatLogService.querAllChatLog(params, req);
}
@Post('exportExcel')
@ApiOperation({ summary: '导出问答记录' })
@ApiBearerAuth()
exportExcel(@Body() body: ExportExcelChatlogDto, @Res() res: Response) {
return this.chatLogService.exportExcel(body, res);
}
@Get('chatList')
@ApiOperation({ summary: '查询我的问答记录' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
chatList(@Req() req: Request, @Query() params: ChatListDto) {
return this.chatLogService.chatList(req, params);
}
@Post('del')
@ApiOperation({ summary: '删除我的问答记录' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
del(@Req() req: Request, @Body() body: DelDto) {
return this.chatLogService.deleteChatLog(req, body);
}
@Post('delByGroupId')
@ApiOperation({ summary: '清空一组对话' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
delByGroupId(@Req() req: Request, @Body() body: DelByGroupDto) {
return this.chatLogService.delByGroupId(req, body);
}
@Get('byAppId')
@ApiOperation({ summary: '查询某个应用的问答记录' })
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
byAppId(@Req() req: Request, @Query() params: QueryByAppIdDto) {
return this.chatLogService.byAppId(req, params);
}
}

View File

@@ -0,0 +1,78 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Check, Column, Entity, JoinColumn, OneToMany, OneToOne } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'chatlog' })
export class ChatLogEntity extends BaseEntity {
@Column({ comment: '用户ID' })
userId: number;
@Column({ comment: '使用类型', nullable: true })
type: string;
@Column({ comment: '询问的问题', type: 'text', nullable: true })
prompt: string;
@Column({ comment: '回答的答案', type: 'text', nullable: true })
answer: string;
@Column({ comment: '本次问题的token', nullable: true })
promptTokens: number;
@Column({ comment: '本次回答的token', nullable: true })
completionTokens: number;
@Column({ comment: '本次总花费的token', nullable: true })
totalTokens: number;
@Column({ comment: '本次使用的模型', nullable: true })
model: string;
@Column({ comment: '本次访问的Ip地址', nullable: true })
curIp: string;
@Column({ comment: '是否推荐0: 默认 1: 推荐', nullable: true, default: 0 })
rec: number;
@Column({ comment: '扩展参数', nullable: true, type: 'text' })
extend: string;
@Column({ comment: 'mj绘画列表携带的一级id用于图片变换或者放大', nullable: true })
message_id: string;
@Column({ comment: '一组图片的第几张、放大或者变换的时候需要使用', nullable: true })
orderId: number;
@Column({ comment: 'mj绘画的动作、放大或者变换、或者全部重新绘制', nullable: true })
action: string;
@Column({ comment: '是否是组图,这种图才可以指定放大', default: 0 })
group: number;
@Column({ comment: '放大图片的Id记录', nullable: true })
upscaleId: string;
@Column({ comment: '变换图片的Id记录', nullable: true })
variationId: string;
@Column({ comment: '图片信息的string', nullable: true, type: 'text' })
fileInfo: string;
@Column({ comment: 'role system user assistant', nullable: true })
role: string;
@Column({ comment: '对话分组ID', nullable: true })
groupId: number;
@Column({ comment: '序列化的本次会话参数', nullable: true, type: 'text' })
conversationOptions: string;
@Column({ comment: '序列化的本次提交参数', nullable: true, type: 'text' })
requestOptions: string;
@Column({ comment: '是否删除', default: false })
isDelete: boolean;
@Column({ comment: '使用的应用id', nullable: true })
appId: number;
}

View File

@@ -0,0 +1,17 @@
import { Global, Module } from '@nestjs/common';
import { ChatLogService } from './chatLog.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ChatLogEntity } from './chatLog.entity';
import { ChatLogController } from './chatLog.controller';
import { UserEntity } from '../user/user.entity';
import { BadWordsEntity } from '../badwords/badwords.entity';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
@Global()
@Module({
imports: [TypeOrmModule.forFeature([ChatLogEntity, UserEntity, BadWordsEntity, ChatGroupEntity])],
controllers: [ChatLogController],
providers: [ChatLogService],
exports: [ChatLogService],
})
export class ChatLogModule {}

View File

@@ -0,0 +1,284 @@
import { ExportExcelChatlogDto } from './dto/exportExcelChatlog.dto';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { ChatLogEntity } from './chatLog.entity';
import { In, Like, Not, Repository } from 'typeorm';
import { Request, Response } from 'express';
import { DeductionKey } from '@/common/constants/balance.constant';
import { QuerAllDrawLogDto } from './dto/queryAllDrawLog.dto';
import { QuerAllChatLogDto } from './dto/queryAllChatLog.dto';
import { recDrawImgDto } from './dto/recDrawImg.dto';
import { UserEntity } from '../user/user.entity';
import { formatDate, maskEmail, utcToShanghaiTime } from '@/common/utils';
import { QuerMyChatLogDto } from './dto/queryMyChatLog.dto';
import excel from 'exceljs';
import { ChatListDto } from './dto/chatList.dto';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { DelDto } from './dto/del.dto';
import { DelByGroupDto } from './dto/delByGroup.dto';
import { QueryByAppIdDto } from './dto/queryByAppId.dto';
@Injectable()
export class ChatLogService {
constructor(
@InjectRepository(ChatLogEntity)
private readonly chatLogEntity: Repository<ChatLogEntity>,
@InjectRepository(UserEntity)
private readonly userEntity: Repository<UserEntity>,
@InjectRepository(ChatGroupEntity)
private readonly chatGroupEntity: Repository<ChatGroupEntity>,
) {}
/* 记录问答日志 */
async saveChatLog(logInfo) {
return await this.chatLogEntity.save(logInfo);
}
/* 查询我的绘制记录 */
async querDrawLog(req: Request, query: QuerMyChatLogDto) {
const { id } = req.user;
const { model } = query;
const where: any = { userId: id, type: DeductionKey.PAINT_TYPE };
if(model){
where.model = model
if(model === 'DALL-E2'){
where.model = In(['DALL-E2', 'dall-e-3'])
}
}
const data = await this.chatLogEntity.find({
where,
order: { id: 'DESC' },
select: ['id', 'answer', 'prompt', 'message_id', 'group', 'model', 'extend', 'type', 'fileInfo'],
});
data.forEach((r: any) => {
if (r.type === 'paintCount') {
const w = r.model === 'mj' ? 310 : 160;
const imgType = r.answer.includes('cos') ? 'tencent' : 'ali';
const compress = imgType === 'tencent' ? `?imageView2/1/w/${w}/q/55` : `?x-oss-process=image/resize,w_${w}`;
r.thumbImg = r.answer + compress;
try {
r.fileInfo = r.fileInfo ? JSON.parse(r.fileInfo) : null
} catch (error) {
r.fileInfo = {}
}
}
});
return data;
}
/* 查询所有绘制记录 */
async querAllDrawLog(params: QuerAllDrawLogDto) {
const { page = 1, size = 20, rec, userId, model } = params;
const where: any = { type: DeductionKey.PAINT_TYPE, prompt: Not(''), answer: Not('') };
rec && Object.assign(where, { rec });
userId && Object.assign(where, { userId });
if(model){
where.model = model
if(model === 'DALL-E2'){
where.model = In(['DALL-E2', 'dall-e-3'])
}
}
const [rows, count] = await this.chatLogEntity.findAndCount({
order: { id: 'DESC' },
skip: (page - 1) * size,
take: size,
where,
});
rows.forEach((r: any) => {
if (r.type === 'paintCount') {
const w = r.model === 'mj' ? 310 : 160; // mj压缩到310 dall-e压缩到160 宽度
/* 需要区分图片是阿里云oss还是腾讯云cos 压缩方式不同 */
const imgType = r.answer.includes('cos') ? 'tencent' : 'ali';
const compress = imgType === 'tencent' ? `?imageView2/1/w/${w}/q/55` : `?x-oss-process=image/resize,w_${w}`;
r.thumbImg = r.answer + compress;
try {
const detailInfo = r.extend ? JSON.parse(r.extend) : null;
if (detailInfo) {
if (detailInfo) {
r.isGroup = detailInfo?.components[0]?.components.length === 5;
} else {
r.isGroup = false;
}
}
} catch (error) {
console.log('querAllDrawLog Json parse error', error)
}
}
});
return { rows, count };
}
/* 推荐图片到对外展示 */
async recDrawImg(body: recDrawImgDto) {
const { id } = body;
const l = await this.chatLogEntity.findOne({ where: { id, type: DeductionKey.PAINT_TYPE } });
if (!l) {
throw new HttpException('你推荐的图片不存在、请检查!', HttpStatus.BAD_REQUEST);
}
const rec = l.rec === 1 ? 0 : 1;
const res = await this.chatLogEntity.update({ id }, { rec });
if (res.affected > 0) {
return `${rec ? '推荐' : '取消推荐'}图片成功!`;
}
throw new HttpException('你操作的图片不存在、请检查!', HttpStatus.BAD_REQUEST);
}
/* 导出为excel对话记录 */
async exportExcel(body: ExportExcelChatlogDto, res: Response) {
const where = { type: DeductionKey.CHAT_TYPE };
const { page = 1, size = 30, prompt, email } = body;
prompt && Object.assign(where, { prompt: Like(`%${prompt}%`) });
if (email) {
const user = await this.userEntity.findOne({ where: { email } });
user?.id && Object.assign(where, { userId: user.id });
}
const [rows, count] = await this.chatLogEntity.findAndCount({
order: { id: 'DESC' },
skip: (page - 1) * size,
take: size,
where,
});
const userIds = rows.map((r) => r.userId);
const userInfos = await this.userEntity.find({ where: { id: In(userIds) } });
const data = rows.map((r) => {
const userInfo = userInfos.find((u) => u.id === r.userId);
return {
username: userInfo ? userInfo.username : '',
email: userInfo ? userInfo.email : '',
prompt: r.prompt,
answer: r.answer,
createdAt: formatDate(r.createdAt),
};
});
const workbook = new excel.Workbook();
const worksheet = workbook.addWorksheet('chatlog');
worksheet.columns = [
{ header: '用户名', key: 'username', width: 20 },
{ header: '用户邮箱', key: 'email', width: 20 },
{ header: '提问时间', key: 'createdAt', width: 20 },
{ header: '提问问题', key: 'prompt', width: 80 },
{ header: '回答答案', key: 'answer', width: 150 },
];
data.forEach((row) => worksheet.addRow(row));
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', 'attachment; filename=' + 'chat.xlsx');
await workbook.xlsx.write(res);
res.end();
}
/* 查询所有对话记录 */
async querAllChatLog(params: QuerAllChatLogDto, req: Request) {
const { page = 1, size = 20, userId, prompt } = params;
const where = { type: DeductionKey.CHAT_TYPE, prompt: Not('') };
userId && Object.assign(where, { userId });
prompt && Object.assign(where, { prompt: Like(`%${prompt}%`) });
const [rows, count] = await this.chatLogEntity.findAndCount({
order: { id: 'DESC' },
skip: (page - 1) * size,
take: size,
where,
});
const userIds = rows.map((item) => item.userId);
const userInfo = await this.userEntity.find({ where: { id: In(userIds) }, select: ['id', 'username', 'email'] });
rows.forEach((item: any) => {
const { username, email } = userInfo.find((u) => u.id === item.userId) || {};
item.username = username;
item.email = email;
});
req.user.role !== 'super' && rows.forEach((t: any) => (t.email = maskEmail(t.email)));
rows.forEach((item: any) => {
!item.email && (item.email = `${item?.userId}@nine.com`)
!item.username && (item.username = `游客${item?.userId}`);
})
return { rows, count };
}
/* 查询当前对话的列表 */
async chatList(req: Request, params: ChatListDto) {
const { id } = req.user;
const { groupId } = params;
const where = { userId: id, isDelete: false };
groupId && Object.assign(where, { groupId });
if (groupId) {
const count = await this.chatGroupEntity.count({ where: { isDelete: false } });
if (count === 0) return [];
}
const list = await this.chatLogEntity.find({ where });
return list.map((item) => {
const { prompt, role, answer, createdAt, model, conversationOptions, requestOptions, id } = item;
let parseConversationOptions: any = null
let parseRequestOptions: any = null
try {
parseConversationOptions = JSON.parse(conversationOptions)
parseRequestOptions = JSON.parse(requestOptions)
} catch (error) {
}
return {
chatId: id,
dateTime: formatDate(createdAt),
text: role === 'user' ? prompt : answer,
inversion: role === 'user',
error: false,
conversationOptions: parseConversationOptions,
requestOptions: parseRequestOptions,
};
});
}
/* 删除单条对话记录 */
async deleteChatLog(req: Request, body: DelDto) {
const { id: userId } = req.user;
const { id } = body;
const c = await this.chatLogEntity.findOne({ where: { id, userId } });
if (!c) {
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
}
const r = await this.chatLogEntity.update({ id }, { isDelete: true });
if (r.affected > 0) {
return '删除对话记录成功!';
} else {
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
}
}
/* 清空一组对话记录 */
async delByGroupId(req: Request, body: DelByGroupDto) {
const { groupId } = body;
const { id } = req.user;
const g = await this.chatGroupEntity.findOne({ where: { id: groupId, userId: id } });
if (!g) {
throw new HttpException('你删除的对话记录不存在、请检查!', HttpStatus.BAD_REQUEST);
}
const r = await this.chatLogEntity.update({ groupId }, { isDelete: true });
if (r.affected > 0) {
return '删除对话记录成功!';
}
if (r.affected === 0) {
throw new HttpException('当前页面已经没有东西可以删除了!', HttpStatus.BAD_REQUEST);
}
}
/* 查询单个应用的使用记录 */
async byAppId(req: Request, body: QueryByAppIdDto) {
const { id } = req.user;
const { appId, page = 1, size = 10 } = body;
const [rows, count] = await this.chatLogEntity.findAndCount({
where: { userId: id, appId, role: 'assistant' },
order: { id: 'DESC' },
take: size,
skip: (page - 1) * size,
});
return { rows, count };
}
}

View File

@@ -0,0 +1,10 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class ChatListDto {
@ApiProperty({ example: 1, description: '对话分组ID', required: false })
@IsOptional()
groupId: number;
}

View File

@@ -0,0 +1,9 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class DelDto {
@ApiProperty({ example: 1, description: '对话Id', required: true })
id: number;
}

View File

@@ -0,0 +1,9 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class DelByGroupDto {
@ApiProperty({ example: 1, description: '对话组Id', required: true })
groupId: number;
}

View File

@@ -0,0 +1,26 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class ExportExcelChatlogDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: '您好', description: '用户询问的问题', required: false })
@IsOptional()
prompt: string;
@ApiProperty({ example: 'J_longyan@163.com', description: '用户邮箱', required: false })
@IsOptional()
email: string;
// @ApiProperty({ example: '小九', description: '用户名称', required: false })
// @IsOptional()
// username: string;
}

View File

@@ -0,0 +1,25 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class QuerAllChatLogDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 99, description: '对话的用户id', required: false })
@IsOptional()
userId: number;
@ApiProperty({ example: '您好', description: '用户询问的问题', required: false })
@IsOptional()
prompt: string;
@ApiProperty({ example: 'user', description: '身份', required: false })
role: string;
}

View File

@@ -0,0 +1,26 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class QuerAllDrawLogDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 1, description: '是否推荐0: 默认 1: 推荐', required: false })
@IsOptional()
rec: number;
@ApiProperty({ example: 99, description: '生成图片的用户id', required: false })
@IsOptional()
userId: number;
@ApiProperty({ example: 'DALL-E2', description: '生成图片使用的模型', required: false })
@IsOptional()
model: string;
}

View File

@@ -0,0 +1,18 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class QueryByAppIdDto {
@ApiProperty({ example: 1, description: '应用Id', required: true })
@IsOptional()
appId: number;
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
}

View File

@@ -0,0 +1,10 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class QuerMyChatLogDto {
@ApiProperty({ example: 'mj', description: '使用的模型', required: false })
@IsOptional()
model: string;
}

View File

@@ -0,0 +1,9 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class recDrawImgDto {
@ApiProperty({ example: 1, description: '推荐图片的id' })
id: number;
}

View File

@@ -0,0 +1,102 @@
const axios = require('axios')
/* 文档地址 https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Nlks5zkzu */
const getApiModelMaps = () => {
let res = {}
const maps = {
'ERNIE-Bot': 'completions',
'ERNIE-Bot-turbo': 'eb-instant',
'BLOOMZ-7B': 'bloomz_7b1',
'ERNIE-Bot-4': 'completions_pro',
'Llama-2-7b-chat': 'llama_2_7b',
'Llama-2-13b-chat': 'llama_2_13b',
// 'Llama-2-70b-chat': 'llama_2_70b',
'ChatGLM2-6B-32K': 'chatglm2_6b_32k',
// 'Qianfan-BLOOMZ-7B-compressed': 'qianfan_bloomz_7b_compressed',
'Qianfan-Chinese-Llama-2-7B': 'qianfan_chinese_llama_2_7b',
// 'AquilaChat-7B': 'aquilachat_7b'
}
Object.keys(maps).map( key => {
res[`${key.toLowerCase()}`] = maps[key]
})
return res
}
/**
* 生成鉴权签名Access Token
* @return string 鉴权签名信息Access Token
*/
export function getAccessToken(key, secret) {
let url = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${key}&client_secret=${secret}`;
return new Promise((resolve, reject) => {
axios
.post(url)
.then((response) => {
resolve(response.data.access_token);
})
.catch((error) => {
reject(error);
});
});
}
export function sendMessageFromBaidu(messagesHistory, { onProgress, accessToken, model, temperature = 0.95 }) {
const endUrl = getApiModelMaps()[model.trim().toLowerCase()]
return new Promise((resolve, reject) => {
const url = `https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${endUrl}?access_token=${accessToken}`;
var options = {
method: 'POST',
url,
responseType: 'stream',
headers: {
'Content-Type': 'application/json',
},
data: {
stream: true,
messages: messagesHistory,
},
};
axios(options)
.then((response) => {
const stream = response.data;
let resData: any = {};
let cacheChunk = '';
let cacheResText = ''
stream.on('data', (chunk) => {
// 处理每个数据块
const lines = chunk
.toString()
.split('\n\n')
.filter((line) => line.trim() !== '');
for (const line of lines) {
const message = line.replace('data: ', '');
try {
const msg = cacheChunk + message;
const parseData = JSON.parse(msg);
cacheChunk = '';
const { is_end, result } = parseData
result && (cacheResText += result)
if (is_end) {
resData = parseData
resData.text = cacheResText
}
onProgress(parseData);
} catch (error) {
cacheChunk = message;
}
}
});
stream.on('end', () => {
cacheResText = ''
cacheChunk = ''
resolve(resData);
});
})
.catch((error) => {
reject(new Error(error));
});
});
}

View File

@@ -0,0 +1,27 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'chat_box' })
export class ChatBoxEntity extends BaseEntity {
@Column({ comment: '分类ID' })
typeId: number;
@Column({ comment: '应用ID', nullable: true })
appId: number;
@Column({ comment: '快速描述词', nullable: true, type: 'text' })
prompt: string;
@Column({ comment: '标题名称' })
title: string;
@Column({ comment: '排序ID', default: 100 })
order: number;
@Column({ comment: '开启状态', default: true })
status: boolean;
@Column({ comment: '跳转地址' })
url: string;
}

View File

@@ -0,0 +1,18 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'chat_box_type' })
export class ChatBoxTypeEntity extends BaseEntity {
@Column({ comment: '分类名称' })
name: string;
@Column({ comment: 'icon图标' })
icon: string;
@Column({ comment: '排序ID', default: 10 })
order: number;
@Column({ comment: '是否打开', default: true })
status: boolean
}

View File

@@ -0,0 +1,20 @@
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'chat_pre' })
export class ChatPreEntity extends BaseEntity {
@Column({ comment: '分类ID' })
typeId: number;
@Column({ comment: '预设问题描述词', nullable: true, type: 'text' })
prompt: string;
@Column({ comment: '标题名称' })
title: string;
@Column({ comment: '排序ID', default: 100 })
order: number;
@Column({ comment: '开启状态', default: true })
status: boolean;
}

View File

@@ -0,0 +1,18 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'chat_pre_type' })
export class ChatPreTypeEntity extends BaseEntity {
@Column({ comment: '分类名称' })
name: string;
@Column({ comment: 'icon图标', nullable: true })
icon: string;
@Column({ comment: '排序ID', default: 10 })
order: number;
@Column({ comment: '是否打开', default: true })
status: boolean
}

View File

@@ -0,0 +1,192 @@
import { JwtAuthGuard } from '../../common/auth/jwtAuth.guard';
import { ApiBearerAuth, ApiHeader, ApiOperation, ApiTags } from '@nestjs/swagger';
import { ChatgptService } from './chatgpt.service';
import { Body, Controller, Get, HttpCode, Post, Query, Req, Res, UseGuards } from '@nestjs/common';
import { ChatProcessDto } from './dto/chatProcess.dto';
import { Request, Response } from 'express';
import { ChatDrawDto } from './dto/chatDraw.dto';
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
@ApiTags('chatgpt')
@Controller('chatgpt')
export class ChatgptController {
constructor(private readonly chatgptService: ChatgptService, private readonly globalConfigService: GlobalConfigService) {}
@Post('chat-process')
@ApiOperation({ summary: 'gpt聊天对话' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
chatProcess(@Body() body: ChatProcessDto, @Req() req: Request, @Res() res: Response) {
return this.chatgptService.chatProcess(body, req, res);
}
@Post('chat-sync')
@ApiOperation({ summary: 'gpt聊天对话' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
chatProcessSync(@Body() body: ChatProcessDto, @Req() req: Request) {
return this.chatgptService.chatProcess({ ...body }, req);
}
@Post('mj-associate')
@ApiOperation({ summary: 'gpt描述词绘画联想' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async mjAssociate(@Body() body: ChatProcessDto, @Req() req: Request) {
const mjCustomLianxiangPrompt = await this.globalConfigService.getConfigs(['mjCustomLianxiangPrompt']);
/* 临时方案 指定其系统预设词 */
body.systemMessage =
mjCustomLianxiangPrompt ||
`midjourney是一款AI绘画工具只要你输入你想到的文字就能通过人工智能产出相对应的图片、我希望你作为MidJourney程序的提示词(prompt)生成器。你的工作是根据我给你的一段提示内容扩展为更详细和更有创意的描述以激发人工智能的独特和有趣的图像。请记住人工智能能够理解广泛的语言并能解释抽象的概念所以请自由发挥想象力和描述力尽可能地发挥。例如你可以描述一个未来城市的场景或一个充满奇怪生物的超现实景观。你的描述越详细、越有想象力产生的图像就越有趣、Midjourney prompt的标准公式为:(image we're prompting).(5 descriptivekeywords). (camera type). (camera lens type). (time of day)(style of photograph).(type offilm)、请记住这个公式后续统一使用该公式进行prompt生成、最终把我给你的提示变成一整段连续不分开的完整内容并且只需要用英文回复您的联想、一定不要回复别内容、包括解释、我只需要纯粹的内容。`;
return this.chatgptService.chatProcess({ ...body, cusromPrompt: true }, req);
}
@Post('mj-fy')
@ApiOperation({ summary: 'gpt描述词绘画翻译' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async mjFanyi(@Body() body: ChatProcessDto, @Req() req: Request) {
/* 临时方案 指定其系统预设词 */
const mjCustomFanyiPrompt = await this.globalConfigService.getConfigs(['mjCustomFanyiPrompt']);
body.systemMessage =
mjCustomFanyiPrompt ||
`接下来我会给你一些内容、我希望你帮我翻译成英文、不管我给你任何语言、你都回复我英文、如果给你了英文、依然回复我更加优化的英文、并且期望你不需要做任何多余的解释、给我英文即可、不要加任何东西、我只需要英文!`;
return this.chatgptService.chatProcess({ ...body, cusromPrompt: true }, req);
}
@Post('chat-mind')
@ApiOperation({ summary: 'mind思维导图提示' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async chatmind(@Body() body: ChatProcessDto, @Req() req: Request, @Res() res: Response) {
const mindCustomPrompt = await this.globalConfigService.getConfigs(['mindCustomPrompt']);
/* 临时方案 指定其系统预设词 */
body.systemMessage =
mindCustomPrompt ||
`我希望你使用markdown格式回答我得问题、我的需求是得到一份markdown格式的大纲、尽量做的精细、层级多一点、不管我问你什么、都需要您回复我一个大纲出来、我想使用大纲做思维导图、除了大纲之外、不要无关内容和总结。`;
return this.chatgptService.chatProcess({ ...body, cusromPrompt: true }, req, res);
}
@Post('chat-draw')
@ApiOperation({ summary: 'gpt绘画' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async draw(@Body() body: ChatDrawDto, @Req() req: Request) {
// const mjCustomFanyiPrompt = await this.globalConfigService.getConfigs(['mjCustomFanyiPrompt']);
// const systemMessage =
// mjCustomFanyiPrompt ||
// `你只可以回复英文、你是一个中译英翻译官、接下来我会给你一些内容、我希望你帮我翻译成英文、不管我给你任何语言、你都回复我英文、如果给你了英文、请不要做任何改变原样回复给我、并且期望你不需要做任何多余的解释、给我英文即可、不要加任何东西、我只需要英文!`;
// const text = await this.chatgptService.chatProcess({ ...body, systemMessage, cusromPrompt: true }, req);
// console.log('text: ', text);
// if (text) {
// body.prompt = text;
// }
return await this.chatgptService.draw(body, req);
}
@Post('setChatBoxType')
@ApiOperation({ summary: '添加修改分类类型' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async setChatBoxType(@Req() req: Request, @Body() body: any) {
return await this.chatgptService.setChatBoxType(req, body);
}
@Post('delChatBoxType')
@ApiOperation({ summary: '添加修改ChatBoxType' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async delChatBoxType(@Req() req: Request, @Body() body: any) {
return await this.chatgptService.delChatBoxType(req, body);
}
@Get('queryChatBoxTypes')
@ApiOperation({ summary: '查询ChatBoxType' })
@UseGuards(AdminAuthGuard)
async queryChatBoxType() {
return await this.chatgptService.queryChatBoxType();
}
@Post('setChatBox')
@ApiOperation({ summary: '添加修改ChatBox' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async setChatBox(@Req() req: Request, @Body() body: any) {
return await this.chatgptService.setChatBox(req, body);
}
@Post('delChatBox')
@ApiOperation({ summary: '添加修改ChatBox提示词' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async delChatBox(@Req() req: Request, @Body() body: any) {
return await this.chatgptService.delChatBox(req, body);
}
@Get('queryChatBoxs')
@ApiOperation({ summary: '查询ChatBox列表' })
@UseGuards(AdminAuthGuard)
async queryChatBox() {
return await this.chatgptService.queryChatBox();
}
@Get('queryChatBoxFrontend')
@ApiOperation({ summary: '查询ChatBox分类加详细' })
async queryChatBoxFrontend() {
return await this.chatgptService.queryChatBoxFrontend();
}
@Post('setChatPreType')
@ApiOperation({ summary: '添加修改预设分类类型' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async setChatPreType(@Req() req: Request, @Body() body: any) {
return await this.chatgptService.setChatPreType(req, body);
}
@Post('delChatPretype')
@ApiOperation({ summary: '添加修改ChatPretype' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async delChatPreType(@Req() req: Request, @Body() body: any) {
return await this.chatgptService.delChatPreType(req, body);
}
@Get('queryChatPretypes')
@ApiOperation({ summary: '查询ChatPretype' })
@UseGuards(AdminAuthGuard)
async queryChatPreType() {
return await this.chatgptService.queryChatPreType();
}
@Post('setChatPre')
@ApiOperation({ summary: '添加修改ChatPre' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async setChatPre(@Req() req: Request, @Body() body: any) {
return await this.chatgptService.setChatPre(req, body);
}
@Post('delChatPre')
@ApiOperation({ summary: '添加修改ChatPre提示词' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async delChatPre(@Req() req: Request, @Body() body: any) {
return await this.chatgptService.delChatPre(req, body);
}
@Get('queryChatPres')
@ApiOperation({ summary: '查询ChatPre列表' })
@UseGuards(AdminAuthGuard)
async queryChatPre() {
return await this.chatgptService.queryChatPre();
}
@Get('queryChatPreList')
@ApiOperation({ summary: '查询ChatPre列表' })
async queryChatPreList() {
return await this.chatgptService.queryChatPreList();
}
}

View File

@@ -0,0 +1,60 @@
import { Global, Module } from '@nestjs/common';
import { ChatgptController } from './chatgpt.controller';
import { ChatgptService } from './chatgpt.service';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BalanceEntity } from '../userBalance/balance.entity';
import { UserService } from '../user/user.service';
import { UserEntity } from '../user/user.entity';
import { VerificationService } from '../verification/verification.service';
import { VerifycationEntity } from '../verification/verifycation.entity';
import { ChatLogService } from '../chatLog/chatLog.service';
import { ChatLogEntity } from '../chatLog/chatLog.entity';
import { AccountLogEntity } from '../userBalance/accountLog.entity';
import { ConfigEntity } from '../globalConfig/config.entity';
import { GptKeysEntity } from './gptkeys.entity';
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
import { WhiteListEntity } from './whiteList.entity';
import { CramiPackageEntity } from '../crami/cramiPackage.entity';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { AppEntity } from '../app/app.entity';
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
import { SalesUsersEntity } from '../sales/salesUsers.entity';
import { RedisCacheService } from '../redisCache/redisCache.service';
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
import { MidjourneyEntity } from '../midjourney/midjourney.entity';
import { ChatBoxTypeEntity } from './chatBoxType.entity';
import { ChatBoxEntity } from './chatBox.entity';
import { ChatPreTypeEntity } from './chatPreType.entity';
import { ChatPreEntity } from './chatPre.entity';
@Global()
@Module({
imports: [
TypeOrmModule.forFeature([
BalanceEntity,
UserEntity,
VerifycationEntity,
ChatLogEntity,
AccountLogEntity,
ConfigEntity,
GptKeysEntity,
WhiteListEntity,
UserEntity,
CramiPackageEntity,
ChatGroupEntity,
AppEntity,
UserBalanceEntity,
SalesUsersEntity,
FingerprintLogEntity,
MidjourneyEntity,
ChatBoxTypeEntity,
ChatBoxEntity,
ChatPreTypeEntity,
ChatPreEntity
]),
],
controllers: [ChatgptController],
providers: [ChatgptService, UserBalanceService, UserService, VerificationService, ChatLogService, RedisCacheService],
exports: [ChatgptService]
})
export class ChatgptModule {}

View File

@@ -0,0 +1,978 @@
import { UploadService } from './../upload/upload.service';
import { UserService } from './../user/user.service';
import { ConfigService } from 'nestjs-config';
import { HttpException, HttpStatus, Injectable, OnModuleInit, Logger } from '@nestjs/common';
import type { ChatGPTAPIOptions, ChatMessage, SendMessageOptions } from 'chatgpt-nine-ai';
import { Request, Response } from 'express';
import { OpenAiErrorCodeMessage } from '@/common/constants/errorMessage.constant';
import {
compileNetwork,
getClientIp,
hideString,
importDynamic,
isNotEmptyString,
maskEmail,
removeSpecialCharacters,
selectKeyWithWeight,
} from '@/common/utils';
import axios from 'axios';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { DeductionKey } from '@/common/constants/balance.constant';
import { ChatLogService } from '../chatLog/chatLog.service';
import { ChatDrawDto } from './dto/chatDraw.dto';
import * as uuid from 'uuid';
import * as jimp from 'jimp';
import { ConfigEntity } from '../globalConfig/config.entity';
import { In, Like, MoreThan, Repository } from 'typeorm';
import { InjectRepository } from '@nestjs/typeorm';
import { BadwordsService } from '../badwords/badwords.service';
import { AutoreplyService } from '../autoreply/autoreply.service';
import { GptKeysEntity } from './gptkeys.entity';
import { GlobalConfigService } from '../globalConfig/globalConfig.service';
import { FanyiService } from '../fanyi/fanyi.service';
import * as dayjs from 'dayjs';
import { AppEntity } from '../app/app.entity';
import { ChatGroupService } from '../chatGroup/chatGroup.service';
import { ModelsService } from '../models/models.service';
import { sendMessageFromBaidu } from './baidu';
import { addOneIfOdd, unifiedFormattingResponse } from './helper';
import { MessageInfo, NineStore, NineStoreInterface } from './store';
import { sendMessageFromZhipu } from './zhipu';
import { getTokenCount, sendMessageFromOpenAi } from './openai';
import { ChatBoxTypeEntity } from './chatBoxType.entity';
import { ChatBoxEntity } from './chatBox.entity';
import { ChatPreEntity } from './chatPre.entity';
import { ChatPreTypeEntity } from './chatPreType.entity';
interface Key {
id: number;
key: string;
weight: number;
model: string;
maxModelTokens: number;
maxResponseTokens: number;
openaiProxyUrl: string;
openaiTimeoutMs: number;
}
@Injectable()
export class ChatgptService implements OnModuleInit {
constructor(
@InjectRepository(GptKeysEntity)
private readonly gptKeysEntity: Repository<GptKeysEntity>,
@InjectRepository(ConfigEntity)
private readonly configEntity: Repository<ConfigEntity>,
@InjectRepository(ChatBoxTypeEntity)
private readonly chatBoxTypeEntity: Repository<ChatBoxTypeEntity>,
@InjectRepository(ChatBoxEntity)
private readonly chatBoxEntity: Repository<ChatBoxEntity>,
@InjectRepository(AppEntity)
private readonly appEntity: Repository<AppEntity>,
@InjectRepository(ChatPreTypeEntity)
private readonly chatPreTypeEntity: Repository<ChatPreTypeEntity>,
@InjectRepository(ChatPreEntity)
private readonly chatPreEntity: Repository<ChatPreEntity>,
private readonly configService: ConfigService,
private readonly userBalanceService: UserBalanceService,
private readonly chatLogService: ChatLogService,
private readonly userService: UserService,
private readonly uploadService: UploadService,
private readonly badwordsService: BadwordsService,
private readonly autoreplyService: AutoreplyService,
private readonly globalConfigService: GlobalConfigService,
private readonly fanyiService: FanyiService,
private readonly chatGroupService: ChatGroupService,
private readonly modelsService: ModelsService,
) {}
private api;
private nineStore: NineStoreInterface = null; // redis存储
private whiteListUser: number[] = [];
private keyPool: {
list3: Key[];
list4: Key[];
} = {
list3: [],
list4: [],
};
async onModuleInit() {
let chatgpt = await importDynamic('chatgpt-nine-ai');
let KeyvRedis = await importDynamic('@keyv/redis');
let Keyv = await importDynamic('keyv');
chatgpt = chatgpt?.default ? chatgpt.default : chatgpt;
KeyvRedis = KeyvRedis?.default ? KeyvRedis.default : KeyvRedis;
Keyv = Keyv?.default ? Keyv.default : Keyv;
const { ChatGPTAPI, ChatGPTError, ChatGPTUnofficialProxyAPI } = chatgpt;
/* get custom set default config */
const port = +process.env.REDIS_PORT;
const host = process.env.REDIS_HOST;
const password = process.env.REDIS_PASSWORD;
const username = process.env.REDIS_USER;
const redisUrl = `redis://${username || ''}:${password || ''}@${host}:${port}`;
const store = new KeyvRedis(redisUrl);
/* chatgpt-nineai 使用的 可以切换给 store使用 */
const messageStore = new Keyv({ store, namespace: 'nineai-chatlog' });
this.nineStore = new NineStore({ store: messageStore, namespace: 'chat' });
}
/* 整理请求的所有入参 */
async getRequestParams(inputOpt, systemMessage, currentRequestModelKey, modelInfo = null) {
if (!modelInfo) {
modelInfo = (await this.modelsService.getBaseConfig())?.modelInfo;
}
const { timeout = 60 } = currentRequestModelKey;
const { topN: temperature, model } = modelInfo;
const { parentMessageId = 0 } = inputOpt;
/* 根据用户区分不同模型使用不同的key */
const globalTimeoutMs: any = await this.globalConfigService.getConfigs(['openaiTimeoutMs']);
const timeoutMs = timeout * 1000 || globalTimeoutMs || 100 * 1000;
const options: any = {
parentMessageId,
timeoutMs: +timeoutMs,
completionParams: {
model,
temperature: temperature, // 温度 使用什么采样温度,介于 0 和 2 之间。较高的值(如 0.8)将使输出更加随机,而较低的值(如 0.2)将使输出更加集中和确定
},
};
systemMessage && (options.systemMessage = systemMessage);
return options;
}
async chatSyncFree(prompt: string) {
const currentRequestModelKey = await this.modelsService.getRandomDrawKey();
const systemMessage = await this.globalConfigService.getConfigs(['systemPreMessage']);
const { maxModelTokens = 8000, maxResponseTokens = 4096, key, model } = currentRequestModelKey;
const proxyUrl = await this.getModelProxyUrl(currentRequestModelKey);
const { context: messagesHistory } = await this.nineStore.buildMessageFromParentMessageId(prompt, { parentMessageId: '', systemMessage });
try {
const response: any = await sendMessageFromOpenAi(messagesHistory, {
apiKey: removeSpecialCharacters(key),
model,
proxyUrl: proxyUrl,
onProgress: null,
});
return response?.text;
} catch (error) {
console.log('error: ', error);
}
}
/* 有res流回复 没有同步回复 */
async chatProcess(body: any, req: Request, res?: Response) {
const abortController = req.abortController;
const { options = {}, appId, cusromPrompt, systemMessage = '' } = body;
/* 不同场景会变更其信息 */
let setSystemMessage = systemMessage;
const { parentMessageId } = options;
const { prompt } = body;
const { groupId, usingNetwork } = options;
// const { model = 3 } = options;
/* 获取当前对话组的详细配置信息 */
const groupInfo = await this.chatGroupService.getGroupInfoFromId(groupId);
/* 当前对话组关于对话的配置信息 */
const groupConfig = groupInfo?.config ? JSON.parse(groupInfo.config) : await this.modelsService.getBaseConfig();
const { keyType, model, topN: temperature, systemMessage: customSystemMessage, rounds } = groupConfig.modelInfo;
/* 获取到本次需要调用的key */
let currentRequestModelKey = null;
if (!cusromPrompt) {
currentRequestModelKey = await this.modelsService.getCurrentModelKeyInfo(model);
} else {
currentRequestModelKey = await this.modelsService.getRandomDrawKey();
}
if (!currentRequestModelKey) {
throw new HttpException('当前流程所需要的模型已被管理员下架、请联系管理员上架专属模型!', HttpStatus.BAD_REQUEST);
}
const { deduct, isTokenBased, deductType, key: modelKey, secret, modelName, id: keyId, accessToken } = currentRequestModelKey;
/* 用户状态检测 */
await this.userService.checkUserStatus(req.user);
/* 用户余额检测 */
await this.userBalanceService.validateBalance(req, deductType === 1 ? 'model3' : 'model4', deduct);
res && res.setHeader('Content-type', 'application/octet-stream; charset=utf-8');
/* 敏感词检测 */
await this.badwordsService.checkBadWords(prompt, req.user.id);
/* 自动回复 */
const autoReplyRes = await this.autoreplyService.checkAutoReply(prompt);
if (autoReplyRes && res) {
const msg = { message: autoReplyRes, code: 500 };
res.write(JSON.stringify(msg));
return res.end();
}
/* 如果传入了appId 那么appId优先级更高 */
if (appId) {
const appInfo = await this.appEntity.findOne({ where: { id: appId, status: In([1, 3, 4, 5]) } });
if (!appInfo) {
throw new HttpException('你当前使用的应用已被下架、请删除当前对话开启新的对话吧!', HttpStatus.BAD_REQUEST);
}
appInfo.preset && (setSystemMessage = appInfo.preset);
} else if (cusromPrompt) {
// 特殊场景系统预设 在co层直接改写
//自定义提示词 特殊场景 思维导图 翻译 联想 不和头部预设结合
setSystemMessage = systemMessage;
} else if (customSystemMessage) {
// 用户自定义的预设信息
setSystemMessage = customSystemMessage;
} else {
// 走系统默认预设
const currentDate = new Date().toISOString().split('T')[0];
const systemPreMessage = await this.globalConfigService.getConfigs(['systemPreMessage']);
setSystemMessage = systemPreMessage + `\n Current date: ${currentDate}`;
}
let netWorkPrompt = '';
/* 使用联网模式 */
if (usingNetwork) {
netWorkPrompt = await compileNetwork(prompt);
const currentDate = new Date().toISOString().split('T')[0];
const systemPreMessage = await this.globalConfigService.getConfigs(['systemPreMessage']);
setSystemMessage = systemPreMessage + `\n Current date: ${currentDate}`;
}
/* 整理本次请求全部数据 */
const mergedOptions: any = await this.getRequestParams(options, setSystemMessage, currentRequestModelKey, groupConfig.modelInfo);
const { maxModelTokens = 8000, maxResponseTokens = 4096, key } = currentRequestModelKey;
res && res.status(200);
let response = null;
let othersInfo = null;
try {
if (res) {
let lastChat: ChatMessage | null = null;
let isSuccess = false;
/* 如果客户端终止请求、我们只存入终止前获取的内容、并且终止此次请求 拿到最后一次数据 虚构一个结构用户后续信息存入 */
res.on('close', async () => {
if (isSuccess) return;
abortController.abort();
const prompt_tokens = (await getTokenCount(prompt)) || 0;
const completion_tokens = (await getTokenCount(lastChat?.text)) || 0;
const total_tokens = prompt_tokens + completion_tokens;
// TODO 待优化
/* 日志记录 */
const curIp = getClientIp(req);
/* 用户询问 */
await this.chatLogService.saveChatLog({
appId,
curIp,
userId: req.user.id,
type: DeductionKey.CHAT_TYPE,
prompt,
answer: '',
promptTokens: prompt_tokens,
completionTokens: 0,
totalTokens: prompt_tokens,
model: model,
role: 'user',
groupId,
requestOptions: JSON.stringify({
options: null,
prompt,
}),
});
// gpt回答
await this.chatLogService.saveChatLog({
appId,
curIp,
userId: req.user.id,
type: DeductionKey.CHAT_TYPE,
prompt: prompt,
answer: lastChat?.text,
promptTokens: prompt_tokens,
completionTokens: completion_tokens,
totalTokens: total_tokens,
model: model,
role: 'assistant',
groupId,
requestOptions: JSON.stringify({
options: {
model: model,
temperature,
},
prompt,
}),
conversationOptions: JSON.stringify({
conversationId: lastChat?.conversationId,
model: model,
parentMessageId: lastChat?.id,
temperature,
}),
});
/* 当用户回答一般停止时 也需要扣费 */
let charge = deduct;
if (isTokenBased === true) {
charge = deduct * total_tokens;
}
await this.userBalanceService.deductFromBalance(req.user.id, `model${deductType === 1 ? 3 : 4}`, charge, total_tokens);
});
/* openAi */
if (Number(keyType) === 1) {
const { key, maxToken, maxTokenRes, proxyResUrl } = await this.formatModelToken(currentRequestModelKey);
const { parentMessageId, completionParams, systemMessage } = mergedOptions;
const { model, temperature } = completionParams;
const { context: messagesHistory } = await this.nineStore.buildMessageFromParentMessageId(usingNetwork ? netWorkPrompt : prompt, {
parentMessageId,
systemMessage,
maxModelToken: maxToken,
maxResponseTokens: maxTokenRes,
maxRounds: addOneIfOdd(rounds),
});
let firstChunk = true;
response = await sendMessageFromOpenAi(messagesHistory, {
maxToken,
maxTokenRes,
apiKey: modelKey,
model,
temperature,
proxyUrl: proxyResUrl,
onProgress: (chat) => {
res.write(firstChunk ? JSON.stringify(chat) : `\n${JSON.stringify(chat)}`);
lastChat = chat;
firstChunk = false;
},
});
isSuccess = true;
}
/* 百度文心 */
if (Number(keyType) === 2) {
let firstChunk = true;
const { context: messagesHistory } = await this.nineStore.buildMessageFromParentMessageId(usingNetwork ? netWorkPrompt : prompt, {
parentMessageId,
maxRounds: addOneIfOdd(rounds),
});
response = await sendMessageFromBaidu(usingNetwork ? netWorkPrompt : messagesHistory, {
temperature,
accessToken,
model,
onProgress: (data) => {
res.write(firstChunk ? JSON.stringify(data) : `\n${JSON.stringify(data)}`);
firstChunk = false;
lastChat = data;
},
});
isSuccess = true;
}
/* 清华智谱 */
if (Number(keyType) === 3) {
let firstChunk = true;
const { context: messagesHistory } = await this.nineStore.buildMessageFromParentMessageId(usingNetwork ? netWorkPrompt : prompt, {
parentMessageId,
maxRounds: addOneIfOdd(rounds),
});
response = await sendMessageFromZhipu(usingNetwork ? netWorkPrompt : messagesHistory, {
temperature,
key,
model,
onProgress: (data) => {
res.write(firstChunk ? JSON.stringify(data) : `\n${JSON.stringify(data)}`);
firstChunk = false;
lastChat = data;
},
});
isSuccess = true;
}
/* 分别将本次用户输入的 和 机器人返回的分两次存入到 store */
const userMessageData: MessageInfo = {
id: this.nineStore.getUuid(),
text: prompt,
role: 'user',
name: undefined,
usage: null,
parentMessageId: parentMessageId,
conversationId: response?.conversationId,
};
othersInfo = { model, parentMessageId };
await this.nineStore.setData(userMessageData);
const assistantMessageData: MessageInfo = {
id: response.id,
text: response.text,
role: 'assistant',
name: undefined,
usage: response.usage,
parentMessageId: userMessageData.id,
conversationId: response?.conversationId,
};
await this.nineStore.setData(assistantMessageData);
othersInfo = { model, parentMessageId: userMessageData.id };
/* 回答完毕 */
} else {
const { key, maxToken, maxTokenRes, proxyResUrl } = await this.formatModelToken(currentRequestModelKey);
const { parentMessageId, completionParams, systemMessage } = mergedOptions;
const { model, temperature } = completionParams;
const { context: messagesHistory } = await this.nineStore.buildMessageFromParentMessageId(usingNetwork ? netWorkPrompt : prompt, {
parentMessageId,
systemMessage,
maxRounds: addOneIfOdd(rounds),
});
response = await sendMessageFromOpenAi(messagesHistory, {
apiKey: modelKey,
model,
temperature,
proxyUrl: proxyResUrl,
onProgress: null,
});
}
/* 统一最终输出格式 */
const formatResponse = await unifiedFormattingResponse(keyType, response, othersInfo);
const { prompt_tokens = 0, completion_tokens = 0, total_tokens = 0 } = formatResponse.usage;
/* 区分扣除普通还是高级余额 model3: 普通余额 model4 高级余额 */
let charge = deduct;
if (isTokenBased === true) {
charge = deduct * total_tokens;
}
await this.userBalanceService.deductFromBalance(req.user.id, `model${deductType === 1 ? 3 : 4}`, charge, total_tokens);
/* 记录key的使用次数 和使用token */
await this.modelsService.saveUseLog(keyId, total_tokens);
const curIp = getClientIp(req);
/* 用户询问 */
await this.chatLogService.saveChatLog({
appId,
curIp,
userId: req.user.id,
type: DeductionKey.CHAT_TYPE,
prompt,
answer: '',
promptTokens: prompt_tokens,
completionTokens: 0,
totalTokens: total_tokens,
model: formatResponse.model,
role: 'user',
groupId,
requestOptions: JSON.stringify({
options: null,
prompt,
}),
});
// gpt回答
await this.chatLogService.saveChatLog({
appId,
curIp,
userId: req.user.id,
type: DeductionKey.CHAT_TYPE,
prompt: prompt,
answer: formatResponse?.text,
promptTokens: prompt_tokens,
completionTokens: completion_tokens,
totalTokens: total_tokens,
model: model,
role: 'assistant',
groupId,
requestOptions: JSON.stringify({
options: {
model: model,
temperature,
},
prompt,
}),
conversationOptions: JSON.stringify({
conversationId: response.conversationId,
model: model,
parentMessageId: response.id,
temperature,
}),
});
Logger.debug(
`本次调用: ${req.user.id} model: ${model} key -> ${key}, 模型名称: ${modelName}, 最大回复token: ${maxResponseTokens}`,
'ChatgptService',
);
const userBalance = await this.userBalanceService.queryUserBalance(req.user.id);
response.userBanance = { ...userBalance };
response.result && (response.result = '');
response.is_end = true; //本次才是表示真的结束
if (res) {
return res.write(`\n${JSON.stringify(response)}`);
} else {
return response.text;
}
} catch (error) {
console.log('chat-error <----------------------------------------->', modelKey, error);
const code = error?.statusCode || 400;
const status = error?.response?.status || error?.statusCode || 400;
console.log(
'chat-error-detail <----------------------------------------->',
'code: ',
code,
'message',
error?.message,
'statusText:',
error?.response?.statusText,
'status',
error?.response?.status,
);
if (error.status && error.status === 402) {
const errMsg = { message: `Catch Error ${error.message}`, code: 402 };
if (res) {
return res.write(JSON.stringify(errMsg));
} else {
throw new HttpException(error.message, HttpStatus.PAYMENT_REQUIRED);
}
}
if (!status) {
if (res) {
return res.write(JSON.stringify({ message: error.message, code: 500 }));
} else {
throw new HttpException(error.message, HttpStatus.BAD_REQUEST);
}
}
let message = OpenAiErrorCodeMessage[status] ? OpenAiErrorCodeMessage[status] : '服务异常、请重新试试吧!!!';
if (error?.message.includes('The OpenAI account associated with this API key has been deactivated.') && Number(keyType) === 1) {
await this.modelsService.lockKey(keyId, '当前模型key已被封禁、已冻结当前调用Key、尝试重新对话试试吧', -1);
message = '当前模型key已被封禁';
}
if (error?.statusCode === 429 && error.message.includes('billing') && Number(keyType) === 1) {
await this.modelsService.lockKey(keyId, '当前模型key余额已耗尽、已冻结当前调用Key、尝试重新对话试试吧', -3);
message = '当前模型key余额已耗尽';
}
if (error?.statusCode === 429 && error?.statusText === 'Too Many Requests') {
message = '当前模型调用过于频繁、请重新试试吧!';
}
/* 提供了错误的秘钥 */
if (error?.statusCode === 401 && error.message.includes('Incorrect API key provided') && Number(keyType) === 1) {
await this.modelsService.lockKey(keyId, '提供了错误的模型秘钥', -2);
message = '提供了错误的模型秘钥、已冻结当前调用Key、请重新尝试对话';
}
/* 模型有问题 */
if (error?.statusCode === 404 && error.message.includes('This is not a chat model and thus not supported') && Number(keyType) === 1) {
await this.modelsService.lockKey(keyId, '当前模型不是聊天模型', -4);
message = '当前模型不是聊天模型、已冻结当前调用Key、请重新尝试对话';
}
if (code === 400) {
console.log('400 error', error, error.message);
}
/* 防止因为key的原因直接导致客户端以为token过期退出 401只给用于鉴权token中 */
const errMsg = { message: message || 'Please check the back-end console', code: code === 401 ? 400 : code || 500 };
if (res) {
return res.write(JSON.stringify(errMsg));
} else {
throw new HttpException(errMsg.message, HttpStatus.BAD_REQUEST);
}
} finally {
res && res.end();
}
}
async draw(body: ChatDrawDto, req: Request) {
/* 敏感词检测 */
await this.badwordsService.checkBadWords(body.prompt, req.user.id);
await this.userService.checkUserStatus(req.user);
// TODO 目前仅支持一张才这样计算
const money = body?.quality === 'hd' ? 4 : 2;
await this.userBalanceService.validateBalance(req, 'mjDraw', money);
let images = [];
/* 从3的卡池随机拿一个key */
const detailKeyInfo = await this.modelsService.getRandomDrawKey();
const keyId = detailKeyInfo?.id;
const { key, proxyResUrl } = await this.formatModelToken(detailKeyInfo);
Logger.log(`draw paompt info <==**==> ${body.prompt}, key ===> ${key}`, 'DrawService');
try {
const api = `${proxyResUrl}/v1/images/generations`;
const params = { ...body, model: 'dall-e-3' };
console.log('dall-e draw params: ', params);
const res = await axios.post(api, { ...params, response_format: 'b64_json' }, { headers: { Authorization: `Bearer ${key}` } });
images = res.data.data;
const task = [];
for (const item of images) {
const filename = uuid.v4().slice(0, 10) + '.png';
const buffer = Buffer.from(item.b64_json, 'base64');
task.push(this.uploadService.uploadFile({ filename, buffer }));
}
const urls = await Promise.all(task);
/* 绘制openai的dall-e2绘画也扣除的是绘画积分次数 */
await this.userBalanceService.deductFromBalance(req.user.id, 'mjDraw', params?.quality === 'standard' ? 2 : 4, money);
const curIp = getClientIp(req);
const taskLog = [];
const cosType = await this.uploadService.getUploadType();
const [width, height] = body.size.split('x');
urls.forEach((url) => {
taskLog.push(
this.chatLogService.saveChatLog({
curIp,
userId: req.user.id,
type: DeductionKey.PAINT_TYPE,
prompt: body.prompt,
answer: url,
fileInfo: JSON.stringify({
cosType,
width,
height,
cosUrl: url,
}),
promptTokens: 0,
completionTokens: 0,
totalTokens: 0,
model: 'dall-e-3',
}),
);
});
await Promise.all(taskLog);
return urls;
} catch (error) {
const status = error?.response?.status || 500;
console.log('openai-draw error: ', JSON.stringify(error), key, status);
const message = error?.response?.data?.error?.message;
if (status === 429) {
throw new HttpException('当前请求已过载、请稍等会儿再试试吧!', HttpStatus.BAD_REQUEST);
}
if (status === 400 && message.includes('This request has been blocked by our content filters')) {
throw new HttpException('您的请求已被系统拒绝。您的提示可能存在一些非法的文本。', HttpStatus.BAD_REQUEST);
}
if (status === 400 && message.includes('Billing hard limit has been reached')) {
await this.modelsService.lockKey(keyId, '当前模型key已被封禁、已冻结当前调用Key、尝试重新对话试试吧', -1);
throw new HttpException('当前Key余额已不足、请重新再试一次吧', HttpStatus.BAD_REQUEST);
}
if (status === 500) {
throw new HttpException('绘制图片失败,请检查你的提示词是否有非法描述!', HttpStatus.BAD_REQUEST);
}
if (status === 401) {
throw new HttpException('绘制图片失败,此次绘画被拒绝了!', HttpStatus.BAD_REQUEST);
}
throw new HttpException('绘制图片失败,请稍后试试吧!', HttpStatus.BAD_REQUEST);
}
}
/* 当前所有key的列表 */
async getAllKeyList() {
const list = await this.gptKeysEntity.find({
where: { status: 1 },
select: ['id', 'key', 'weight', 'model', 'maxModelTokens', 'maxResponseTokens', 'openaiProxyUrl', 'openaiTimeoutMs'],
});
const list3 = list.filter((t) => t.model.includes('gpt-3'));
const list4 = list.filter((t) => t.model.includes('gpt-4'));
this.keyPool = {
list3,
list4,
};
}
/* 拿到代理地址 */
async getModelProxyUrl(modelKey) {
const openaiBaseUrl = await this.globalConfigService.getConfigs(['openaiBaseUrl']);
return modelKey?.proxyUrl || openaiBaseUrl || 'https://api.openai.com';
}
/* TODO 区分整理不同默认的token数量管理 */
async formatModelToken(detailKeyInfo) {
/* global config */
const {
openaiModel3MaxTokens = 0,
openaiModel3MaxTokensRes = 0,
openaiModel3MaxTokens16k = 0,
openaiModel3MaxTokens16kRes = 0,
openaiModel4MaxTokens = 0,
openaiModel4MaxTokensRes = 0,
openaiModel4MaxTokens32k = 0,
openaiModel4MaxTokens32kRes = 0,
openaiBaseUrl = '',
} = await this.globalConfigService.getConfigs([
'openaiModel3MaxTokens',
'openaiModel3MaxTokensRes',
'openaiModel3MaxTokens16k',
'openaiModel3MaxTokens16kRes',
'openaiModel4MaxTokens',
'openaiModel4MaxTokensRes',
'openaiModel4MaxTokens32k',
'openaiModel4MaxTokens32kRes',
'openaiBaseUrl',
]);
let maxToken = null;
let maxTokenRes = null;
let proxyResUrl = null;
let { model, maxModelTokens = 0, maxResponseTokens = 0, proxyUrl = '', key } = detailKeyInfo;
if (model.toLowerCase().includes('gpt-4')) {
maxModelTokens >= 8192 && (maxModelTokens = 8192);
maxTokenRes >= 4096 && (maxModelTokens = 4096);
maxToken = maxModelTokens || openaiModel4MaxTokens || 8192;
maxTokenRes = maxResponseTokens || openaiModel4MaxTokensRes || 4096;
if (model.toLowerCase().includes('32k')) {
maxModelTokens >= 32768 && (maxModelTokens = 32768);
maxTokenRes >= 16384 && (maxModelTokens = 16384);
maxToken = maxModelTokens || openaiModel4MaxTokens32k || 32768;
maxTokenRes = maxResponseTokens || openaiModel4MaxTokens32kRes || 16384;
}
if (model.toLowerCase().includes('1106')) {
maxModelTokens >= 16380 && (maxModelTokens = 16380);
maxTokenRes >= 4096 && (maxModelTokens = 4096);
maxToken = maxModelTokens || 16380;
maxTokenRes = maxResponseTokens || 4096;
}
}
if (model.toLowerCase().includes('gpt-3')) {
maxModelTokens >= 4096 && (maxModelTokens = 4096);
maxTokenRes >= 2000 && (maxModelTokens = 2000);
maxToken = maxModelTokens || openaiModel3MaxTokens || 4096;
maxTokenRes = maxResponseTokens || openaiModel3MaxTokensRes || 2000;
if (model.toLowerCase().includes('16k')) {
maxModelTokens >= 16384 && (maxModelTokens = 16384);
maxTokenRes >= 8192 && (maxModelTokens = 8192);
maxToken = maxModelTokens || openaiModel3MaxTokens16k || 16384;
maxTokenRes = maxResponseTokens || openaiModel3MaxTokens16kRes || 8192;
}
if (model.toLowerCase().includes('1106')) {
maxModelTokens >= 16384 && (maxModelTokens = 16384);
maxTokenRes >= 4096 && (maxModelTokens = 4096);
maxToken = maxModelTokens || 16384;
maxTokenRes = maxResponseTokens || 4096;
}
}
proxyResUrl = proxyUrl || openaiBaseUrl || 'https://api.openai.com';
if (maxTokenRes >= maxToken) {
maxTokenRes = Math.floor(maxToken / 2);
}
return {
key,
maxToken,
maxTokenRes,
proxyResUrl,
};
}
async setChatBoxType(req: Request, body) {
try {
const { name, icon, order, id, status } = body;
if (id) {
return await this.chatBoxTypeEntity.update({ id }, { name, icon, order, status });
} else {
return await this.chatBoxTypeEntity.save({ name, icon, order, status });
}
} catch (error) {
console.log('error: ', error);
}
}
async delChatBoxType(req: Request, body) {
const { id } = body;
if (!id) {
throw new HttpException('非法操作!', HttpStatus.BAD_REQUEST);
}
const count = await this.chatBoxEntity.count({ where: { typeId: id } });
if (count) {
throw new HttpException('当前分类下有未处理数据不可移除!', HttpStatus.BAD_REQUEST);
}
return await this.chatBoxTypeEntity.delete({ id });
}
async queryChatBoxType() {
return await this.chatBoxTypeEntity.find({
order: { order: 'DESC' },
});
}
async setChatBox(req: Request, body) {
const { title, prompt, appId, order, status, typeId, id, url } = body;
if (!typeId) {
throw new HttpException('缺失必要参数!', HttpStatus.BAD_REQUEST);
}
try {
const params: any = { title, order, status, typeId, url };
params.appId = appId || 0;
params.prompt = prompt || '';
if (id) {
return await this.chatBoxEntity.update({ id }, params);
} else {
return await this.chatBoxEntity.save(params);
}
} catch (error) {
console.log('error: ', error);
}
}
async delChatBox(req: Request, body) {
const { id } = body;
if (!id) {
throw new HttpException('非法操作!', HttpStatus.BAD_REQUEST);
}
return await this.chatBoxEntity.delete({ id });
}
async queryChatBox() {
const data = await this.chatBoxEntity.find({
order: { order: 'DESC' },
});
const typeIds = [...new Set(data.map((t) => t.typeId))];
const appIds = [...new Set(data.map((t) => t.appId))];
const typeRes = await this.chatBoxTypeEntity.find({ where: { id: In(typeIds) } });
const appRes = await this.appEntity.find({ where: { id: In(appIds) } });
return data.map((item: any) => {
const { typeId, appId } = item;
item.typeInfo = typeRes.find((t) => t.id === typeId);
item.appInfo = appRes.find((t) => t.id === appId);
return item;
});
}
async queryChatBoxFrontend() {
const typeRes = await this.chatBoxTypeEntity.find({ order: { order: 'DESC' }, where: { status: true } });
const boxinfos = await this.chatBoxEntity.find({ where: { status: true } });
const appIds = [...new Set(boxinfos.map((t) => t.appId))];
const appInfos = await this.appEntity.find({ where: { id: In(appIds) } });
boxinfos.forEach((item: any) => {
const app = appInfos.find((k) => k.id === item.appId);
item.coverImg = app?.coverImg;
return item;
});
return typeRes.map((t: any) => {
t.childList = boxinfos.filter((box) => box.typeId === t.id && box.status);
return t;
});
}
async setChatPreType(req: Request, body) {
try {
const { name, icon, order, id, status } = body;
if (id) {
return await this.chatPreTypeEntity.update({ id }, { name, icon, order, status });
} else {
return await this.chatPreTypeEntity.save({ name, icon, order, status });
}
} catch (error) {
console.log('error: ', error);
}
}
async delChatPreType(req: Request, body) {
const { id } = body;
if (!id) {
throw new HttpException('非法操作!', HttpStatus.BAD_REQUEST);
}
const count = await this.chatBoxEntity.count({ where: { typeId: id } });
if (count) {
throw new HttpException('当前分类下有未处理数据不可移除!', HttpStatus.BAD_REQUEST);
}
return await this.chatPreTypeEntity.delete({ id });
}
async queryChatPreType() {
return await this.chatPreTypeEntity.find({
order: { order: 'DESC' },
});
}
async setChatPre(req: Request, body) {
const { title, prompt, appId, order, status, typeId, id, url } = body;
if (!typeId) {
throw new HttpException('缺失必要参数!', HttpStatus.BAD_REQUEST);
}
try {
const params: any = { title, prompt, order, status, typeId, url };
if (id) {
return await this.chatPreEntity.update({ id }, params);
} else {
return await this.chatPreEntity.save(params);
}
} catch (error) {
console.log('error: ', error);
}
}
async delChatPre(req: Request, body) {
const { id } = body;
if (!id) {
throw new HttpException('非法操作!', HttpStatus.BAD_REQUEST);
}
return await this.chatPreEntity.delete({ id });
}
async queryChatPre() {
const data = await this.chatPreEntity.find({
order: { order: 'DESC' },
});
const typeIds = [...new Set(data.map((t) => t.typeId))];
const typeRes = await this.chatPreTypeEntity.find({ where: { id: In(typeIds) } });
return data.map((item: any) => {
const { typeId, appId } = item;
item.typeInfo = typeRes.find((t) => t.id === typeId);
return item;
});
}
async queryChatPreList() {
const typeRes = await this.chatPreTypeEntity.find({ order: { order: 'DESC' }, where: { status: true } });
const chatPreData = await this.chatPreEntity.find({ where: { status: true } });
return typeRes.map((t: any) => {
t.childList = chatPreData.filter((box) => box.typeId === t.id && box.status);
return t;
});
}
/* 通过模型拿到当前模型支持的最大上下文 */
async getMaxTokenFromModelWithOpenAi(model: string, maxModelToken, maxResToken) {
let maxToken = 4096;
let maxRes = 2048;
if (model.toLowerCase().includes('gpt-4')) {
/* 普通的4 是8196最大token 32k为32768 */
maxToken = maxModelToken >= 8196 ? 8196 : maxModelToken;
maxRes = maxResToken >= 4096 ? 4096 : maxResToken;
/* 32k模型最大回复 */
if (model.toLowerCase().includes('32k')) {
maxToken = maxModelToken >= 32768 ? 32768 : maxModelToken;
maxRes = maxResToken >= 16000 ? 16000 : maxResToken;
}
/* gpt4 1106 或者 preview 最大 128k 回复最大 4096 */
if (model.toLowerCase().includes('gpt-4-1106') || model.toLowerCase().includes('gpt-4-vision-preview')) {
maxToken = maxModelToken >= 128000 ? 128000 : maxModelToken;
maxRes = maxResToken >= 4096 ? 4096 : maxResToken;
}
}
/* 3的模型 */
if (model.toLowerCase().includes('gpt-3')) {
/* 普通的模型 最大上下文4096 */
maxToken = maxModelToken >= 4096 ? 4096 : maxModelToken;
maxRes = maxResToken >= 2048 ? 2048 : maxResToken;
if (model.toLowerCase().includes('16k')) {
maxToken = maxModelToken >= 16384 ? 16384 : maxModelToken;
maxRes = maxResToken >= 8000 ? 8000 : maxResToken;
}
if (model.toLowerCase().includes('1106')) {
maxToken = maxModelToken >= 16384 ? 16384 : maxModelToken;
maxRes = maxResToken >= 8000 ? 8000 : maxResToken;
}
}
return {
maxToken,
maxRes,
};
}
}

View File

@@ -0,0 +1,16 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class ChatDrawDto {
@ApiProperty({ example: 'Draw a cute little dog', description: '绘画描述信息' })
prompt: string;
@ApiProperty({ example: 1, description: '绘画张数', required: true })
n: number;
@ApiProperty({ example: '1024x1024', description: '图片尺寸', required: true })
size: string;
@ApiProperty({ example: 'standard', description: '图片质量', required: true })
quality: string;
}

View File

@@ -0,0 +1,33 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsOptional } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class Options {
@IsString()
parentMessageId: string;
model?: string;
temperature?: number;
top_p?: number;
groupId?: number;
}
export class ChatProcessDto {
@ApiProperty({ example: 'hello, Who are you', description: '对话信息' })
@IsNotEmpty({ message: '提问信息不能为空!' })
prompt: string;
@ApiProperty({ example: '{ parentMessageId: 0 }', description: '上次对话信息', required: false })
@Type(() => Options)
options: Options;
@ApiProperty({
example: "You are ChatGPT, a large language model trained by OpenAI. Follow the user's instructions carefully. Respond using markdown.",
description: '系统预设信息',
})
@IsOptional()
systemMessage?: string;
@ApiProperty({ example: 1, description: '应用id', required: false })
@IsOptional()
appId: number;
}

View File

@@ -0,0 +1,45 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'gpt_keys' })
export class GptKeysEntity extends BaseEntity {
@Column({ unique: true, comment: 'gpt key', length: 255 })
key: string;
@Column({ comment: '使用的状态: 0:禁用 1启用', default: 0 })
status: number;
@Column({ comment: '绑定的模型是?', default: 'gpt-3.5-turbo' })
model: string;
@Column({ comment: 'key的余额', type: 'decimal', precision: 10, scale: 2, default: 0 })
balance: string;
@Column({ comment: 'key的余额类型', default: '', nullable: true })
type: string;
@Column({ comment: 'key的状态: 1:有效 2:余额耗尽 -1:被封号', default: 1 })
keyStatus: number;
@Column({ comment: 'key的到期时间', nullable: true })
expireTime: Date;
@Column({ comment: 'key权重', default: 1 })
weight: number;
@Column({ comment: 'key的使用次数', default: 0 })
useCount: number;
@Column({ comment: '模型支持的最大Token', nullable: true })
maxModelTokens: number;
@Column({ comment: '模型设置的最大回复Token', nullable: true })
maxResponseTokens: number;
@Column({ comment: '当前模型的代理地址', nullable: true })
openaiProxyUrl: string;
@Column({ comment: '当前模型的超时时间单位ms', nullable: true })
openaiTimeoutMs: number;
}

View File

@@ -0,0 +1,71 @@
/**
* @desc 处理不同模型返回的最后一次汇总内容 输出为相同格式 方便后面使用
* @param keyType 模型key类型
* @param response 模型返回的整体内容
*/
export function unifiedFormattingResponse(keyType, response, others){
let formatRes = {
keyType, // 模型类型
model: '', // 调用模型名称
parentMessageId: '', // 父级对话id
text: '', //本次回复内容
usage: {
prompt_tokens: 0, //提问token
completion_tokens: 0, // 回答token
total_tokens: 0, // 总消耗token
}
}
/* openai */
if([1].includes(Number(keyType))){
const { model, parentMessageId } = response?.detail
let { usage } = response?.detail
if(!usage){
usage = {
prompt_tokens: 0,
completion_tokens: 0,
total_tokens: 0
}
}
const { prompt_tokens, completion_tokens, total_tokens } = usage
formatRes = {
keyType,
model,
parentMessageId,
text: response.text,
usage: {
prompt_tokens,
completion_tokens,
total_tokens
}
}
}
/* 百度 */
if([2, 3].includes(Number(keyType))) {
const { usage, text } = response
const { prompt_tokens, completion_tokens, total_tokens } = usage
const { model, parentMessageId } = others
formatRes = {
keyType,
model,
parentMessageId,
text,
usage: {
prompt_tokens,
completion_tokens,
total_tokens
}
}
}
return formatRes;
}
/*百度的模型不允许传入偶数的message数组 让round为奇数的时候 加一 */
export function addOneIfOdd(num) {
if (num % 2 !== 0) {
return num + 1;
} else {
return num;
}
}

View File

@@ -0,0 +1,134 @@
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { get_encoding } from '@dqbd/tiktoken'
import { removeSpecialCharacters } from '@/common/utils';
const tokenizer = get_encoding('cl100k_base')
interface SendMessageResult {
id?: string;
text: string;
role?: string;
detail?: any;
}
function getFullUrl(proxyUrl){
const processedUrl = proxyUrl.endsWith('/') ? proxyUrl.slice(0, -1) : proxyUrl;
const baseUrl = processedUrl || 'https://api.openai.com'
return `${baseUrl}/v1/chat/completions`
}
export function sendMessageFromOpenAi(messagesHistory, inputs ){
const { onProgress, maxToken, apiKey, model, temperature = 0.95, proxyUrl } = inputs
console.log('current request options: ',apiKey, model, maxToken, proxyUrl );
const max_tokens = compilerToken(model, maxToken)
const options: AxiosRequestConfig = {
method: 'POST',
url: getFullUrl(proxyUrl),
responseType: 'stream',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${removeSpecialCharacters(apiKey)}`,
},
data: {
max_tokens,
stream: true,
temperature,
model,
messages: messagesHistory
},
};
const prompt = messagesHistory[messagesHistory.length-1]?.content
return new Promise(async (resolve, reject) =>{
try {
const response: any = await axios(options);
const stream = response.data;
let result: any = { text: '' };
stream.on('data', (chunk) => {
const splitArr = chunk.toString().split('\n\n').filter((line) => line.trim() !== '');
for (const line of splitArr) {
const data = line.replace('data:', '');
let ISEND = false;
try {
ISEND = JSON.parse(data).choices[0].finish_reason === 'stop';
} catch (error) {
ISEND = false;
}
/* 如果结束 返回所有 */
if (data === '[DONE]' || ISEND) {
result.text = result.text.trim();
return result;
}
try {
const parsedData = JSON.parse(data);
if (parsedData.id) {
result.id = parsedData.id;
}
if (parsedData.choices?.length) {
const delta = parsedData.choices[0].delta;
result.delta = delta.content;
if (delta?.content) result.text += delta.content;
if (delta.role) {
result.role = delta.role;
}
result.detail = parsedData;
}
onProgress && onProgress({text:result.text})
} catch (error) {
console.log('parse Error', data )
}
}
});
stream.on('end', () => {
// 手动计算token
if(result.detail && result.text){
const promptTokens = getTokenCount(prompt)
const completionTokens = getTokenCount(result.text)
result.detail.usage = {
prompt_tokens: promptTokens,
completion_tokens: completionTokens ,
total_tokens: promptTokens + completionTokens,
estimated: true
}
}
return resolve(result);
});
} catch (error) {
reject(error)
}
})
}
export function getTokenCount(text: string) {
if(!text) return 0;
text = text.replace(/<\|endoftext\|>/g, '')
return tokenizer.encode(text).length
}
function compilerToken(model, maxToken){
let max = 0
/* 3.5 */
if(model.includes(3.5)){
max = maxToken > 4096 ? 4096 : maxToken
}
/* 4.0 */
if(model.includes('gpt-4')){
max = maxToken > 8192 ? 8192 : maxToken
}
/* 4.0 preview */
if(model.includes('preview')){
max = maxToken > 4096 ? 4096 : maxToken
}
/* 4.0 32k */
if(model.includes('32k')){
max = maxToken > 32768 ? 32768 : maxToken
}
return max
}

View File

@@ -0,0 +1,172 @@
import Keyv from 'keyv'
import { v4 as uuidv4 } from "uuid";
import { get_encoding } from '@dqbd/tiktoken'
import { Logger } from '@nestjs/common';
const tokenizer = get_encoding('cl100k_base')
export type Role = 'user' | 'assistant' | 'function'
interface Options {
store: Keyv
namespace: string
expires?: number
}
export interface MessageInfo {
id: string
text: string
role: Role
name?: string
usage: {
prompt_tokens?: number
completion_tokens?: number
total_tokens?: number
}
parentMessageId?: string
conversationId?: string
}
export interface BuildMessageOptions {
systemMessage?: string
parentMessageId: string
maxRounds?: number
maxModelToken?: number
maxResponseTokens?: number
name?: string
}
// export interface BuildMessageRes {
// message: any[]
// numTokens: number
// maxTokens: number
// }
export type BuildMessageRes = any[]
export interface NineStoreInterface {
getData(id: string): Promise<string>;
setData(message: MessageInfo, expires?: number): Promise<void>;
getUuid(): string
buildMessageFromParentMessageId(string, opt?: BuildMessageOptions): Promise<any>;
}
export class NineStore implements NineStoreInterface {
private namespace: string;
private store: Keyv;
private expires: number;
constructor(options: Options) {
const { store, namespace, expires } = this.formatOptions(options)
this.store = store
this.namespace = namespace
this.expires = expires
}
public formatOptions(options: Options){
const { store, expires = 1000 * 60 * 60 * 24 * 3, namespace = 'chat'} = options
return { store, namespace, expires }
}
public generateKey(key){
return this.namespace ? `${this.namespace}-${key}` : key
}
public async getData(id: string ): Promise<any> {
const res = await this.store.get(id)
return res
}
public async setData(message, expires = this.expires): Promise<void> {
await this.store.set(message.id, message, expires)
}
/**
* @desc 通过传入prompt和parentMessageId 递归往上拿到历史记录组装为模型需要的上下文、
* 可以传入maxRounds限制最大轮次的对话 传入maxModelToken, maxResponseTokens 则通过计算上下文占用的token计算出最大容量
*/
public async buildMessageFromParentMessageId(text: string, options: BuildMessageOptions){
let { maxRounds, maxModelToken, maxResponseTokens, systemMessage = '', name } = options
let { parentMessageId } = options
let messages = []
let nextNumTokensEstimate = 0
if (systemMessage) {
messages.push({ role: 'system', content: systemMessage })
}
const systemMessageOffset = messages.length
let round = 0
let nextMessages = text ? messages.concat([{ role: 'user', content: text, name }]) : messages
do {
// let parentId = '1bf30262-8f25-4a03-88ad-9d42d55e6f0b'
/* 没有parentMessageId就没有历史 直接返回 */
if(!parentMessageId){
break;
}
const parentMessage = await this.getData(parentMessageId)
if(!parentMessage){
break;
}
const { text, name, role } = parentMessage
/* 将本轮消息插入到列表中 */
nextMessages = nextMessages.slice(0, systemMessageOffset).concat([
{ role, content: text, name },
...nextMessages.slice(systemMessageOffset)
])
round++
/* 如果超出了限制的最大轮次 就退出 不包含本次发送的本身 */
if(maxRounds && round >= maxRounds){
break;
}
/* 如果传入maxModelToken maxResponseTokens 则判断是否超过边界 */
if(maxModelToken && maxResponseTokens){
const maxNumTokens = maxModelToken - maxResponseTokens // 模型最大token限制减去限制回复剩余空间
/* 当前的对话历史列表合并的总token容量 */
nextNumTokensEstimate = await this._getTokenCount(nextMessages)
/* 200是添加的一个安全区间 防止少量超过 待优化 */
const isValidPrompt = nextNumTokensEstimate + 200 <= maxNumTokens
/* 如果大于这个区间了说明本轮加入之后导致超过限制、则递归删除头部的对话轮次来保证不出边界 */
if(!isValidPrompt){
nextMessages = this._recursivePruning(nextMessages, maxNumTokens, systemMessage)
}
}
parentMessageId = parentMessage.parentMessageId
} while (true);
const maxTokens = Math.max(
1,
Math.min(maxModelToken - nextNumTokensEstimate, maxResponseTokens)
)
// Logger.debug(`本轮调用:模型:${model}`)
console.log('本次携带上下文的长度',nextMessages.length, nextNumTokensEstimate )
return { context: nextMessages, round: nextMessages.length, historyToken:nextNumTokensEstimate }
}
protected _getTokenCount(messages: any[]) {
let text = messages.reduce( (pre: string, cur: any) => {
return pre+=cur.content
}, '')
text = text.replace(/<\|endoftext\|>/g, '')
return tokenizer.encode(text).length
}
/* 递归删除 当token超过模型限制容量 删除到在限制区域内 */
protected _recursivePruning(
messages: MessageInfo[],
maxNumTokens: number,
systemMessage: string
) {
const currentTokens = this._getTokenCount(messages)
if (currentTokens <= maxNumTokens) {
return messages
}
/* 有系统预设则跳过第一条删除 没有则直接删除 */
messages.splice(systemMessage ? 1 : 0, 1)
return this._recursivePruning(messages, maxNumTokens, systemMessage)
}
public getUuid(){
return uuidv4()
}
}

View File

@@ -0,0 +1,86 @@
// const AK = "58Uq1GPUPvGBoIKQ2NNrRk8I"
// const SK = "CB3i28zVY2O5Hyb5OCQomwOWrjKQKwMY"
// const AK = "VSUQWIV0FCyDC6FwfHo04jQ6"
// const SK = "1sv3lkXGOqwbyEUDFVMA5O5Y9L27LNtP"
const AK = "vdzYBsVGfz8eidePaZzT3nlC"
const SK = "ZMyEVTR1VhGlGcsReK9BHZjgpne9ujsC"
const axios = require('axios');
/**
* 使用 AKSK 生成鉴权签名Access Token
* @return string 鉴权签名信息Access Token
*/
function getAccessToken() {
let url = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${AK}&client_secret=${SK}`;
return new Promise((resolve, reject) => {
axios.post(url)
.then(response => {
resolve(response.data.access_token);
})
.catch(error => {
console.log('error: ', error);
reject(error);
});
});
}
async function main() {
const accessToken = await getAccessToken();
const url = `https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/completions?access_token=${accessToken}`;
var options = {
method: 'POST',
url,
responseType: 'stream',
headers: {
'Content-Type': 'application/json'
},
data: {
stream: true,
messages: [
{
role: "user",
content: "请介绍一下你自己"
}
]
}
};
axios(options)
.then(response => {
const stream = response.data;
let resData = ''
stream.on('data', chunk => {
// 处理每个数据块
try {
const lines = chunk
.toString()
.split("\n\n")
.filter((line) => line.trim() !== "");
for (const line of lines) {
const message = line.replace("data: ", "");
const parsed = JSON.parse(message);
console.log('parsed: ', parsed);
}
} catch (error) {
}
});
stream.on('end', () => {
// 处理流的结束
console.log('Stream end');
});
})
.catch(error => {
throw new Error(error);
});
}
main();

View File

@@ -0,0 +1,18 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'white_list' })
export class WhiteListEntity extends BaseEntity {
@Column({ unique: true, comment: '用户ID' })
userId: number;
@Column({ comment: '使用次数限制', default: 0 })
count: number;
@Column({ comment: '当前用户状态', default: 1 })
status: number;
@Column({ comment: '已经使用的次数', default: 0 })
useCount: number;
}

View File

@@ -0,0 +1,106 @@
const AK = "58Uq1GPUPvGBoIKQ2NNrRk8I"
const SK = "CB3i28zVY2O5Hyb5OCQomwOWrjKQKwMY"
const axios = require('axios');
const jwt = require('jsonwebtoken');
function generateToken(apikey, expSeconds) {
const [id, secret] = apikey.split('.');
const payload = {
api_key: id,
exp: Math.round(Date.now()) + expSeconds * 1000,
timestamp: Math.round(Date.now()),
};
return jwt.sign(payload, secret, { algorithm: 'HS256', header: { alg: 'HS256', sign_type: 'SIGN' }});
}
const key = '6f3e78ee46553487a30d1404882e435a.6AWDxxlNDGjHioew'
async function test() {
const token = await generateToken(key, 600000)
console.log('token: ', token);
const url = `https://open.bigmodel.cn/api/paas/v3/model-api/chatglm_pro/sse-invoke`;
var options = {
method: 'POST',
url,
responseType: 'stream',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
data: {
prompt: [
{
role: "user",
content: "请介绍下自己"
}
]
}
};
axios(options)
.then(response => {
const stream = response.data;
let resData;
let cacheResText = ''
stream.on('data', chunk => {
const stramArr = chunk.toString().split("\n").filter((line) => line.trim() !== "")
const parseData = compilerStream(stramArr)
if(!parseData) return
const { event, id, result, meta, is_end } = parseData
result && (cacheResText += result.trim())
if (is_end) {
resData = parseData
resData.text = cacheResText
}
});
stream.on('end', () => {
console.log(resData,'结束了')
});
})
.catch(error => {
throw new Error(error);
});
}
/* 格式化信息并且输出为和百度一样的格式 前端不用变动了 */
function compilerStream(streamArr){
if(streamArr.length === 3){
return {
event: streamArr[0].replace('event:', ''),
id: streamArr[1].replace('id:', ''),
is_end: false,
result: streamArr[2].replace('data:', '').trim()
}
}
if(streamArr.length === 4){
console.log('streamArr: ', streamArr);
return {
event: streamArr[0].replace('event:', ''),
id: streamArr[1].replace('id:', ''),
result: streamArr[2].replace('data:', '').trim(),
is_end: true,
meta: compilerMetaJsonStr(streamArr[3].replace('meta:', ''))
}
}
}
function compilerMetaJsonStr(data){
let jsonStr = {}
try {
/*
{
task_status: 'SUCCESS',
usage: { completion_tokens: 49, prompt_tokens: 719, total_tokens: 768 },
task_id: '8008779509197849552',
request_id: '8008779509197849552'
}
*/
jsonStr = JSON.parse(data)
} catch (error) {
console.error('序列化数据出错', data)
}
return jsonStr;
}
test();

View File

@@ -0,0 +1,112 @@
import { resolve } from "path";
const axios = require('axios')
const jwt = require('jsonwebtoken')
/* 生成token */
export function generateToken(apikey, expSeconds = 1000 * 60 * 60 * 24 * 360) {
const [id, secret] = apikey.split('.');
const payload = {
api_key: id,
exp: Math.round(Date.now()) + expSeconds * 1000,
timestamp: Math.round(Date.now()),
};
// ts-ignore
return jwt.sign(payload, secret, { algorithm: 'HS256', header: { alg: 'HS256', sign_type: 'SIGN' } });
}
/* 解析最后一次结果 */
export function compilerMetaJsonStr(data): any {
let jsonStr = {}
try {
/*
{
task_status: 'SUCCESS',
usage: { completion_tokens: 49, prompt_tokens: 719, total_tokens: 768 },
task_id: '8008779509197849552',
request_id: '8008779509197849552'
}
*/
jsonStr = JSON.parse(data)
} catch (error) {
/* 解析失败暂定一个固定值 待优化 */
jsonStr = {
usage: {
completion_tokens: 49,
prompt_tokens: 333,
total_tokens: 399
},
}
console.error('json parse error from zhipu!', data)
}
return jsonStr;
}
/* 格式化信息并且输出为和百度一样的格式 前端不用变动了 */
export function compilerStream(streamArr) {
if (streamArr.length === 3) {
return {
event: streamArr[0].replace('event:', ''),
id: streamArr[1].replace('id:', ''),
is_end: false,
result: streamArr[2].replace('data:', '').trim()
}
}
if (streamArr.length === 4) {
return {
event: streamArr[0].replace('event:', ''),
id: streamArr[1].replace('id:', ''),
result: streamArr[2].replace('data:', '').trim(),
is_end: true,
usage: compilerMetaJsonStr(streamArr[3].replace('meta:', ''))?.usage
}
}
}
export async function sendMessageFromZhipu(messagesHistory, { onProgress, key, model, temperature = 0.95 }) {
const token = await generateToken(key)
return new Promise((resolve, reject) => {
const url = `https://open.bigmodel.cn/api/paas/v3/model-api/${model}/sse-invoke`;
const options = {
method: 'POST',
url,
responseType: 'stream',
headers: {
'Content-Type': 'application/json',
'Authorization': token
},
data: {
prompt: messagesHistory,
temperature
}
};
axios(options)
.then(response => {
const stream = response.data;
let resData;
let cacheResText = ''
stream.on('data', chunk => {
const stramArr = chunk.toString().split("\n").filter((line) => line.trim() !== "")
const parseData = compilerStream(stramArr)
if (!parseData) return
const { id, result, is_end } = parseData
result && (cacheResText += result.trim())
if (is_end) {
parseData.is_end = false //为了在后续的消费之后添加上余额 本次并不是真正的结束
resData = parseData
resData.text = cacheResText
}
onProgress(parseData);
});
stream.on('end', () => {
resolve(resData);
cacheResText = ''
});
})
.catch(error => {
console.log('error: ', error);
});
})
}

View File

@@ -0,0 +1,97 @@
import { CramiService } from './crami.service';
import { Body, Controller, Get, Post, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { CreatePackageDto } from './dto/createPackage.dto';
import { UpdatePackageDto } from './dto/updatePackage.dto';
import { CreatCramiDto } from './dto/createCrami.dto';
import { SuperAuthGuard } from '@/common/auth/superAuth.guard';
import { Request } from 'express';
import { JwtAuthGuard } from '@/common/auth/jwtAuth.guard';
import { UseCramiDto } from './dto/useCrami.dto';
import { QuerAllPackageDto } from './dto/queryAllPackage.dto';
import { DeletePackageDto } from './dto/deletePackage.dto';
import { QuerAllCramiDto } from './dto/queryAllCrami.dto';
import { AdminAuthGuard } from '@/common/auth/adminAuth.guard';
import { BatchDelCramiDto } from './dto/batchDelCrami.dto';
@ApiTags('Crami')
@Controller('crami')
export class CramiController {
constructor(private readonly cramiService: CramiService) {}
@Get('queryOnePackage')
@ApiOperation({ summary: '查询单个套餐' })
async queryOnePackage(@Query('id') id: number) {
return this.cramiService.queryOnePackage(id);
}
@Get('queryAllPackage')
@ApiOperation({ summary: '查询所有套餐' })
async queryAllPackage(@Query() query: QuerAllPackageDto) {
return this.cramiService.queryAllPackage(query);
}
@Post('createPackage')
@ApiOperation({ summary: '创建套餐' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async createPackage(@Body() body: CreatePackageDto) {
return this.cramiService.createPackage(body);
}
@Post('updatePackage')
@ApiOperation({ summary: '更新套餐' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async updatePackage(@Body() body: UpdatePackageDto) {
return this.cramiService.updatePackage(body);
}
@Post('delPackage')
@ApiOperation({ summary: '删除套餐' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async delPackage(@Body() body: DeletePackageDto) {
return this.cramiService.delPackage(body);
}
@Post('createCrami')
@ApiOperation({ summary: '生成卡密' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async createCrami(@Body() body: CreatCramiDto) {
return this.cramiService.createCrami(body);
}
@Post('useCrami')
@ApiOperation({ summary: '使用卡密' })
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
async useCrami(@Req() req: Request, @Body() body: UseCramiDto) {
return this.cramiService.useCrami(req, body);
}
@Get('queryAllCrami')
@ApiOperation({ summary: '查询所有卡密' })
@UseGuards(AdminAuthGuard)
@ApiBearerAuth()
async queryAllCrami(@Query() params: QuerAllCramiDto, @Req() req: Request) {
return this.cramiService.queryAllCrami(params, req);
}
@Post('delCrami')
@ApiOperation({ summary: '删除卡密' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async delCrami(@Body('id') id: number) {
return this.cramiService.delCrami(id);
}
@Post('batchDelCrami')
@ApiOperation({ summary: '批量删除卡密' })
@UseGuards(SuperAuthGuard)
@ApiBearerAuth()
async batchDelCrami(@Body() body: BatchDelCramiDto) {
return this.cramiService.batchDelCrami(body);
}
}

View File

@@ -0,0 +1,33 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'crami' })
export class CramiEntity extends BaseEntity {
@Column({ unique: true, comment: '存储卡密CDK编码', length: 50 })
code: string;
@Column({ comment: '卡密CDK类型1 普通 | 2 单人可使用一次 ', default: 1 })
cramiType: number;
@Column({ comment: '卡密CDK类型 默认套餐类型 | 不填就是自定义类型', nullable: true })
packageId: number;
@Column({ comment: '卡密CDK状态如已使用、未使用等', default: 0 })
status: number;
@Column({ comment: '卡密使用账户用户ID信息', nullable: true })
useId: number;
@Column({ comment: '卡密有效期天数、从生成创建的时候开始计算设为0则不限时间', default: 0 })
days: number;
@Column({ comment: '卡密模型3额度', nullable: true })
model3Count: number;
@Column({ comment: '卡密模型4额度', nullable: true })
model4Count: number;
@Column({ comment: '卡密MJ绘画额度', nullable: true })
drawMjCount: number;
}

View File

@@ -0,0 +1,43 @@
import { Global, Module } from '@nestjs/common';
import { CramiService } from './crami.service';
import { CramiController } from './crami.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { CramiEntity } from './crami.entity';
import { CramiPackageEntity } from './cramiPackage.entity';
import { UserEntity } from '../user/user.entity';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { BalanceEntity } from '../userBalance/balance.entity';
import { AccountLogEntity } from '../userBalance/accountLog.entity';
import { ConfigEntity } from '../globalConfig/config.entity';
import { UserBalanceEntity } from '../userBalance/userBalance.entity';
import { SalesUsersEntity } from '../sales/salesUsers.entity';
import { WhiteListEntity } from '../chatgpt/whiteList.entity';
import { FingerprintLogEntity } from '../userBalance/fingerprint.entity';
import { ChatLogEntity } from '../chatLog/chatLog.entity';
import { ChatGroupEntity } from '../chatGroup/chatGroup.entity';
import { MidjourneyEntity } from '../midjourney/midjourney.entity';
@Global()
@Module({
imports: [
TypeOrmModule.forFeature([
SalesUsersEntity,
CramiEntity,
CramiPackageEntity,
UserEntity,
BalanceEntity,
AccountLogEntity,
ConfigEntity,
UserBalanceEntity,
WhiteListEntity,
FingerprintLogEntity,
ChatLogEntity,
ChatGroupEntity,
MidjourneyEntity
]),
],
providers: [CramiService, UserBalanceService],
controllers: [CramiController],
exports: [CramiService],
})
export class CramiModule {}

View File

@@ -0,0 +1,218 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { CramiEntity } from './crami.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, Like, In, MoreThan, LessThanOrEqual, Not } from 'typeorm';
import { CramiPackageEntity } from './cramiPackage.entity';
import { CreatePackageDto } from './dto/createPackage.dto';
import { CreatCramiDto } from './dto/createCrami.dto';
import { generateCramiCode, isExpired, maskCrami, maskEmail } from '@/common/utils';
import { Request } from 'express';
import { UseCramiDto } from './dto/useCrami.dto';
import { UserEntity } from '../user/user.entity';
import { UserBalanceService } from '../userBalance/userBalance.service';
import { RechargeType } from '@/common/constants/balance.constant';
import { QuerAllPackageDto } from './dto/queryAllPackage.dto';
import { DeletePackageDto } from './dto/deletePackage.dto';
import { QuerAllCramiDto } from './dto/queryAllCrami.dto';
import { BatchDelCramiDto } from './dto/batchDelCrami.dto';
@Injectable()
export class CramiService {
constructor(
@InjectRepository(CramiEntity)
private readonly cramiEntity: Repository<CramiEntity>,
@InjectRepository(CramiPackageEntity)
private readonly cramiPackageEntity: Repository<CramiPackageEntity>,
@InjectRepository(UserEntity)
private readonly userEntity: Repository<UserEntity>,
private readonly userBalanceService: UserBalanceService,
) {}
/* 查询单个套餐 */
async queryOnePackage(id) {
return await this.cramiPackageEntity.findOne({ where: { id } });
}
/* 查询所有套餐 */
async queryAllPackage(query: QuerAllPackageDto) {
try {
const { page = 1, size = 10, name, status, type } = query;
const where = {};
name && Object.assign(where, { name: Like(`%${name}%`) });
status && Object.assign(where, { status });
if (type) {
if (type > 0) {
Object.assign(where, { days: MoreThan(0) });
} else {
Object.assign(where, { days: LessThanOrEqual(0) });
}
}
const [rows, count] = await this.cramiPackageEntity.findAndCount({
skip: (page - 1) * size,
take: size,
where,
order: { order: 'DESC' },
});
return { rows, count };
} catch (error) {
console.log('error: ', error);
}
}
/* 创建套餐 */
async createPackage(body: CreatePackageDto) {
const { name, weight } = body;
const p = await this.cramiPackageEntity.findOne({ where: [{ name }, { weight }] });
if (p) {
throw new HttpException('套餐名称或套餐等级重复、请检查!', HttpStatus.BAD_REQUEST);
}
try {
return await this.cramiPackageEntity.save(body);
} catch (error) {
console.log('error: ', error);
throw new HttpException(error, HttpStatus.BAD_REQUEST);
}
}
/* 更新套餐 E */
async updatePackage(body) {
const { id, name, weight } = body;
const op = await this.cramiPackageEntity.findOne({ where: { id } });
if (!op) {
throw new HttpException('当前套餐不存在、请检查你的输入参数!', HttpStatus.BAD_REQUEST);
}
const count = await this.cramiPackageEntity.count({
where: [
{ name, id: Not(id) },
{ weight, id: Not(id) },
],
});
if (count) {
throw new HttpException('套餐名称或套餐等级重复、请检查!', HttpStatus.BAD_REQUEST);
}
const res = await this.cramiPackageEntity.update({ id }, body);
if (res.affected > 0) {
return '更新套餐成功!';
} else {
throw new HttpException('更新套餐失败、请重试!', HttpStatus.BAD_REQUEST);
}
}
/* 删除套餐 */
async delPackage(body: DeletePackageDto) {
const { id } = body;
const count = await this.cramiEntity.count({ where: { packageId: id } });
if (count) {
throw new HttpException('当前套餐下存在卡密、请先删除卡密后才可删除套餐!', HttpStatus.BAD_REQUEST);
}
return await this.cramiPackageEntity.delete({ id });
}
/* 生成卡密 */
async createCrami(body: CreatCramiDto) {
const { packageId, count = 1 } = body;
/* 创建有套餐的卡密 */
if (packageId) {
const pkg = await this.cramiPackageEntity.findOne({ where: { id: packageId } });
if (!pkg) {
throw new HttpException('当前套餐不存在、请确认您选择的套餐是否存在!', HttpStatus.BAD_REQUEST);
}
const { days = -1, model3Count = 0, model4Count = 0, drawMjCount = 0 } = pkg;
const baseCrami = { packageId, days, model3Count, model4Count, drawMjCount };
return await this.generateCrami(baseCrami, count);
}
/* 创建自定义的卡密 */
if (!packageId) {
const { model3Count = 0, model4Count = 0, drawMjCount = 0 } = body;
if ([model3Count, model4Count, drawMjCount].every((v) => !v)) {
throw new HttpException('自定义卡密必须至少一项余额不为0', HttpStatus.BAD_REQUEST);
}
const baseCrami = { days: -1, model3Count, model4Count, drawMjCount };
return await this.generateCrami(baseCrami, count);
}
}
/* 创建卡密 */
async generateCrami(cramiInfo, count: number) {
const cramiList = [];
for (let i = 0; i < count; i++) {
const code = generateCramiCode();
const crami = this.cramiEntity.create({ ...cramiInfo, code });
cramiList.push(crami);
}
return await this.cramiEntity.save(cramiList);
}
/* 使用卡密 */
async useCrami(req: Request, body: UseCramiDto) {
const { id } = req.user;
const crami = await this.cramiEntity.findOne({ where: { code: body.code } });
if (!crami) {
throw new HttpException('当前卡密不存在、请确认您输入的卡密是否正确!', HttpStatus.BAD_REQUEST);
}
const { status, days = -1, model3Count = 0, model4Count = 0, drawMjCount = 0, packageId } = crami;
if (status === 1) {
throw new HttpException('当前卡密已被使用、请确认您输入的卡密是否正确!', HttpStatus.BAD_REQUEST);
}
const balanceInfo = { model3Count, model4Count, drawMjCount, packageId };
await this.userBalanceService.addBalanceToUser(id, { ...balanceInfo }, days);
await this.userBalanceService.saveRecordRechargeLog({
userId: id,
rechargeType: RechargeType.PACKAGE_GIFT,
model3Count,
model4Count,
drawMjCount,
days,
});
await this.cramiEntity.update({ code: body.code }, { useId: id, status: 1 });
return '使用卡密成功';
}
/* 查询所有卡密 */
async queryAllCrami(params: QuerAllCramiDto, req: Request) {
const { page = 1, size = 10, status, useId } = params;
const where = {};
status && Object.assign(where, { status });
useId && Object.assign(where, { useId });
const [rows, count] = await this.cramiEntity.findAndCount({
skip: (page - 1) * size,
take: size,
order: { createdAt: 'DESC' },
where,
});
const userIds = rows.map((t) => t.useId);
const packageIds = rows.map((t) => t.packageId);
const userInfos = await this.userEntity.find({ where: { id: In(userIds) } });
const packageInfos = await this.cramiPackageEntity.find({ where: { id: In(packageIds) } });
rows.forEach((t: any) => {
t.username = userInfos.find((u) => u.id === t.useId)?.username;
t.email = userInfos.find((u) => u.id === t.useId)?.email;
t.packageName = packageInfos.find((p) => p.id === t.packageId)?.name;
});
req.user.role !== 'super' && rows.forEach((t: any) => (t.email = maskEmail(t.email)));
req.user.role !== 'super' && rows.forEach((t: any) => (t.code = maskCrami(t.code)));
return { rows, count };
}
/* 删除卡密 */
async delCrami(id) {
const c = await this.cramiEntity.findOne({ where: { id } });
if (!c) {
throw new HttpException('当前卡密不存在、请确认您要删除的卡密是否存在!', HttpStatus.BAD_REQUEST);
}
if (c.status === 1) {
throw new HttpException('当前卡密已被使用、已使用的卡密禁止删除!', HttpStatus.BAD_REQUEST);
}
return await this.cramiEntity.delete({ id });
}
async batchDelCrami(body: BatchDelCramiDto) {
const { ids } = body;
const res = await this.cramiEntity.delete(ids);
if (res.affected > 0) {
return '删除卡密成功!';
} else {
throw new HttpException('删除卡密失败、请重试!', HttpStatus.BAD_REQUEST);
}
}
}

View File

@@ -0,0 +1,39 @@
import { UserStatusEnum } from '../../common/constants/user.constant';
import { Column, Entity } from 'typeorm';
import { BaseEntity } from 'src/common/entity/baseEntity';
@Entity({ name: 'crami_package' })
export class CramiPackageEntity extends BaseEntity {
@Column({ unique: true, comment: '套餐名称' })
name: string;
@Column({ comment: '套餐介绍详细信息' })
des: string;
@Column({ comment: '套餐封面图片', nullable: true })
coverImg: string;
@Column({ comment: '套餐价格¥', type: 'decimal', scale: 2, precision: 10 })
price: number;
@Column({ comment: '套餐排序、数字越大越靠前', default: 100 })
order: number;
@Column({ comment: '套餐是否启用中 0禁用 1启用', default: 1 })
status: number;
@Column({ comment: '套餐权重、数字越大表示套餐等级越高越贵', unique: true })
weight: number;
@Column({ comment: '卡密有效期天数、从使用的时候开始计算,设为-1则不限时间', default: 0 })
days: number;
@Column({ comment: '套餐包含的模型3数量', default: 0, nullable: true })
model3Count: number;
@Column({ comment: '套餐包含的模型4数量', default: 0, nullable: true })
model4Count: number;
@Column({ comment: '套餐包含的MJ绘画数量', default: 0, nullable: true })
drawMjCount: number;
}

View File

@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMinSize, IsArray, IsNumber } from 'class-validator';
export class BatchDelCramiDto {
@ApiProperty({ example: 1, description: '要删除的套餐Ids', required: true })
@IsArray({ message: '参数类型为数组' })
@ArrayMinSize(1, { message: '最短长度为1' })
ids: number[];
}

View File

@@ -0,0 +1,44 @@
import {
IsNotEmpty,
IsString,
IsIn,
IsOptional,
IsNumber,
IsDefined,
ValidatorConstraint,
ValidatorConstraintInterface,
Validate,
Max,
Min,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
export class CreatCramiDto {
@ApiProperty({ example: 1, description: '套餐类型', required: true })
@IsNumber({}, { message: '套餐类型必须是number' })
@IsOptional()
packageId: number;
@ApiProperty({ example: 1, description: '单次生成卡密数量' })
@IsNumber({}, { message: '创建卡密的张数数量' })
@Max(50, { message: '单次创建卡密的张数数量不能超过50张' })
@Min(1, { message: '单次创建卡密的张数数量不能少于1张' })
@IsOptional()
count: number;
@ApiProperty({ example: 0, description: '卡密携带模型3额度' })
@IsNumber({}, { message: '卡密携带的余额必须是number' })
@IsOptional()
model3Count: number;
@ApiProperty({ example: 100, description: '卡密携带模型4额度' })
@IsNumber({}, { message: '卡密携带额度类型必须是number' })
@IsOptional()
model4Count: number;
@ApiProperty({ example: 3, description: '卡密携带MJ绘画额度' })
@IsNumber({}, { message: '卡密携带额度类型必须是number' })
@IsOptional()
drawMjCount: number;
}

View File

@@ -0,0 +1,102 @@
import {
IsNotEmpty,
MinLength,
MaxLength,
IsString,
IsIn,
IsOptional,
Max,
Min,
ValidateNested,
IsNumber,
IsDefined,
IsDecimal,
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer';
export class CreatePackageDto {
@ApiProperty({ example: '基础套餐100次卡', description: '套餐名称', required: true })
@IsDefined({ message: '套餐名称是必传参数' })
name: string;
@ApiProperty({
example: '这是一个100次对话余额的套餐我们将为您额外赠送3次绘画余额活动期间我们将在套餐基础上额外赠送您十次对话余额和1次绘画余额',
description: '套餐详情描述',
required: true,
})
@IsDefined({ message: '套餐描述是必传参数' })
des: string;
@ApiProperty({ example: 7, default: 0, description: '套餐等级设置' })
@IsNumber({}, { message: '套餐等级权重必须为数字' })
weight: number;
@ApiProperty({ example: 1, description: '套餐扣费类型 1按次数 2按Token', required: true })
deductionType: number;
@ApiProperty({ example: 'https://xxxx.png', description: '套餐封面图片' })
@IsOptional()
coverImg: string;
@Transform(({ value }) => parseFloat(value))
price: number;
@ApiProperty({ example: 100, description: '套餐排序、数字越大越靠前' })
@IsOptional()
order?: number;
@ApiProperty({ example: 1, description: '套餐状态 0禁用 1启用', required: true })
@IsNumber({}, { message: '套餐状态必须是Number' })
@IsIn([0, 1], { message: '套餐状态错误' })
status: number;
@ApiProperty({ example: 7, default: 0, description: '套餐有效期 -1为永久不过期' })
@IsNumber({}, { message: '套餐有效期天数类型必须是number' })
days: number;
@ApiProperty({ example: 1000, default: 0, description: '模型3对话次数' })
@IsNumber({}, { message: '模型3对话次数必须是number类型' })
model3Count: number;
@ApiProperty({ example: 10, default: 0, description: '模型4对话次数' })
@IsNumber({}, { message: '模型4对话次数必须是number类型' })
model4Count: number;
@ApiProperty({ example: 10, default: 0, description: 'MJ绘画次数' })
@IsNumber({}, { message: 'MJ绘画次数必须是number类型' })
drawMjCount: number;
// @ApiProperty({ example: 0, description: '卡密携带的用户余额金额' })
// @IsNumber({}, { message: '卡密余额类型必须是number' })
// balance: number;
// @ApiProperty({ example: 100, description: '卡密携带的用户对话次数' })
// @IsNumber({}, { message: '对话次数类型必须是number' })
// usesLeft: number;
// @ApiProperty({ example: 3, description: '卡密携带的用户绘画次数' })
// @IsNumber({}, { message: '绘画次数类型必须是number' })
// paintCount: number;
// @ApiProperty({ example: 1, description: '是否开启额外赠送状态 0关闭 1开启' })
// @IsNumber({}, { message: '是否开启额外赠送状态必须是Number' })
// @IsIn([0, 1], { message: '是否开启额外赠送状态只能是0或1' })
// @IsOptional()
// extraReward: number;
// @ApiProperty({ example: 0, description: '卡密携带的额外赠送用户余额金额' })
// @IsNumber({}, { message: '额外赠送卡密余额类型必须是number' })
// @IsOptional()
// extraBalance: number;
// @ApiProperty({ example: 10, description: '卡密携带的额外赠送用户对话次数' })
// @IsNumber({}, { message: '额外赠送对话次数类型必须是number' })
// @IsOptional()
// extraUsesLeft: number;
// @ApiProperty({ example: 1, description: '卡密携带的额外赠送用户绘画次数' })
// @IsNumber({}, { message: '额外赠送绘画次数类型必须是number' })
// @IsOptional()
// extraPaintCount: number;
}

View File

@@ -0,0 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNumber } from 'class-validator';
export class DeletePackageDto {
@ApiProperty({ example: 1, description: '要修改的套餐Id', required: true })
@IsNumber({}, { message: '套餐ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,22 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class QuerAllCramiDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 1, description: '使用人Id', required: false })
@IsOptional()
useId: number;
@ApiProperty({ example: 1, description: '卡密状态 0未使用 1已消费', required: false })
@IsOptional()
status: number;
}

View File

@@ -0,0 +1,26 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { BaseEntity } from 'typeorm';
export class QuerAllPackageDto {
@ApiProperty({ example: 1, description: '查询页数', required: false })
@IsOptional()
page: number;
@ApiProperty({ example: 10, description: '每页数量', required: false })
@IsOptional()
size: number;
@ApiProperty({ example: 'name', description: '套餐名称', required: false })
@IsOptional()
name: string;
@ApiProperty({ example: 1, description: '套餐状态 0禁用 1启用', required: false })
@IsOptional()
status: number;
@ApiProperty({ example: 1, description: '套餐类型 -1永久套餐 1限时套餐', required: false })
@IsOptional()
type: number;
}

View File

@@ -0,0 +1,10 @@
import { IsNotEmpty, MinLength, MaxLength, IsString, IsIn, IsOptional, Max, Min, ValidateNested, IsNumber, IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { CreatePackageDto } from './createPackage.dto';
export class UpdatePackageDto extends CreatePackageDto {
@ApiProperty({ example: 1, description: '要修改的套餐Id', required: true })
@IsNumber({}, { message: '套餐ID必须是Number' })
id: number;
}

View File

@@ -0,0 +1,8 @@
import { IsDefined } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class UseCramiDto {
@ApiProperty({ example: 'ffar684rv254fs4f', description: '卡密信息', required: true })
@IsDefined({ message: '套餐名称是必传参数' })
code: string;
}

View File

@@ -0,0 +1,23 @@
import { Module, OnModuleInit, Logger } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigService } from 'nestjs-config';
import { Connection } from 'typeorm';
import { DatabaseService } from './database.service';
@Module({
imports: [
TypeOrmModule.forRootAsync({
useFactory: (config: ConfigService) => config.get('database'),
inject: [ConfigService],
}),
],
providers: [DatabaseService],
})
export class DatabaseModule implements OnModuleInit {
constructor(private readonly connection: Connection, private readonly config: ConfigService) {}
private readonly logger = new Logger(DatabaseModule.name);
onModuleInit(): void {
const { database } = this.connection.options;
this.logger.log(`Your MySQL database named ${database} has been connected`);
}
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { DatabaseService } from './database.service';
describe('DatabaseService', () => {
let service: DatabaseService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [DatabaseService],
}).compile();
service = module.get<DatabaseService>(DatabaseService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

Some files were not shown because too many files have changed in this diff Show More