feat: 添加任务重新排序功能并增强任务实体属性。
This commit is contained in:
parent
1fe53ab9de
commit
7274c58329
|
|
@ -29,4 +29,4 @@ import { AuthController } from './auth.controller';
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AuthModule { }
|
export class AuthModule {}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@ export class GitHubStrategy extends PassportStrategy(Strategy, 'github') {
|
||||||
const email = emails?.[0]?.value || null;
|
const email = emails?.[0]?.value || null;
|
||||||
|
|
||||||
// 检查 GitHub 用户是否已存在
|
// 检查 GitHub 用户是否已存在
|
||||||
let user = await this.userService.findUserByProvider('github', id.toString());
|
let user = await this.userService.findUserByProvider(
|
||||||
|
'github',
|
||||||
|
id.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// 如果用户不存在,创建新的 OAuth 用户
|
// 如果用户不存在,创建新的 OAuth 用户
|
||||||
|
|
@ -28,7 +31,7 @@ export class GitHubStrategy extends PassportStrategy(Strategy, 'github') {
|
||||||
'github',
|
'github',
|
||||||
id.toString(),
|
id.toString(),
|
||||||
username,
|
username,
|
||||||
email || `${username}@github.local`
|
email || `${username}@github.local`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,17 +20,19 @@ export class DeepseekService {
|
||||||
{
|
{
|
||||||
messages: [
|
messages: [
|
||||||
{
|
{
|
||||||
role: "system",
|
role: 'system',
|
||||||
content: "你是一个虚拟货币分析师,擅长分析虚拟货币的统计文件,并且给用户提供建议,应该做空还是做多以及具体的杠杆倍数,还有应该在什么点位入场以及具体的止盈止损点位"
|
content:
|
||||||
|
'你是一个虚拟货币分析师,擅长分析虚拟货币的统计文件,并且给用户提供建议,应该做空还是做多以及具体的杠杆倍数,还有应该在什么点位入场以及具体的止盈止损点位',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: 'user',
|
||||||
content: "帮我分析下这份虚拟货币的统计文件,并且给我提供建议,应该做空还是做多以及具体的杠杆倍数,还有应该在什么点位入场以及具体的止盈止损点位"
|
content:
|
||||||
|
'帮我分析下这份虚拟货币的统计文件,并且给我提供建议,应该做空还是做多以及具体的杠杆倍数,还有应该在什么点位入场以及具体的止盈止损点位',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
role: "user",
|
role: 'user',
|
||||||
content: message
|
content: message,
|
||||||
}
|
},
|
||||||
],
|
],
|
||||||
model,
|
model,
|
||||||
stream: false,
|
stream: false,
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,7 @@ import { GitHubService } from './github.service';
|
||||||
import { UserService } from '../user/user.service';
|
import { UserService } from '../user/user.service';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Public } from '../auth/decorators/public.decorator';
|
import { Public } from '../auth/decorators/public.decorator';
|
||||||
import {
|
import { ApiTags, ApiOperation, ApiResponse, ApiQuery } from '@nestjs/swagger';
|
||||||
ApiTags,
|
|
||||||
ApiOperation,
|
|
||||||
ApiResponse,
|
|
||||||
ApiQuery,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
|
|
||||||
@ApiTags('Github 模块')
|
@ApiTags('Github 模块')
|
||||||
@Controller('github')
|
@Controller('github')
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,4 @@ import { UserModule } from '../user/user.module';
|
||||||
providers: [GitHubService],
|
providers: [GitHubService],
|
||||||
exports: [GitHubService],
|
exports: [GitHubService],
|
||||||
})
|
})
|
||||||
export class GitHubModule { }
|
export class GitHubModule {}
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,9 @@ export class GitHubService {
|
||||||
|
|
||||||
async getUserInfo(accessToken: string): Promise<any> {
|
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) {
|
if (cachedUserInfo) {
|
||||||
return 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(
|
||||||
headers: {
|
'https://api.github.com/user/emails',
|
||||||
Authorization: `token ${accessToken}`,
|
{
|
||||||
Accept: 'application/vnd.github.v3+json',
|
headers: {
|
||||||
|
Authorization: `token ${accessToken}`,
|
||||||
|
Accept: 'application/vnd.github.v3+json',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const userInfo = {
|
const userInfo = {
|
||||||
...userResponse.data,
|
...userResponse.data,
|
||||||
emails: emailResponse.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,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 缓存用户信息
|
// 缓存用户信息
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,7 @@ import { LarkService } from './lark.service';
|
||||||
import { UserService } from '../user/user.service';
|
import { UserService } from '../user/user.service';
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { Public } from '../auth/decorators/public.decorator';
|
import { Public } from '../auth/decorators/public.decorator';
|
||||||
import {
|
import { ApiTags, ApiOperation, ApiQuery, ApiResponse } from '@nestjs/swagger';
|
||||||
ApiTags,
|
|
||||||
ApiOperation,
|
|
||||||
ApiQuery,
|
|
||||||
ApiResponse,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
|
|
||||||
@ApiTags('Lark 模块')
|
@ApiTags('Lark 模块')
|
||||||
@Controller('lark')
|
@Controller('lark')
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,4 @@ import { UserModule } from '../user/user.module';
|
||||||
providers: [LarkService],
|
providers: [LarkService],
|
||||||
exports: [LarkService],
|
exports: [LarkService],
|
||||||
})
|
})
|
||||||
export class LarkModule { }
|
export class LarkModule {}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import axios from 'axios';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LarkService {
|
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 appId = process.env.FEISHU_APP_ID;
|
||||||
private readonly appSecret = process.env.FEISHU_APP_SECRET;
|
private readonly appSecret = process.env.FEISHU_APP_SECRET;
|
||||||
|
|
||||||
|
|
@ -41,25 +41,34 @@ export class LarkService {
|
||||||
|
|
||||||
async getUserInfo(accessToken: string): Promise<any> {
|
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) {
|
if (cachedUserInfo) {
|
||||||
return cachedUserInfo;
|
return cachedUserInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取用户基本信息
|
// 获取用户基本信息
|
||||||
const userResponse = await axios.get('https://open.feishu.cn/open-apis/authen/v1/user_info', {
|
const userResponse = await axios.get(
|
||||||
headers: {
|
'https://open.feishu.cn/open-apis/authen/v1/user_info',
|
||||||
Authorization: `Bearer ${accessToken}`,
|
{
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
|
Authorization: `Bearer ${accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
const userInfo = userResponse.data;
|
const userInfo = userResponse.data;
|
||||||
|
|
||||||
if (userInfo && userInfo.code === 0) {
|
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;
|
return userInfo.data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error('获取飞书用户信息失败');
|
throw new Error('获取飞书用户信息失败');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
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 {
|
export class CreateTaskDto {
|
||||||
@ApiProperty({ description: '任务标题' })
|
@ApiProperty({ description: '任务标题' })
|
||||||
|
|
@ -16,4 +24,19 @@ export class CreateTaskDto {
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
isCompleted?: boolean;
|
isCompleted?: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '任务截止日期', required: false })
|
||||||
|
@IsDateString()
|
||||||
|
@IsOptional()
|
||||||
|
dueDate?: Date;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '任务优先级',
|
||||||
|
enum: TaskPriority,
|
||||||
|
default: TaskPriority.MEDIUM,
|
||||||
|
required: false,
|
||||||
|
})
|
||||||
|
@IsEnum(TaskPriority)
|
||||||
|
@IsOptional()
|
||||||
|
priority?: TaskPriority;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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[];
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
|
export enum TaskPriority {
|
||||||
|
LOW = 'low',
|
||||||
|
MEDIUM = 'medium',
|
||||||
|
HIGH = 'high',
|
||||||
|
}
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class Task {
|
export class Task {
|
||||||
@PrimaryGeneratedColumn()
|
@PrimaryGeneratedColumn()
|
||||||
|
|
@ -11,11 +17,31 @@ export class Task {
|
||||||
@ApiProperty({ description: '任务标题' })
|
@ApiProperty({ description: '任务标题' })
|
||||||
title: string;
|
title: string;
|
||||||
|
|
||||||
@Column({ nullable: true })
|
@Column({ type: 'text', nullable: true })
|
||||||
@ApiProperty({ description: '任务描述', required: false })
|
@ApiProperty({ description: '任务描述', required: false })
|
||||||
description?: string;
|
description?: string;
|
||||||
|
|
||||||
@Column({ default: false })
|
@Column({ default: false })
|
||||||
@ApiProperty({ description: '是否完成', default: false })
|
@ApiProperty({ description: '是否完成', default: false })
|
||||||
isCompleted: boolean;
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,12 +10,8 @@ import {
|
||||||
import { TaskService } from './task.service';
|
import { TaskService } from './task.service';
|
||||||
import { CreateTaskDto } from './dto/create-task.dto';
|
import { CreateTaskDto } from './dto/create-task.dto';
|
||||||
import { UpdateTaskDto } from './dto/update-task.dto';
|
import { UpdateTaskDto } from './dto/update-task.dto';
|
||||||
import {
|
import { ReorderTasksDto } from './dto/reorder-tasks.dto';
|
||||||
ApiTags,
|
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
|
||||||
ApiOperation,
|
|
||||||
ApiResponse,
|
|
||||||
ApiParam,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
import { Task } from './entities/task.entity';
|
import { Task } from './entities/task.entity';
|
||||||
|
|
||||||
@ApiTags('任务模块')
|
@ApiTags('任务模块')
|
||||||
|
|
@ -45,6 +41,13 @@ export class TaskController {
|
||||||
return this.taskService.findOne(+id);
|
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')
|
@Patch(':id')
|
||||||
@ApiOperation({ summary: '更新任务' })
|
@ApiOperation({ summary: '更新任务' })
|
||||||
@ApiParam({ name: 'id', description: '任务ID' })
|
@ApiParam({ name: 'id', description: '任务ID' })
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,33 @@ export class TaskService {
|
||||||
private taskRepository: Repository<Task>,
|
private taskRepository: Repository<Task>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
create(createTaskDto: CreateTaskDto) {
|
async create(createTaskDto: CreateTaskDto) {
|
||||||
const task = this.taskRepository.create(createTaskDto);
|
return await this.taskRepository.manager.transaction(
|
||||||
return this.taskRepository.save(task);
|
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() {
|
findAll() {
|
||||||
return this.taskRepository.find();
|
return this.taskRepository.find({
|
||||||
|
order: {
|
||||||
|
position: 'ASC',
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
findOne(id: number) {
|
findOne(id: number) {
|
||||||
|
|
@ -34,4 +54,19 @@ export class TaskService {
|
||||||
await this.taskRepository.delete(id);
|
await this.taskRepository.delete(id);
|
||||||
return { message: 'Deleted successfully' };
|
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' };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,7 @@ import {
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { UserService } from '../user/user.service';
|
import { UserService } from '../user/user.service';
|
||||||
import { Public } from '../auth/decorators/public.decorator';
|
import { Public } from '../auth/decorators/public.decorator';
|
||||||
import {
|
import { ApiTags, ApiOperation, ApiResponse, ApiBody } from '@nestjs/swagger';
|
||||||
ApiTags,
|
|
||||||
ApiOperation,
|
|
||||||
ApiResponse,
|
|
||||||
ApiBody,
|
|
||||||
} from '@nestjs/swagger';
|
|
||||||
import { User } from './entities/user.entity';
|
import { User } from './entities/user.entity';
|
||||||
|
|
||||||
@ApiTags('用户模块')
|
@ApiTags('用户模块')
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export class UserService {
|
||||||
@InjectRepository(User)
|
@InjectRepository(User)
|
||||||
private userRepository: Repository<User>,
|
private userRepository: Repository<User>,
|
||||||
private jwtService: JwtService,
|
private jwtService: JwtService,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
async createUser(username: string, password: string | null, email: string) {
|
async createUser(username: string, password: string | null, email: string) {
|
||||||
let hashedPassword = null;
|
let hashedPassword = null;
|
||||||
|
|
@ -35,9 +35,12 @@ export class UserService {
|
||||||
return this.userRepository.findOne({ where: { username } });
|
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({
|
return this.userRepository.findOne({
|
||||||
where: { provider, providerId }
|
where: { provider, providerId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,7 +48,7 @@ export class UserService {
|
||||||
provider: string,
|
provider: string,
|
||||||
providerId: string,
|
providerId: string,
|
||||||
providerUsername: string,
|
providerUsername: string,
|
||||||
email: string
|
email: string,
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
// 生成唯一的用户名:provider_providerUsername
|
// 生成唯一的用户名:provider_providerUsername
|
||||||
const username = `${provider}_${providerUsername}`;
|
const username = `${provider}_${providerUsername}`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue