diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts index 9c5d848..f340bed 100644 --- a/src/auth/auth.module.ts +++ b/src/auth/auth.module.ts @@ -29,4 +29,4 @@ import { AuthController } from './auth.controller'; }, ], }) -export class AuthModule { } +export class AuthModule {} diff --git a/src/auth/github.strategy.ts b/src/auth/github.strategy.ts index bb27b5a..a7c1868 100644 --- a/src/auth/github.strategy.ts +++ b/src/auth/github.strategy.ts @@ -20,7 +20,10 @@ export class GitHubStrategy extends PassportStrategy(Strategy, 'github') { const email = emails?.[0]?.value || null; // 检查 GitHub 用户是否已存在 - let user = await this.userService.findUserByProvider('github', id.toString()); + let user = await this.userService.findUserByProvider( + 'github', + id.toString(), + ); if (!user) { // 如果用户不存在,创建新的 OAuth 用户 @@ -28,7 +31,7 @@ export class GitHubStrategy extends PassportStrategy(Strategy, 'github') { 'github', id.toString(), username, - email || `${username}@github.local` + email || `${username}@github.local`, ); } diff --git a/src/auth/guards/jwt-auth.guard.ts b/src/auth/guards/jwt-auth.guard.ts index 2e72bd7..6fb27d8 100644 --- a/src/auth/guards/jwt-auth.guard.ts +++ b/src/auth/guards/jwt-auth.guard.ts @@ -32,7 +32,7 @@ export class JwtAuthGuard extends AuthGuard('jwt') { handleRequest(err: any, user: any) { if (err || !user) { - throw new UnauthorizedException('请先登录'); + throw new UnauthorizedException('请先登录'); } return user; } diff --git a/src/deepseek/deepseek.service.ts b/src/deepseek/deepseek.service.ts index ba5e582..a983e08 100644 --- a/src/deepseek/deepseek.service.ts +++ b/src/deepseek/deepseek.service.ts @@ -20,17 +20,19 @@ export class DeepseekService { { messages: [ { - role: "system", - content: "你是一个虚拟货币分析师,擅长分析虚拟货币的统计文件,并且给用户提供建议,应该做空还是做多以及具体的杠杆倍数,还有应该在什么点位入场以及具体的止盈止损点位" + role: 'system', + content: + '你是一个虚拟货币分析师,擅长分析虚拟货币的统计文件,并且给用户提供建议,应该做空还是做多以及具体的杠杆倍数,还有应该在什么点位入场以及具体的止盈止损点位', }, { - role: "user", - content: "帮我分析下这份虚拟货币的统计文件,并且给我提供建议,应该做空还是做多以及具体的杠杆倍数,还有应该在什么点位入场以及具体的止盈止损点位" + role: 'user', + content: + '帮我分析下这份虚拟货币的统计文件,并且给我提供建议,应该做空还是做多以及具体的杠杆倍数,还有应该在什么点位入场以及具体的止盈止损点位', }, { - role: "user", - content: message - } + role: 'user', + content: message, + }, ], model, stream: false, diff --git a/src/github/github.controller.ts b/src/github/github.controller.ts index 030cf7e..589e021 100644 --- a/src/github/github.controller.ts +++ b/src/github/github.controller.ts @@ -3,12 +3,7 @@ import { GitHubService } from './github.service'; import { UserService } from '../user/user.service'; import { Response } from 'express'; import { Public } from '../auth/decorators/public.decorator'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiQuery, -} from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger'; @ApiTags('Github 模块') @Controller('github') diff --git a/src/github/github.module.ts b/src/github/github.module.ts index 20e60c2..60703b4 100644 --- a/src/github/github.module.ts +++ b/src/github/github.module.ts @@ -9,4 +9,4 @@ import { UserModule } from '../user/user.module'; providers: [GitHubService], exports: [GitHubService], }) -export class GitHubModule { } +export class GitHubModule {} diff --git a/src/github/github.service.ts b/src/github/github.service.ts index 551edc6..625b5da 100644 --- a/src/github/github.service.ts +++ b/src/github/github.service.ts @@ -6,7 +6,7 @@ import axios from 'axios'; @Injectable() export class GitHubService { constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} - + private readonly clientId = process.env.GITHUB_CLIENT_ID; private readonly clientSecret = process.env.GITHUB_CLIENT_SECRET; private readonly redirectUri = process.env.GITHUB_CALLBACK_URL; @@ -15,10 +15,10 @@ export class GitHubService { const state = Math.random().toString(36).substring(2, 15); const scope = 'user:email'; const callbackUrl = redirectUri || this.redirectUri; - + // 缓存 state 用于验证 await this.cacheManager.set(`github_state_${state}`, callbackUrl, 600); // 10分钟过期 - + return `https://github.com/login/oauth/authorize?client_id=${this.clientId}&redirect_uri=${encodeURIComponent( callbackUrl, )}&scope=${scope}&state=${state}`; @@ -39,9 +39,9 @@ export class GitHubService { Accept: 'application/json', }, }); - + const tokenData = response.data; - + if (tokenData && tokenData.access_token) { // 将 token 存储到缓存中,设置过期时间 await this.cacheManager.set( @@ -61,7 +61,9 @@ export class GitHubService { async getUserInfo(accessToken: string): Promise { // 先检查缓存 - const cachedUserInfo = await this.cacheManager.get(`github_user_${accessToken}`); + const cachedUserInfo = await this.cacheManager.get( + `github_user_${accessToken}`, + ); if (cachedUserInfo) { return cachedUserInfo; } @@ -76,17 +78,22 @@ export class GitHubService { }); // 获取用户邮箱信息 - const emailResponse = await axios.get('https://api.github.com/user/emails', { - headers: { - Authorization: `token ${accessToken}`, - Accept: 'application/vnd.github.v3+json', + const emailResponse = await axios.get( + 'https://api.github.com/user/emails', + { + headers: { + Authorization: `token ${accessToken}`, + Accept: 'application/vnd.github.v3+json', + }, }, - }); + ); const userInfo = { ...userResponse.data, emails: emailResponse.data, - email: emailResponse.data.find((email: any) => email.primary)?.email || userResponse.data.email, + email: + emailResponse.data.find((email: any) => email.primary)?.email || + userResponse.data.email, }; // 缓存用户信息 diff --git a/src/lark/lark.controller.ts b/src/lark/lark.controller.ts index 6651fea..95c8e7a 100644 --- a/src/lark/lark.controller.ts +++ b/src/lark/lark.controller.ts @@ -3,12 +3,7 @@ import { LarkService } from './lark.service'; import { UserService } from '../user/user.service'; import { Response } from 'express'; import { Public } from '../auth/decorators/public.decorator'; -import { - ApiTags, - ApiOperation, - ApiQuery, - ApiResponse, -} from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger'; @ApiTags('Lark 模块') @Controller('lark') diff --git a/src/lark/lark.module.ts b/src/lark/lark.module.ts index f27898b..6bcdeb4 100644 --- a/src/lark/lark.module.ts +++ b/src/lark/lark.module.ts @@ -9,4 +9,4 @@ import { UserModule } from '../user/user.module'; providers: [LarkService], exports: [LarkService], }) -export class LarkModule { } +export class LarkModule {} diff --git a/src/lark/lark.service.ts b/src/lark/lark.service.ts index 69fb4ff..f7ad854 100644 --- a/src/lark/lark.service.ts +++ b/src/lark/lark.service.ts @@ -5,7 +5,7 @@ import axios from 'axios'; @Injectable() export class LarkService { - constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) { } + constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {} private readonly appId = process.env.FEISHU_APP_ID; private readonly appSecret = process.env.FEISHU_APP_SECRET; @@ -41,25 +41,34 @@ export class LarkService { async getUserInfo(accessToken: string): Promise { // 先检查缓存 - const cachedUserInfo = await this.cacheManager.get(`lark_user_${accessToken}`); + const cachedUserInfo = await this.cacheManager.get( + `lark_user_${accessToken}`, + ); if (cachedUserInfo) { return cachedUserInfo; } try { // 获取用户基本信息 - const userResponse = await axios.get('https://open.feishu.cn/open-apis/authen/v1/user_info', { - headers: { - Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', + const userResponse = await axios.get( + 'https://open.feishu.cn/open-apis/authen/v1/user_info', + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + }, }, - }); + ); const userInfo = userResponse.data; if (userInfo && userInfo.code === 0) { // 缓存用户信息 - await this.cacheManager.set(`lark_user_${accessToken}`, userInfo.data, 3600); // 缓存1小时 + await this.cacheManager.set( + `lark_user_${accessToken}`, + userInfo.data, + 3600, + ); // 缓存1小时 return userInfo.data; } else { throw new Error('获取飞书用户信息失败'); diff --git a/src/task/dto/create-task.dto.ts b/src/task/dto/create-task.dto.ts index e608378..cccba13 100644 --- a/src/task/dto/create-task.dto.ts +++ b/src/task/dto/create-task.dto.ts @@ -1,5 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsString, IsNotEmpty, IsBoolean, IsOptional } from 'class-validator'; +import { + IsString, + IsNotEmpty, + IsBoolean, + IsOptional, + IsDateString, + IsEnum, +} from 'class-validator'; +import { TaskPriority } from '../entities/task.entity'; export class CreateTaskDto { @ApiProperty({ description: '任务标题' }) @@ -16,4 +24,19 @@ export class CreateTaskDto { @IsBoolean() @IsOptional() isCompleted?: boolean; + + @ApiProperty({ description: '任务截止日期', required: false }) + @IsDateString() + @IsOptional() + dueDate?: Date; + + @ApiProperty({ + description: '任务优先级', + enum: TaskPriority, + default: TaskPriority.MEDIUM, + required: false, + }) + @IsEnum(TaskPriority) + @IsOptional() + priority?: TaskPriority; } diff --git a/src/task/dto/reorder-tasks.dto.ts b/src/task/dto/reorder-tasks.dto.ts new file mode 100644 index 0000000..9b3c1dc --- /dev/null +++ b/src/task/dto/reorder-tasks.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsArray, IsNumber } from 'class-validator'; + +export class ReorderTasksDto { + @ApiProperty({ + description: '按新顺序排列的任务ID数组', + type: [Number], + }) + @IsArray() + @IsNumber({}, { each: true }) + taskIds: number[]; +} diff --git a/src/task/entities/task.entity.ts b/src/task/entities/task.entity.ts index 66a9eb4..08354a6 100644 --- a/src/task/entities/task.entity.ts +++ b/src/task/entities/task.entity.ts @@ -1,6 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm'; +export enum TaskPriority { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', +} + @Entity() export class Task { @PrimaryGeneratedColumn() @@ -11,11 +17,31 @@ export class Task { @ApiProperty({ description: '任务标题' }) title: string; - @Column({ nullable: true }) + @Column({ type: 'text', nullable: true }) @ApiProperty({ description: '任务描述', required: false }) description?: string; @Column({ default: false }) @ApiProperty({ description: '是否完成', default: false }) isCompleted: boolean; + + @Column({ type: 'int' }) + @ApiProperty({ description: '任务排序位置' }) + position: number; + + @Column({ type: 'datetime', nullable: true }) + @ApiProperty({ description: '任务截止日期', required: false }) + dueDate?: Date; + + @Column({ + type: 'enum', + enum: TaskPriority, + default: TaskPriority.MEDIUM, + }) + @ApiProperty({ + description: '任务优先级', + enum: TaskPriority, + default: TaskPriority.MEDIUM, + }) + priority: TaskPriority; } diff --git a/src/task/task.controller.ts b/src/task/task.controller.ts index e4f7256..d984712 100644 --- a/src/task/task.controller.ts +++ b/src/task/task.controller.ts @@ -10,12 +10,8 @@ import { import { TaskService } from './task.service'; import { CreateTaskDto } from './dto/create-task.dto'; import { UpdateTaskDto } from './dto/update-task.dto'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiParam, -} from '@nestjs/swagger'; +import { ReorderTasksDto } from './dto/reorder-tasks.dto'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger'; import { Task } from './entities/task.entity'; @ApiTags('任务模块') @@ -45,6 +41,13 @@ export class TaskController { return this.taskService.findOne(+id); } + @Patch('reorder') + @ApiOperation({ summary: '对任务进行重新排序' }) + @ApiResponse({ status: 200, description: '排序成功' }) + reorder(@Body() reorderTasksDto: ReorderTasksDto) { + return this.taskService.reorder(reorderTasksDto.taskIds); + } + @Patch(':id') @ApiOperation({ summary: '更新任务' }) @ApiParam({ name: 'id', description: '任务ID' }) diff --git a/src/task/task.service.ts b/src/task/task.service.ts index b514eb1..878c03a 100644 --- a/src/task/task.service.ts +++ b/src/task/task.service.ts @@ -12,13 +12,33 @@ export class TaskService { private taskRepository: Repository, ) {} - create(createTaskDto: CreateTaskDto) { - const task = this.taskRepository.create(createTaskDto); - return this.taskRepository.save(task); + async create(createTaskDto: CreateTaskDto) { + return await this.taskRepository.manager.transaction( + async (transactionalEntityManager) => { + // 使用悲观写锁防止并发竞态条件 + const maxPosition = await transactionalEntityManager + .createQueryBuilder(Task, 'task') + .select('MAX(task.position)', 'max') + .setLock('pessimistic_write') + .getRawOne(); + + const position = (maxPosition.max ?? -1) + 1; + + const task = transactionalEntityManager.create(Task, { + ...createTaskDto, + position, + }); + return await transactionalEntityManager.save(task); + }, + ); } findAll() { - return this.taskRepository.find(); + return this.taskRepository.find({ + order: { + position: 'ASC', + }, + }); } findOne(id: number) { @@ -34,4 +54,19 @@ export class TaskService { await this.taskRepository.delete(id); return { message: 'Deleted successfully' }; } + async reorder(taskIds: number[]) { + await this.taskRepository.manager.transaction( + async (transactionalEntityManager) => { + for (let i = 0; i < taskIds.length; i++) { + const taskId = taskIds[i]; + await transactionalEntityManager.update( + Task, + { id: taskId }, + { position: i }, + ); + } + }, + ); + return { message: 'Tasks reordered successfully' }; + } } diff --git a/src/user/user.controller.ts b/src/user/user.controller.ts index 948aa92..4a1a7aa 100644 --- a/src/user/user.controller.ts +++ b/src/user/user.controller.ts @@ -7,12 +7,7 @@ import { } from '@nestjs/common'; import { UserService } from '../user/user.service'; import { Public } from '../auth/decorators/public.decorator'; -import { - ApiTags, - ApiOperation, - ApiResponse, - ApiBody, -} from '@nestjs/swagger'; +import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger'; import { User } from './entities/user.entity'; @ApiTags('用户模块') diff --git a/src/user/user.service.ts b/src/user/user.service.ts index 4a24fdc..f512003 100644 --- a/src/user/user.service.ts +++ b/src/user/user.service.ts @@ -12,7 +12,7 @@ export class UserService { @InjectRepository(User) private userRepository: Repository, private jwtService: JwtService, - ) { } + ) {} async createUser(username: string, password: string | null, email: string) { let hashedPassword = null; @@ -35,9 +35,12 @@ export class UserService { return this.userRepository.findOne({ where: { username } }); } - async findUserByProvider(provider: string, providerId: string): Promise { + async findUserByProvider( + provider: string, + providerId: string, + ): Promise { return this.userRepository.findOne({ - where: { provider, providerId } + where: { provider, providerId }, }); } @@ -45,7 +48,7 @@ export class UserService { provider: string, providerId: string, providerUsername: string, - email: string + email: string, ): Promise { // 生成唯一的用户名:provider_providerUsername const username = `${provider}_${providerUsername}`;