feat: 添加任务重新排序功能并增强任务实体属性。

This commit is contained in:
lichao 2025-12-02 20:57:09 +08:00
parent 1fe53ab9de
commit 7274c58329
17 changed files with 175 additions and 67 deletions

View File

@ -29,4 +29,4 @@ import { AuthController } from './auth.controller';
},
],
})
export class AuthModule { }
export class AuthModule {}

View File

@ -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`,
);
}

View File

@ -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,

View File

@ -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')

View File

@ -9,4 +9,4 @@ import { UserModule } from '../user/user.module';
providers: [GitHubService],
exports: [GitHubService],
})
export class GitHubModule { }
export class GitHubModule {}

View File

@ -61,7 +61,9 @@ export class GitHubService {
async getUserInfo(accessToken: string): Promise<any> {
// 先检查缓存
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', {
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,
};
// 缓存用户信息

View File

@ -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')

View File

@ -9,4 +9,4 @@ import { UserModule } from '../user/user.module';
providers: [LarkService],
exports: [LarkService],
})
export class LarkModule { }
export class LarkModule {}

View File

@ -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<any> {
// 先检查缓存
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', {
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('获取飞书用户信息失败');

View File

@ -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;
}

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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' })

View File

@ -12,13 +12,33 @@ export class TaskService {
private taskRepository: Repository<Task>,
) {}
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' };
}
}

View File

@ -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('用户模块')

View File

@ -12,7 +12,7 @@ export class UserService {
@InjectRepository(User)
private userRepository: Repository<User>,
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<User | undefined> {
async findUserByProvider(
provider: string,
providerId: string,
): Promise<User | undefined> {
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<User> {
// 生成唯一的用户名provider_providerUsername
const username = `${provider}_${providerUsername}`;