mirror of
https://github.com/xiaoyiweb/YiAi.git
synced 2026-04-24 12:04:31 +08:00
初始化
This commit is contained in:
BIN
service/src/.DS_Store
vendored
Normal file
BIN
service/src/.DS_Store
vendored
Normal file
Binary file not shown.
86
service/src/app.module.ts
Normal file
86
service/src/app.module.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { ConfigModule } from 'nestjs-config';
|
||||
import { AbortInterceptor } from '@/common/interceptors/abort.interceptor';
|
||||
import { DatabaseModule } from './modules/database/database.module';
|
||||
import { resolve } from 'path';
|
||||
import { UserModule } from './modules/user/user.module';
|
||||
import { AuthModule } from './modules/auth/auth.module';
|
||||
import { MailerModule } from './modules/mailer/mailer.module';
|
||||
import { VerificationModule } from './modules/verification/verification.module';
|
||||
import { ChatgptModule } from './modules/chatgpt/chatgpt.module';
|
||||
import { CramiModule } from './modules/crami/crami.module';
|
||||
import { UserBalanceModule } from './modules/userBalance/userBalance.module';
|
||||
import { ChatLogModule } from './modules/chatLog/chatLog.module';
|
||||
import { UploadModule } from './modules/upload/upload.module';
|
||||
import { DrawModule } from './modules/draw/draw.module';
|
||||
import { RedisCacheModule } from './modules/redisCache/redisCache.module';
|
||||
import { GlobalConfigModule } from './modules/globalConfig/globalConfig.module';
|
||||
import { StatisticModule } from './modules/statistic/statistic.module';
|
||||
import { BadwordsModule } from './modules/badwords/badwords.module';
|
||||
import { AutoreplyModule } from './modules/autoreply/autoreply.module';
|
||||
import { AppModule as ApplicationModule } from './modules/app/app.module';
|
||||
// import { MjModule } from './modules/mj/mj.module';
|
||||
import { PayModule } from './modules/pay/pay.module';
|
||||
import { OrderModule } from './modules/order/order.module';
|
||||
import { FanyiModule } from './modules/fanyi/fanyi.module';
|
||||
import { OfficialModule } from './modules/official/official.module';
|
||||
import { TaskModule } from './modules/task/task.module';
|
||||
import { QueueModule } from './modules/queue/queue.module';
|
||||
import { MidjourneyModule } from './modules/midjourney/midjourney.module';
|
||||
import { ChatGroupModule } from './modules/chatGroup/chatGroup.module';
|
||||
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||
import * as fetch from 'isomorphic-fetch';
|
||||
import { join } from 'path';
|
||||
global.fetch = fetch;
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { SalesModule } from './modules/sales/sales.module';
|
||||
import { SigninModule } from './modules/signin/signin.module';
|
||||
import { MenuModule } from './modules/menu/menu.module';
|
||||
import { ModelsModule } from './modules/models/models.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [
|
||||
ServeStaticModule.forRoot({
|
||||
rootPath: join(__dirname, '..', 'public'),
|
||||
}),
|
||||
ConfigModule.load(resolve(__dirname, 'config', '**/!(*.d).{ts,js}')),
|
||||
DatabaseModule,
|
||||
UserModule,
|
||||
AuthModule,
|
||||
MailerModule,
|
||||
VerificationModule,
|
||||
ChatgptModule,
|
||||
CramiModule,
|
||||
UserBalanceModule,
|
||||
ChatLogModule,
|
||||
UploadModule,
|
||||
DrawModule,
|
||||
RedisCacheModule,
|
||||
GlobalConfigModule,
|
||||
StatisticModule,
|
||||
BadwordsModule,
|
||||
AutoreplyModule,
|
||||
ApplicationModule,
|
||||
// MjModule,
|
||||
PayModule,
|
||||
OrderModule,
|
||||
FanyiModule,
|
||||
OfficialModule,
|
||||
TaskModule,
|
||||
QueueModule,
|
||||
MidjourneyModule,
|
||||
ChatGroupModule,
|
||||
SalesModule,
|
||||
SigninModule,
|
||||
MenuModule,
|
||||
ModelsModule,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: AbortInterceptor,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
BIN
service/src/common/.DS_Store
vendored
Normal file
BIN
service/src/common/.DS_Store
vendored
Normal file
Binary file not shown.
19
service/src/common/auth/adminAuth.guard.ts
Normal file
19
service/src/common/auth/adminAuth.guard.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from './jwtAuth.guard';
|
||||
|
||||
@Injectable()
|
||||
export class AdminAuthGuard extends JwtAuthGuard {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isAuthorized = await super.canActivate(context);
|
||||
if (!isAuthorized) {
|
||||
return false;
|
||||
}
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
if (user && ['admin', 'super'].includes(user.role)) {
|
||||
return true;
|
||||
} else {
|
||||
throw new UnauthorizedException('非法操作、您的权限等级不足、无法执行当前请求!');
|
||||
}
|
||||
}
|
||||
}
|
||||
20
service/src/common/auth/jwt.strategy.ts
Normal file
20
service/src/common/auth/jwt.strategy.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ConfigService } from 'nestjs-config';
|
||||
import { AuthService } from '../../modules/auth/auth.service';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
secretOrKey: configService.get('jwt').secret,
|
||||
});
|
||||
}
|
||||
|
||||
/* fromat decode token return */
|
||||
async validate(payload): Promise<any> {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
70
service/src/common/auth/jwtAuth.guard.ts
Normal file
70
service/src/common/auth/jwtAuth.guard.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { RedisCacheService } from '@/modules/redisCache/redisCache.service';
|
||||
import { HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import * as jwt from 'jsonwebtoken';
|
||||
import { ModuleRef } from '@nestjs/core';
|
||||
import { GlobalConfigService } from '@/modules/globalConfig/globalConfig.service';
|
||||
import { atob, copyRightMsg, getRandomItemFromArray } from '../utils';
|
||||
import { AuthService } from '../../modules/auth/auth.service';
|
||||
@Injectable()
|
||||
export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
constructor(
|
||||
private redisCacheService: RedisCacheService,
|
||||
private readonly moduleRef: ModuleRef,
|
||||
private readonly globalConfigService: GlobalConfigService,
|
||||
private readonly authService: AuthService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async canActivate(context) {
|
||||
if (!this.redisCacheService) {
|
||||
this.redisCacheService = this.moduleRef.get(RedisCacheService, { strict: false });
|
||||
}
|
||||
const request = context.switchToHttp().getRequest();
|
||||
// TODO 域名检测
|
||||
const domain = request.headers['x-website-domain'];
|
||||
const token = this.extractToken(request);
|
||||
request.user = this.validateToken(token);
|
||||
const auth = this.globalConfigService.getNineAiToken();
|
||||
await this.redisCacheService.checkTokenAuth(token, request);
|
||||
return true;
|
||||
}
|
||||
|
||||
private extractToken(request) {
|
||||
if (!request.headers.authorization) {
|
||||
if (request.headers.fingerprint) {
|
||||
let id = request.headers.fingerprint;
|
||||
/* 超过mysql最大值进行截取 */
|
||||
if (id > 2147483647) {
|
||||
id = id.toString().slice(-9);
|
||||
id = Number(String(Number(id)));
|
||||
}
|
||||
const token = this.authService.createTokenFromFingerprint(id);
|
||||
return token;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const parts = request.headers.authorization.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
||||
return null;
|
||||
}
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
private validateToken(token) {
|
||||
try {
|
||||
return jwt.verify(token, process.env.JWT_SECRET);
|
||||
} catch (error) {
|
||||
throw new HttpException('亲爱的用户,请登录后继续操作,我们正在等您的到来!', HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
||||
handleRequest(err, user, info) {
|
||||
if (err || !user) {
|
||||
console.log('err: ', err);
|
||||
throw err || new UnauthorizedException();
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
19
service/src/common/auth/superAuth.guard.ts
Normal file
19
service/src/common/auth/superAuth.guard.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from './jwtAuth.guard';
|
||||
|
||||
@Injectable()
|
||||
export class SuperAuthGuard extends JwtAuthGuard {
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const isAuthorized = await super.canActivate(context);
|
||||
if (!isAuthorized) {
|
||||
return false;
|
||||
}
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user;
|
||||
if (user && user.role === 'super') {
|
||||
return true;
|
||||
} else {
|
||||
throw new UnauthorizedException('非法操作、非超级管理员无权操作!');
|
||||
}
|
||||
}
|
||||
}
|
||||
32
service/src/common/constants/balance.constant.ts
Normal file
32
service/src/common/constants/balance.constant.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export const DeductionType = {
|
||||
BALANCE: 'BALANCE_TYPE',
|
||||
CHAT: 'CHAT_TYPE',
|
||||
PAINT: 'PAINT_TYPE',
|
||||
};
|
||||
|
||||
/**
|
||||
* @description: 扣费类型
|
||||
* @param {type}
|
||||
* 1: 模型3 模型4 MJ TODO 新版更新已经修改了 TYPE 这里暂不处理
|
||||
*/
|
||||
export const DeductionKey = {
|
||||
BALANCE_TYPE: 'balance',
|
||||
CHAT_TYPE: 'usesLeft',
|
||||
PAINT_TYPE: 'paintCount',
|
||||
};
|
||||
|
||||
/**
|
||||
* @description: 账户充值类型
|
||||
* @param {type}
|
||||
* 1: 注册赠送 2: 受邀请赠送 3: 邀请人赠送 4: 购买套餐赠送 5: 管理员赠送 6:扫码支付 7: 绘画失败退款 8: 签到奖励
|
||||
*/
|
||||
export const RechargeType = {
|
||||
REG_GIFT: 1,
|
||||
INVITE_GIFT: 2,
|
||||
REFER_GIFT: 3,
|
||||
PACKAGE_GIFT: 4,
|
||||
ADMIN_GIFT: 5,
|
||||
SCAN_PAY: 6,
|
||||
DRAW_FAIL_REFUND: 7,
|
||||
SIGN_IN: 8,
|
||||
};
|
||||
21
service/src/common/constants/errorMessage.constant.ts
Normal file
21
service/src/common/constants/errorMessage.constant.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export enum ErrorMessageEnum {
|
||||
USERNAME_OR_EMAIL_ALREADY_REGISTERED = '用户名或邮箱已注册!',
|
||||
USER_NOT_FOUND = '用户不存在!',
|
||||
VERIFICATION_NOT_FOUND = '验证记录不存在!',
|
||||
VERIFICATION_CODE_EXPIRED = '验证码已过期!',
|
||||
VERIFICATION_CODE_INVALID = '验证码无效!',
|
||||
VERIFICATION_CODE_MISMATCH = '验证码不匹配!',
|
||||
VERIFICATION_CODE_SEND_FAILED = '验证码发送失败!',
|
||||
VERIFICATION_CODE_SEND_TOO_OFTEN = '验证码发送过于频繁!',
|
||||
}
|
||||
|
||||
export const OpenAiErrorCodeMessage: Record<string, string> = {
|
||||
400: '[Inter Error] 服务端错误[400]',
|
||||
401: '[Inter Error] 服务出现错误、请稍后再试一次吧[401]',
|
||||
403: '[Inter Error] 服务器拒绝访问,请稍后再试 | Server refused to access, please try again later',
|
||||
429: '[Inter Error] 当前key调用频率过高、请重新对话再试一次吧[429]',
|
||||
502: '[Inter Error] 错误的网关 | Bad Gateway[502]',
|
||||
503: '[Inter Error] 服务器繁忙,请稍后再试 | Server is busy, please try again later[503]',
|
||||
504: '[Inter Error] 网关超时 | Gateway Time-out[504]',
|
||||
500: '[Inter Error] 服务器繁忙,请稍后再试 | Internal Server Error[500]',
|
||||
};
|
||||
23
service/src/common/constants/midjourney.constant.ts
Normal file
23
service/src/common/constants/midjourney.constant.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 任务状态枚举 1: 等待中 2: 绘制中 3: 绘制完成 4: 绘制失败 5: 绘制超时
|
||||
*/
|
||||
export enum MidjourneyStatusEnum {
|
||||
WAITING = 1,
|
||||
DRAWING = 2,
|
||||
DRAWED = 3,
|
||||
DRAWFAIL = 4,
|
||||
DRAWTIMEOUT = 5,
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘画动作枚举 1: 绘画 2: 放大 3: 变换 4: 图生图 5: 重新生成 6: 无线缩放 7: 单张变化【很大|微小】
|
||||
*/
|
||||
export enum MidjourneyActionEnum {
|
||||
DRAW = 1,
|
||||
UPSCALE = 2,
|
||||
VARIATION = 3,
|
||||
GENERATE = 4,
|
||||
REGENERATE = 5,
|
||||
ZOOM = 6,
|
||||
VARY = 7,
|
||||
}
|
||||
10
service/src/common/constants/status.constant.ts
Normal file
10
service/src/common/constants/status.constant.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export enum VerificationUseStatusEnum {
|
||||
UNUSED,
|
||||
USED,
|
||||
}
|
||||
|
||||
export const ModelsMapCn = {
|
||||
1: '系统内置大模型',
|
||||
2: '百度千帆大模型',
|
||||
3: '清华智谱大模型'
|
||||
}
|
||||
19
service/src/common/constants/user.constant.ts
Normal file
19
service/src/common/constants/user.constant.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* PENDING: 审核中
|
||||
* ACTIVE: 正常状态
|
||||
* LOCKED: 账号锁定
|
||||
* BLACKLISTED: 黑名单账号
|
||||
*/
|
||||
export enum UserStatusEnum {
|
||||
PENDING,
|
||||
ACTIVE,
|
||||
LOCKED,
|
||||
BLACKLISTED,
|
||||
}
|
||||
|
||||
export const UserStatusErrMsg = {
|
||||
[UserStatusEnum.PENDING]: '当前账户未激活,请前往邮箱验证或重新发送验证码!',
|
||||
[UserStatusEnum.ACTIVE]: '当前账户已激活!',
|
||||
[UserStatusEnum.LOCKED]: '当前账户已锁定,请联系管理员解锁!',
|
||||
[UserStatusEnum.BLACKLISTED]: '当前账户已被永久封禁!',
|
||||
};
|
||||
10
service/src/common/constants/verification.constant.ts
Normal file
10
service/src/common/constants/verification.constant.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Registration: 注册账户
|
||||
* PasswordReset: 重置密码
|
||||
* ChangeEmail: 换绑邮箱
|
||||
*/
|
||||
export enum VerificationEnum {
|
||||
Registration,
|
||||
PasswordReset,
|
||||
ChangeEmail,
|
||||
}
|
||||
16
service/src/common/entity/baseEntity.ts
Normal file
16
service/src/common/entity/baseEntity.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Entity, PrimaryGeneratedColumn, CreateDateColumn, UpdateDateColumn, DeleteDateColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class BaseEntity {
|
||||
@PrimaryGeneratedColumn()
|
||||
id: number;
|
||||
|
||||
@CreateDateColumn({ type: 'datetime', length: 0, nullable: false, name: 'createdAt', comment: '创建时间' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'datetime', length: 0, nullable: false, name: 'updatedAt', comment: '更新时间' })
|
||||
updatedAt: Date;
|
||||
|
||||
@DeleteDateColumn({ type: 'datetime', length: 0, nullable: false, name: 'deletedAt', comment: '删除时间' })
|
||||
deletedAt: Date;
|
||||
}
|
||||
19
service/src/common/filters/allExceptions.filter.ts
Normal file
19
service/src/common/filters/allExceptions.filter.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus, Logger } from '@nestjs/common';
|
||||
import { formatDate } from '@/common/utils/date';
|
||||
import { Result } from '@/common/result';
|
||||
|
||||
@Catch()
|
||||
export class AllExceptionsFilter<T> implements ExceptionFilter {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
const exceptionRes: any = exception.getResponse() || 'inter server error';
|
||||
const message = exceptionRes?.message ? (Array.isArray(exceptionRes) ? exceptionRes['message'][0] : exceptionRes['message']) : exceptionRes;
|
||||
const statusCode = exception.getStatus() || 400;
|
||||
const status = exception instanceof HttpException ? exception.getStatus() : HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
response.status(status);
|
||||
response.header('Content-Type', 'application/json; charset=utf-8');
|
||||
response.send(Result.fail(statusCode, Array.isArray(message) ? message[0] : message));
|
||||
}
|
||||
}
|
||||
23
service/src/common/filters/typeOrmQueryFailed.filter.ts
Normal file
23
service/src/common/filters/typeOrmQueryFailed.filter.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Catch, ArgumentsHost, ExceptionFilter, BadRequestException } from '@nestjs/common';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
@Catch(QueryFailedError)
|
||||
export class TypeOrmQueryFailedFilter implements ExceptionFilter {
|
||||
catch(exception: QueryFailedError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse();
|
||||
const request = ctx.getRequest();
|
||||
if ((exception as any).code === 'ER_DUP_ENTRY') {
|
||||
throw new BadRequestException('该记录已经存在,请勿重复添加!');
|
||||
} else {
|
||||
console.log('other query error');
|
||||
}
|
||||
|
||||
response.status(500).json({
|
||||
statusCode: 500,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
message: `Database query failed: ${exception.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
9
service/src/common/guards/roles/roles.guard.ts
Normal file
9
service/src/common/guards/roles/roles.guard.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@Injectable()
|
||||
export class RolesGuard implements CanActivate {
|
||||
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
13
service/src/common/interceptors/abort.interceptor.ts
Normal file
13
service/src/common/interceptors/abort.interceptor.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { AbortController } from 'abort-controller';
|
||||
import { Observable } from 'rxjs';
|
||||
@Injectable()
|
||||
export class AbortInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const abortController = new AbortController();
|
||||
request.abortController = abortController;
|
||||
return next.handle();
|
||||
}
|
||||
}
|
||||
28
service/src/common/interceptors/transform.interceptor.ts
Normal file
28
service/src/common/interceptors/transform.interceptor.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, HttpException, HttpCode, HttpStatus, Logger } from '@nestjs/common';
|
||||
import { Observable, catchError, throwError } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { Result } from '@/common/result';
|
||||
|
||||
@Injectable()
|
||||
export class TransformInterceptor implements NestInterceptor {
|
||||
intercept(context: ExecutionContext, next: CallHandler): any {
|
||||
return next.handle().pipe(
|
||||
map((data) => {
|
||||
const response = context.switchToHttp().getResponse();
|
||||
const request = context.switchToHttp().getRequest();
|
||||
response.statusCode = 200;
|
||||
/* 微信类支付类通知接口需要原样输出 */
|
||||
if (request.path.includes('notify')) {
|
||||
return data;
|
||||
}
|
||||
const message = response.status < 400 ? null : response.statusText;
|
||||
return Result.success(data, message);
|
||||
}),
|
||||
catchError((error) => {
|
||||
const statusCode = error.status || 500;
|
||||
const message = (error.response || 'Internal server error') as string;
|
||||
return throwError(new HttpException(message, statusCode));
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
13
service/src/common/middleware/xml.middleware.ts
Normal file
13
service/src/common/middleware/xml.middleware.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import * as bodyParser from 'body-parser';
|
||||
|
||||
const bodyParserMiddleware = bodyParser.text({
|
||||
type: 'application/xml',
|
||||
});
|
||||
|
||||
@Injectable()
|
||||
export class XMLMiddleware implements NestMiddleware {
|
||||
use(req: any, res: any, next: () => void) {
|
||||
bodyParserMiddleware(req, res, next);
|
||||
}
|
||||
}
|
||||
21
service/src/common/result/index.ts
Normal file
21
service/src/common/result/index.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
export class Result<T> {
|
||||
code: number;
|
||||
data?: T;
|
||||
success: boolean;
|
||||
message?: string;
|
||||
|
||||
constructor(code: number, success: boolean, data?: T, message?: string) {
|
||||
this.code = code;
|
||||
this.data = data;
|
||||
this.success = success;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
static success<T>(data?: T, message = '请求成功'): Result<T> {
|
||||
return new Result<T>(200, true, data, message);
|
||||
}
|
||||
|
||||
static fail<T>(code: number, message = '请求失败', data?: T): Result<T> {
|
||||
return new Result<T>(code, false, data, message);
|
||||
}
|
||||
}
|
||||
14
service/src/common/swagger/index.ts
Normal file
14
service/src/common/swagger/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||
import { PORT, SWAGGERPREFIX, APIPREFIX } from '@/config/main';
|
||||
|
||||
const swaggerOptions = new DocumentBuilder()
|
||||
.setTitle('Nine Team api document')
|
||||
.setDescription('Nine Team api document')
|
||||
.setVersion('1.0.0')
|
||||
.addBearerAuth()
|
||||
.build();
|
||||
|
||||
export function createSwagger(app) {
|
||||
const document = SwaggerModule.createDocument(app, swaggerOptions);
|
||||
SwaggerModule.setup('/nineai/swagger/docs', app, document);
|
||||
}
|
||||
24
service/src/common/utils/base.ts
Normal file
24
service/src/common/utils/base.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Logger } from '@nestjs/common';
|
||||
import * as crypto from 'crypto';
|
||||
|
||||
const encryptionKey = 'bf3c116f2470cb4che9071240917c171';
|
||||
const initializationVector = '518363fh72eec1v4';
|
||||
const algorithm = 'aes-256-cbc';
|
||||
|
||||
export function encrypt(text: string): string {
|
||||
const cipher = crypto.createCipheriv(algorithm, encryptionKey, initializationVector);
|
||||
let encrypted = cipher.update(text, 'utf8', 'base64');
|
||||
encrypted += cipher.final('base64');
|
||||
return encrypted;
|
||||
}
|
||||
|
||||
export function decrypt(text: string): string {
|
||||
try {
|
||||
const decipher = crypto.createDecipheriv(algorithm, encryptionKey, initializationVector);
|
||||
let decrypted = decipher.update(text, 'base64', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return decrypted;
|
||||
} catch (error) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
25
service/src/common/utils/compileNetwork.ts
Normal file
25
service/src/common/utils/compileNetwork.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import axios from 'axios';
|
||||
|
||||
function formatSearchData(searchData, question) {
|
||||
const formatStr = searchData.map(({ title, body, href }) => `'${title}' : ${body} ;`).join('\n\n');
|
||||
// const formatStr = searchData.map(({ title, body, href }) => `'${title}' : ${body} ; (${href})`).join('\n\n');
|
||||
const instructions =
|
||||
'Instructions: Reply to me in the language of my request or question above. Give a comprehensive answer to the question or request I have made above. Below are some results from a web search. Use the following results to summarize the answers \n\n';
|
||||
return `${question}\n\n${instructions}\n${formatStr}`;
|
||||
}
|
||||
|
||||
export async function compileNetwork(question: string, limit = 7) {
|
||||
let searchData = [];
|
||||
try {
|
||||
const responseData = await axios.get(`https://s0.awsl.app/search?q=${question}&max_results=${limit}`);
|
||||
searchData = responseData.data;
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
searchData = [];
|
||||
}
|
||||
if (searchData.length === 0) {
|
||||
return question;
|
||||
} else {
|
||||
return formatSearchData(searchData, question);
|
||||
}
|
||||
}
|
||||
5
service/src/common/utils/createOrderId.ts
Normal file
5
service/src/common/utils/createOrderId.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { v1 as uuidv1 } from 'uuid';
|
||||
|
||||
export function createOrderId(): string {
|
||||
return uuidv1().toString().replace(/-/g, '');
|
||||
}
|
||||
5
service/src/common/utils/createRandomCode.ts
Normal file
5
service/src/common/utils/createRandomCode.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export function createRandomCode(): number {
|
||||
const min = 100000;
|
||||
const max = 999999;
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
12
service/src/common/utils/createRandomInviteCode.ts
Normal file
12
service/src/common/utils/createRandomInviteCode.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function generateRandomString(): string {
|
||||
const length = 10;
|
||||
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
const randomIndex: number = Math.floor(Math.random() * characters.length);
|
||||
result += characters.charAt(randomIndex);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
8
service/src/common/utils/createRandomNonceStr.ts
Normal file
8
service/src/common/utils/createRandomNonceStr.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function createRandomNonceStr(len: number): string {
|
||||
const data = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
let str = '';
|
||||
for (let i = 0; i < len; i++) {
|
||||
str += data.charAt(parseInt((Math.random() * data.length).toFixed(0), 10));
|
||||
}
|
||||
return str;
|
||||
}
|
||||
6
service/src/common/utils/createRandomUid.ts
Normal file
6
service/src/common/utils/createRandomUid.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { Guid } from 'guid-typescript';
|
||||
|
||||
export function createRandomUid(): string {
|
||||
const uuid = Guid.create();
|
||||
return uuid.toString().substr(0, 10).replace('-', '');
|
||||
}
|
||||
41
service/src/common/utils/date.ts
Normal file
41
service/src/common/utils/date.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import * as dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import * as a from 'dayjs/plugin/utc';
|
||||
import * as b from 'dayjs/plugin/timezone';
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
dayjs.extend(a);
|
||||
dayjs.extend(b);
|
||||
dayjs.tz.setDefault('Asia/Shanghai');
|
||||
|
||||
export function formatDate(date: string | number | Date, format = 'YYYY-MM-DD HH:mm:ss'): string {
|
||||
return dayjs(date).format(format);
|
||||
}
|
||||
|
||||
export function formatCreateOrUpdateDate(input, format = 'YYYY-MM-DD HH:mm:ss'): any[] {
|
||||
if (Array.isArray(input)) {
|
||||
return input.map((t: any) => {
|
||||
t.createdAt = t?.createdAt ? dayjs(t.createdAt).format(format) : dayjs().format(format);
|
||||
t.updatedAt = t?.updatedAt ? dayjs(t.updatedAt).format(format) : dayjs().format(format);
|
||||
return t;
|
||||
});
|
||||
} else {
|
||||
let obj: any = {}
|
||||
try {
|
||||
obj = JSON.parse(JSON.stringify(input));
|
||||
} catch (error) {
|
||||
|
||||
}
|
||||
obj?.createdAt && (obj.createdAt = dayjs(obj.createdAt).format(format));
|
||||
obj?.updatedAt && (obj.updatedAt = dayjs(obj.updatedAt).format(format));
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
export function isExpired(createdAt: Date, days: number): boolean {
|
||||
const expireDate = new Date(createdAt.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
const now = new Date();
|
||||
return now > expireDate;
|
||||
}
|
||||
|
||||
export default dayjs;
|
||||
12
service/src/common/utils/encrypt.ts
Normal file
12
service/src/common/utils/encrypt.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export function atob(str) {
|
||||
return Buffer.from(str, 'base64').toString('utf-8');
|
||||
}
|
||||
|
||||
|
||||
export const copyRightMsg = [
|
||||
'agxoTstMY8m+DJO89Iwy4zqcFTqlcj/Fa/erMTvn0IexetXaDttr4K/BN2+RbtfouXOeFjPDYnxOfQ+IIpuJ3PmtyHAzmlGFls/HvBDeh6EXAQ3waALbvK9Ue96soAb5/3Tv6VuZE7npISqXiYhI6Vqx4yDVYf6vUUkEO9jvVotWQkLOLkr6M/guLK6sik/ZOgHvSlDYKAv79NFJJ0Tt0WkH2SyN8l+woMiWVTOKkdE=',
|
||||
'nXdXi8UU7J5av2eDOFjxQWlZDa+3bdASE4UwpqT6B11XSCweKKuzHxmFO2wx45iVlib/V0tt+NbEcOQZtzEWKqHsREkwEb5aqVCUl2Kj4nJeEFId2iyvY6MWEV1lHtCY+htpJoyqwQJc7yeNfpTl2SLBubWk77p4AHei1QFEs1rpOOwyE79lF0RqzY/Cpzhs',
|
||||
'VjVCGib1VFp7hNynpKGQPUrX+ishpxi2u5a4txHXzk2nyUP1NZfIomEDmGhDTQ7VRJLox+8urtVG1CBBSct1v+4OA2ucAcDUFoy1H1Kl1z+dndVcNU6gz5YGnDppsxY8uGFAVGsWrDl2DIOKxk7kMURaRiQCXCHRF/3sLGyIEmE6KL9Q4kDInB6vuzBScxupFShMXTq2XrOhwRgn2elcig==',
|
||||
'ZPcz1IaPDMGI3Yn9sm4QOT0qCZo7yZbJl4/c2RTrhUKINkjGB5yb0yN5vAnLtt/o8cmpoOoH3PUSOOWQa9aKD86NWK+1r8wBOVjwXZOpp2gbB1ZJLbWvjRbENvEJxVsLROXnpNDqUXVGxFMaIt+gmEi3Rp0thqC1soXUpvM1zqU4+LkQmunR7UytvzwXEmXBlIfPwz5hv+n/lxDsw526KWixC3jLLpeijw5433Zh7cI=',
|
||||
'YPo1HNzS6p6190ku4f1PQENUBa/ip+v+6sPuQXVyAn3axo6SLKQBszNr3PAW2EzWhZLy2o+nBgr3o3IOy9OgNit1JHrCklpVp172wbGDKh8sB8HCXyJoRv3BaZVY5UhyhpV5K+4nPoM2RUwvIGONUGFPQfPQv9N8MS8UCL7UnWYcVLzxWo0ZDg+UXFRr7NhXKu7KQ7e1+Wiqm0qE+olfDVowi4pGDRGrYL154wEEJUo='
|
||||
]
|
||||
6
service/src/common/utils/generateCrami.ts
Normal file
6
service/src/common/utils/generateCrami.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export function generateCramiCode(): string {
|
||||
const code = uuidv4().replace(/-/g, '').slice(0, 16);
|
||||
return code;
|
||||
}
|
||||
51
service/src/common/utils/getClientIp.ts
Normal file
51
service/src/common/utils/getClientIp.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { Request } from 'express';
|
||||
|
||||
export function getClientIp(request: Request): string {
|
||||
let ipAddress = '';
|
||||
|
||||
// 预定义的一组请求头列表,按优先级排序
|
||||
const headerList = [
|
||||
'X-Client-IP',
|
||||
'X-Real-IP',
|
||||
'X-Forwarded-For',
|
||||
'CF-Connecting-IP',
|
||||
'True-Client-IP',
|
||||
'X-Cluster-Client-IP',
|
||||
'Proxy-Client-IP',
|
||||
'WL-Proxy-Client-IP',
|
||||
'HTTP_CLIENT_IP',
|
||||
'HTTP_X_FORWARDED_FOR',
|
||||
];
|
||||
|
||||
// 尝试从预定义的请求头列表中提取客户端的真实 IP 地址
|
||||
for (const header of headerList) {
|
||||
const value = request.headers[header];
|
||||
if (value && typeof value === 'string') {
|
||||
const ips = value.split(',');
|
||||
// 取最左侧的 IP 地址作为客户端的真实 IP 地址
|
||||
ipAddress = ips[0].trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法从请求头中获取到客户端的真实 IP 地址,则回退到使用 connection.remoteAddress 属性
|
||||
if (!ipAddress) {
|
||||
ipAddress = request.connection.remoteAddress || '';
|
||||
}
|
||||
|
||||
// 对获取到的 IP 地址进行格式化和过滤操作
|
||||
if (ipAddress && ipAddress.includes('::')) {
|
||||
const isLocal = /^(::1|fe80(:1)?::1(%.*)?)$/i.test(ipAddress);
|
||||
if (isLocal) {
|
||||
ipAddress = '';
|
||||
} else if (ipAddress.includes('::ffff:')) {
|
||||
ipAddress = ipAddress.split(':').pop() || '';
|
||||
}
|
||||
}
|
||||
|
||||
// 如果获取到的 IP 地址不符合格式要求,则设置为空字符串
|
||||
if (!ipAddress || !/\d+\.\d+\.\d+\.\d+/.test(ipAddress)) {
|
||||
ipAddress = '';
|
||||
}
|
||||
return ipAddress;
|
||||
}
|
||||
14
service/src/common/utils/getDiffArray.ts
Normal file
14
service/src/common/utils/getDiffArray.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export function getDiffArray(aLength: number, bLength: number, str: string): string[] {
|
||||
const a = Array.from({ length: aLength }, (_, i) => i + 1);
|
||||
const b = Array.from({ length: bLength }, (_, i) => i + 1);
|
||||
|
||||
const diffArray: string[] = [];
|
||||
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (!b.includes(a[i])) {
|
||||
diffArray.push(`${str}${a[i]}`);
|
||||
}
|
||||
}
|
||||
|
||||
return diffArray;
|
||||
}
|
||||
4
service/src/common/utils/getRandomItem.ts
Normal file
4
service/src/common/utils/getRandomItem.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export function getRandomItem<T>(array: T[]): T {
|
||||
const randomIndex = Math.floor(Math.random() * array.length);
|
||||
return array[randomIndex];
|
||||
}
|
||||
7
service/src/common/utils/getRandomItemFromArray.ts
Normal file
7
service/src/common/utils/getRandomItemFromArray.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export function getRandomItemFromArray<T>(array: T[]): T | null {
|
||||
if (array.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const randomIndex = Math.floor(Math.random() * array.length);
|
||||
return array[randomIndex];
|
||||
}
|
||||
10
service/src/common/utils/hideString.ts
Normal file
10
service/src/common/utils/hideString.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export function hideString(input: string, str?: string): string {
|
||||
const length = input.length;
|
||||
const start = input.slice(0, (length - 10) / 2);
|
||||
const end = input.slice((length + 10) / 2, length);
|
||||
const hidden = '*'.repeat(10);
|
||||
if (str) {
|
||||
return `**********${str}**********`;
|
||||
}
|
||||
return `${start}${hidden}${end}`;
|
||||
}
|
||||
22
service/src/common/utils/index.ts
Normal file
22
service/src/common/utils/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export * from './date';
|
||||
export * from './createRandomCode';
|
||||
export * from './tools';
|
||||
export * from './createRandomInviteCode';
|
||||
export * from './maskEmail';
|
||||
export * from './createRandomUid';
|
||||
export * from './generateCrami';
|
||||
export * from './base';
|
||||
export * from './hideString';
|
||||
export * from './getDiffArray';
|
||||
export * from './getRandomItem';
|
||||
export * from './getClientIp';
|
||||
export * from './maskIpAddress';
|
||||
export * from './maskCrami';
|
||||
export * from './selectKeyWithWeight';
|
||||
export * from './createOrderId';
|
||||
export * from './createRandomNonceStr';
|
||||
export * from './utcformatTime';
|
||||
export * from './removeSpecialCharacters';
|
||||
export * from './encrypt';
|
||||
export * from './compileNetwork';
|
||||
export * from './getRandomItemFromArray'
|
||||
8
service/src/common/utils/maskCrami.ts
Normal file
8
service/src/common/utils/maskCrami.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export function maskCrami(str: string): string {
|
||||
if (str.length !== 16) {
|
||||
throw new Error('Invalid input');
|
||||
}
|
||||
|
||||
const masked = str.substring(0, 6) + '****' + str.substring(10);
|
||||
return masked;
|
||||
}
|
||||
11
service/src/common/utils/maskEmail.ts
Normal file
11
service/src/common/utils/maskEmail.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export function maskEmail(email: string): string {
|
||||
if (!email) return '';
|
||||
const atIndex = email.indexOf('@');
|
||||
if (atIndex <= 1) {
|
||||
return email;
|
||||
}
|
||||
const firstPart = email.substring(0, atIndex - 1);
|
||||
const lastPart = email.substring(atIndex);
|
||||
const maskedPart = '*'.repeat(firstPart.length - 1);
|
||||
return `${firstPart.charAt(0)}${maskedPart}${email.charAt(atIndex - 1)}${lastPart}`;
|
||||
}
|
||||
6
service/src/common/utils/maskIpAddress.ts
Normal file
6
service/src/common/utils/maskIpAddress.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function maskIpAddress(ipAddress: string): string {
|
||||
if (!ipAddress) return '';
|
||||
const ipArray = ipAddress.split('.');
|
||||
ipArray[2] = '***';
|
||||
return ipArray.join('.');
|
||||
}
|
||||
3
service/src/common/utils/removeSpecialCharacters.ts
Normal file
3
service/src/common/utils/removeSpecialCharacters.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function removeSpecialCharacters(inputString) {
|
||||
return inputString.replace(/[^\w\s-]/g, '');
|
||||
}
|
||||
87
service/src/common/utils/selectKeyWithWeight.ts
Normal file
87
service/src/common/utils/selectKeyWithWeight.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
// export function selectKeyWithWeight(keys) {
|
||||
// // 创建两个数组用于存储每个键值及其对应的概率分布
|
||||
// const values = [];
|
||||
// const probabilities = [];
|
||||
|
||||
// // 获取所有 keys 的总权重
|
||||
// const totalWeight = keys.reduce((prev, curr) => prev + curr.weight, 0);
|
||||
|
||||
// // 计算每个键值所占的概率,并将其放入对应的数组中
|
||||
// for (let i = 0; i < keys.length; i++) {
|
||||
// const probability = keys[i].weight / totalWeight;
|
||||
// probabilities.push(probability);
|
||||
// values.push(i);
|
||||
// }
|
||||
|
||||
// // 创建两个辅助数组,用于记录各个键值的别名(alias)和概率分布(prob)
|
||||
// const alias = new Array(keys.length).fill(0);
|
||||
// const prob = new Array(keys.length).fill(0);
|
||||
|
||||
// // 创建两个栈,分别用于存储大于等于均值和小于均值的键值
|
||||
// const small = [];
|
||||
// const large = [];
|
||||
|
||||
// // 初始化栈以及 prob 和 alias 数组
|
||||
// for (let i = 0; i < keys.length; i++) {
|
||||
// if (probabilities[i] < 1) {
|
||||
// small.push(i);
|
||||
// } else {
|
||||
// large.push(i);
|
||||
// }
|
||||
// prob[i] = probabilities[i] * keys.length;
|
||||
// }
|
||||
|
||||
// // 循环填充 alias 和 prob 数组
|
||||
// while (small.length > 0 && large.length > 0) {
|
||||
// const smallIndex = small.pop();
|
||||
// const largeIndex = large.pop();
|
||||
|
||||
// alias[smallIndex] = largeIndex;
|
||||
// prob[largeIndex] = prob[largeIndex] + prob[smallIndex] - 1;
|
||||
|
||||
// if (prob[largeIndex] < 1) {
|
||||
// small.push(largeIndex);
|
||||
// } else {
|
||||
// large.push(largeIndex);
|
||||
// }
|
||||
// }
|
||||
|
||||
// // 随机生成一个 [0, keys.length) 范围内的整数
|
||||
// const rand = Math.floor(Math.random() * keys.length);
|
||||
|
||||
// // 根据随机值和对应的别名和概率分布数组,返回选中的键值
|
||||
// if (Math.random() < prob[rand]) {
|
||||
// return keys[rand];
|
||||
// } else {
|
||||
// return keys[alias[rand]];
|
||||
// }
|
||||
// }
|
||||
|
||||
export interface KeyItem {
|
||||
id: number;
|
||||
key: string;
|
||||
weight: number;
|
||||
model: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据概率按权重随机选择一项
|
||||
*
|
||||
* @param data 包含id、key和weight字段的Item数组
|
||||
* @returns 随机选择的一项
|
||||
*/
|
||||
export function selectKeyWithWeight(data: KeyItem[]): KeyItem | undefined {
|
||||
if (data.length === 0) return undefined;
|
||||
|
||||
const totalWeight = data.reduce((sum, item) => sum + item.weight, 0);
|
||||
let randomWeight = Math.random() * totalWeight;
|
||||
|
||||
for (const item of data) {
|
||||
randomWeight -= item.weight;
|
||||
if (randomWeight < 0) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return data[data.length - 1];
|
||||
}
|
||||
6
service/src/common/utils/tools.ts
Normal file
6
service/src/common/utils/tools.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export function isNotEmptyString(value: any): boolean {
|
||||
return typeof value === 'string' && value.length > 0;
|
||||
}
|
||||
|
||||
// === await eval('import("module")');
|
||||
export const importDynamic = new Function('modulePath', 'return import(modulePath)');
|
||||
14
service/src/common/utils/utcformatTime.ts
Normal file
14
service/src/common/utils/utcformatTime.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export function utcToShanghaiTime(utcTime: string, format = 'YYYY/MM/DD hh:mm:ss'): string {
|
||||
const date = new Date(utcTime);
|
||||
const shanghaiTime = date.getTime() + 8 * 60 * 60 * 1000;
|
||||
const shanghaiDate = new Date(shanghaiTime);
|
||||
|
||||
let result = format.replace('YYYY', shanghaiDate.getFullYear().toString());
|
||||
result = result.replace('MM', `0${shanghaiDate.getMonth() + 1}`.slice(-2));
|
||||
result = result.replace('DD', `0${shanghaiDate.getDate()}`.slice(-2));
|
||||
result = result.replace('hh', `0${shanghaiDate.getHours()}`.slice(-2));
|
||||
result = result.replace('mm', `0${shanghaiDate.getMinutes()}`.slice(-2));
|
||||
result = result.replace('ss', `0${shanghaiDate.getSeconds()}`.slice(-2));
|
||||
|
||||
return result;
|
||||
}
|
||||
30
service/src/config/.env.example
Normal file
30
service/src/config/.env.example
Normal file
@@ -0,0 +1,30 @@
|
||||
# 服务器ip
|
||||
NINE_AI_HOST=
|
||||
# 授权码
|
||||
NINE_AI_KEY=
|
||||
|
||||
# mysql
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASS=chat-nine-ai
|
||||
DB_DATABASE=chatgpt
|
||||
DB_LOG=false
|
||||
DB_SYNC=true
|
||||
|
||||
# mailer 邮件服务
|
||||
MAILER_HOST=smtp.163.com
|
||||
MAILER_PORT=465
|
||||
MAILER_USER=
|
||||
MAILER_PASS=
|
||||
MAILER_FROM=
|
||||
|
||||
# jwt token
|
||||
JWT_SECRET=chat-cooper
|
||||
JWT_EXPIRESIN=5
|
||||
SWAGGERPREFIX=/docs
|
||||
|
||||
# 系统预设 请勿更改
|
||||
PORT=9520
|
||||
PREFIX=/docs
|
||||
APIPREFIX=/api
|
||||
11
service/src/config/cos.ts
Normal file
11
service/src/config/cos.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import * as Dotenv from 'dotenv';
|
||||
Dotenv.config({ path: '.env' });
|
||||
|
||||
const config = {
|
||||
SecretId: process.env.TENTCENT_SECRET_ID,
|
||||
SecretKey: process.env.TENTCENT_SECRET_KEY,
|
||||
Bucket: process.env.COS_BUCKET_PUBLIC,
|
||||
Region: process.env.COS_REGION,
|
||||
};
|
||||
|
||||
export default config;
|
||||
18
service/src/config/database.ts
Normal file
18
service/src/config/database.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { join } from 'path';
|
||||
import { ConnectionOptions, Connection } from 'typeorm';
|
||||
|
||||
const config: ConnectionOptions = {
|
||||
type: 'mysql',
|
||||
port: parseInt(process.env.DB_PORT),
|
||||
host: process.env.DB_HOST,
|
||||
username: process.env.DB_USER,
|
||||
password: process.env.DB_PASS,
|
||||
database: process.env.DB_DATABASE,
|
||||
entities: [__dirname + '/../**/*.entity{.ts,.js}'],
|
||||
logging: false,
|
||||
synchronize: true,
|
||||
charset: 'utf8mb4',
|
||||
// timezone: 'Z',
|
||||
timezone: '+08:00',
|
||||
};
|
||||
export default config;
|
||||
8
service/src/config/jwt.ts
Normal file
8
service/src/config/jwt.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
const config = {
|
||||
secret: process.env.JWT_SECRET,
|
||||
signOptions: {
|
||||
expiresIn: process.env.JWT_EXPIRESIN || '7d',
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
25
service/src/config/mailer.ts
Normal file
25
service/src/config/mailer.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';
|
||||
import { MailerOptions } from '@nestjs-modules/mailer';
|
||||
|
||||
const mailConfig: MailerOptions = {
|
||||
transport: {
|
||||
host: process.env.MAILER_HOST || 'smtpdm.aliyun.com',
|
||||
port: process.env.MAILER_PORT || '80',
|
||||
auth: {
|
||||
user: process.env.MAILER_USER,
|
||||
pass: process.env.MAILER_PASS,
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
from: process.env.MAILER_FROM,
|
||||
},
|
||||
template: {
|
||||
dir: 'templates/mail',
|
||||
adapter: new HandlebarsAdapter(),
|
||||
options: {
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default mailConfig;
|
||||
5
service/src/config/main.ts
Normal file
5
service/src/config/main.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const SWAGGERPREFIX = process.env.SWAGGERPREFIX || '/docs';
|
||||
const APIPREFIX = process.env.APIPREFIX || '/api';
|
||||
|
||||
export { PORT, SWAGGERPREFIX, APIPREFIX };
|
||||
10
service/src/config/redis.ts
Normal file
10
service/src/config/redis.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { join } from 'path';
|
||||
|
||||
const config = {
|
||||
port: parseInt(process.env.REDIS_PORT),
|
||||
host: process.env.REDIS_HOST,
|
||||
password: process.env.REDIS_PASSWORD,
|
||||
username: process.env.REDIS_USER,
|
||||
};
|
||||
|
||||
export default config;
|
||||
6
service/src/interfaces/mail.interface.ts
Normal file
6
service/src/interfaces/mail.interface.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
export interface SendMailerOptions {
|
||||
to: string;
|
||||
subject: string;
|
||||
html?: string;
|
||||
content?: Record<string, unknown>;
|
||||
}
|
||||
40
service/src/main.ts
Normal file
40
service/src/main.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import * as Dotenv from 'dotenv';
|
||||
Dotenv.config({ path: '.env' });
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { AppModule } from './app.module';
|
||||
import { createSwagger } from '@/common/swagger';
|
||||
import { AllExceptionsFilter } from '@/common/filters/allExceptions.filter';
|
||||
import { TypeOrmQueryFailedFilter } from '@/common/filters/typeOrmQueryFailed.filter';
|
||||
import { ValidationPipe, Logger } from '@nestjs/common';
|
||||
import { TransformInterceptor } from '@/common/interceptors/transform.interceptor';
|
||||
import { join } from 'path';
|
||||
import * as express from 'express';
|
||||
import { PORT, APIPREFIX } from '@/config/main';
|
||||
import { initDatabase } from '@/modules/database/initDatabase';
|
||||
import * as compression from 'compression';
|
||||
import * as xmlBodyParser from 'express-xml-bodyparser';
|
||||
import { resolve } from 'path';
|
||||
|
||||
async function bootstrap() {
|
||||
await initDatabase();
|
||||
const app = await NestFactory.create(AppModule);
|
||||
app.use(compression());
|
||||
const www = resolve(__dirname, './public');
|
||||
app.use(xmlBodyParser());
|
||||
app.enableCors();
|
||||
app.setGlobalPrefix(APIPREFIX);
|
||||
app.useGlobalInterceptors(new TransformInterceptor());
|
||||
app.useGlobalFilters(new TypeOrmQueryFailedFilter());
|
||||
app.useGlobalFilters(new AllExceptionsFilter());
|
||||
app.useGlobalPipes(new ValidationPipe());
|
||||
app.getHttpAdapter().getInstance().set('views', 'templates/pages');
|
||||
app.getHttpAdapter().getInstance().set('view engine', 'hbs');
|
||||
|
||||
createSwagger(app);
|
||||
const server = await app.listen(PORT, () => {
|
||||
Logger.log(`服务启动成功: http://localhost:${PORT}/nineai/swagger/docs 作者:小易 QQ:805239273`, 'Main');
|
||||
});
|
||||
server.timeout = 5 * 60 * 1000;
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
BIN
service/src/modules/.DS_Store
vendored
Normal file
BIN
service/src/modules/.DS_Store
vendored
Normal file
Binary file not shown.
154
service/src/modules/app/app.controller.ts
Normal file
154
service/src/modules/app/app.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
39
service/src/modules/app/app.entity.ts
Normal file
39
service/src/modules/app/app.entity.ts
Normal 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;
|
||||
}
|
||||
14
service/src/modules/app/app.module.ts
Normal file
14
service/src/modules/app/app.module.ts
Normal 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 {}
|
||||
318
service/src/modules/app/app.service.ts
Normal file
318
service/src/modules/app/app.service.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
21
service/src/modules/app/appCats.entity.ts
Normal file
21
service/src/modules/app/appCats.entity.ts
Normal 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;
|
||||
}
|
||||
8
service/src/modules/app/dto/collectApp.dto.ts
Normal file
8
service/src/modules/app/dto/collectApp.dto.ts
Normal 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;
|
||||
}
|
||||
44
service/src/modules/app/dto/createApp.dto.ts
Normal file
44
service/src/modules/app/dto/createApp.dto.ts
Normal 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;
|
||||
}
|
||||
30
service/src/modules/app/dto/createCats.dto.ts
Normal file
30
service/src/modules/app/dto/createCats.dto.ts
Normal 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;
|
||||
}
|
||||
35
service/src/modules/app/dto/custonApp.dto.ts
Normal file
35
service/src/modules/app/dto/custonApp.dto.ts
Normal 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;
|
||||
}
|
||||
8
service/src/modules/app/dto/deleteApp.dto.ts
Normal file
8
service/src/modules/app/dto/deleteApp.dto.ts
Normal 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;
|
||||
}
|
||||
8
service/src/modules/app/dto/deleteCats.dto.ts
Normal file
8
service/src/modules/app/dto/deleteCats.dto.ts
Normal 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;
|
||||
}
|
||||
30
service/src/modules/app/dto/queryApp.dto.ts
Normal file
30
service/src/modules/app/dto/queryApp.dto.ts
Normal 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;
|
||||
}
|
||||
22
service/src/modules/app/dto/queryCats.dto.ts
Normal file
22
service/src/modules/app/dto/queryCats.dto.ts
Normal 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;
|
||||
}
|
||||
10
service/src/modules/app/dto/updateApp.dto.ts
Normal file
10
service/src/modules/app/dto/updateApp.dto.ts
Normal 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;
|
||||
}
|
||||
10
service/src/modules/app/dto/updateCats.dto.ts
Normal file
10
service/src/modules/app/dto/updateCats.dto.ts
Normal 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;
|
||||
}
|
||||
27
service/src/modules/app/userApps.entity.ts
Normal file
27
service/src/modules/app/userApps.entity.ts
Normal 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;
|
||||
}
|
||||
102
service/src/modules/auth/auth.controller.ts
Normal file
102
service/src/modules/auth/auth.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
60
service/src/modules/auth/auth.module.ts
Normal file
60
service/src/modules/auth/auth.module.ts
Normal 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 {}
|
||||
274
service/src/modules/auth/auth.service.ts
Normal file
274
service/src/modules/auth/auth.service.ts
Normal 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}®isterSuccessEmailTitle=${
|
||||
configMap.registerSuccessEmailTitle
|
||||
}®isterSuccessEmailTeamName=${configMap.registerSuccessEmailTeamName}®isterSuccessEmaileAppend=${
|
||||
configMap.registerSuccessEmaileAppend
|
||||
}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.log('error: ', error);
|
||||
const message = error.response;
|
||||
res.redirect(
|
||||
`/api/auth/registerError?message=${message}®isterFailEmailTitle=${configMap.registerFailEmailTitle}®isterFailEmailTeamName=${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;
|
||||
}
|
||||
}
|
||||
18
service/src/modules/auth/dto/adminLogin.dto.ts
Normal file
18
service/src/modules/auth/dto/adminLogin.dto.ts
Normal 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;
|
||||
}
|
||||
22
service/src/modules/auth/dto/authLogin.dto.ts
Normal file
22
service/src/modules/auth/dto/authLogin.dto.ts
Normal 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;
|
||||
}
|
||||
46
service/src/modules/auth/dto/authRegister.dto.ts
Normal file
46
service/src/modules/auth/dto/authRegister.dto.ts
Normal 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;
|
||||
}
|
||||
16
service/src/modules/auth/dto/loginByPhone.dt.ts
Normal file
16
service/src/modules/auth/dto/loginByPhone.dt.ts
Normal 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;
|
||||
}
|
||||
19
service/src/modules/auth/dto/sendPhoneCode.dto.ts
Normal file
19
service/src/modules/auth/dto/sendPhoneCode.dto.ts
Normal 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;
|
||||
}
|
||||
10
service/src/modules/auth/dto/updatePassByOther.dto.ts
Normal file
10
service/src/modules/auth/dto/updatePassByOther.dto.ts
Normal 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;
|
||||
}
|
||||
16
service/src/modules/auth/dto/updatePassword.dto.ts
Normal file
16
service/src/modules/auth/dto/updatePassword.dto.ts
Normal 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;
|
||||
}
|
||||
30
service/src/modules/auth/dto/userRegisterByPhone.dto.ts
Normal file
30
service/src/modules/auth/dto/userRegisterByPhone.dto.ts
Normal 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;
|
||||
}
|
||||
14
service/src/modules/autoreply/autoreplay.entity.ts
Normal file
14
service/src/modules/autoreply/autoreplay.entity.ts
Normal 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;
|
||||
}
|
||||
47
service/src/modules/autoreply/autoreply.controller.ts
Normal file
47
service/src/modules/autoreply/autoreply.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
14
service/src/modules/autoreply/autoreply.module.ts
Normal file
14
service/src/modules/autoreply/autoreply.module.ts
Normal 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 {}
|
||||
87
service/src/modules/autoreply/autoreply.service.ts
Normal file
87
service/src/modules/autoreply/autoreply.service.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
10
service/src/modules/autoreply/dto/addAutoReply.dto.ts
Normal file
10
service/src/modules/autoreply/dto/addAutoReply.dto.ts
Normal 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;
|
||||
}
|
||||
7
service/src/modules/autoreply/dto/delBadWords.dto.ts
Normal file
7
service/src/modules/autoreply/dto/delBadWords.dto.ts
Normal 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;
|
||||
}
|
||||
20
service/src/modules/autoreply/dto/queryAutoReply.dto.ts
Normal file
20
service/src/modules/autoreply/dto/queryAutoReply.dto.ts
Normal 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;
|
||||
}
|
||||
20
service/src/modules/autoreply/dto/updateAutoReply.dto.ts
Normal file
20
service/src/modules/autoreply/dto/updateAutoReply.dto.ts
Normal 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;
|
||||
}
|
||||
56
service/src/modules/badwords/badwords.controller.ts
Normal file
56
service/src/modules/badwords/badwords.controller.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
14
service/src/modules/badwords/badwords.entity.ts
Normal file
14
service/src/modules/badwords/badwords.entity.ts
Normal 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;
|
||||
}
|
||||
16
service/src/modules/badwords/badwords.module.ts
Normal file
16
service/src/modules/badwords/badwords.module.ts
Normal 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 {}
|
||||
239
service/src/modules/badwords/badwords.service.ts
Normal file
239
service/src/modules/badwords/badwords.service.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
7
service/src/modules/badwords/dto/addBadWords.dto.ts
Normal file
7
service/src/modules/badwords/dto/addBadWords.dto.ts
Normal 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;
|
||||
}
|
||||
7
service/src/modules/badwords/dto/delBadWords.dto.ts
Normal file
7
service/src/modules/badwords/dto/delBadWords.dto.ts
Normal 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;
|
||||
}
|
||||
20
service/src/modules/badwords/dto/queryBadWords.dto.ts
Normal file
20
service/src/modules/badwords/dto/queryBadWords.dto.ts
Normal 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;
|
||||
}
|
||||
20
service/src/modules/badwords/dto/queryViolation.dto.ts
Normal file
20
service/src/modules/badwords/dto/queryViolation.dto.ts
Normal 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;
|
||||
}
|
||||
16
service/src/modules/badwords/dto/updateBadWords.dto.ts
Normal file
16
service/src/modules/badwords/dto/updateBadWords.dto.ts
Normal 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;
|
||||
}
|
||||
20
service/src/modules/badwords/violationLog.entity.ts
Normal file
20
service/src/modules/badwords/violationLog.entity.ts
Normal 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;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user