初始化游戏小组管理系统后端项目
- 基于 NestJS + TypeScript + MySQL + Redis 架构 - 完整的模块化设计(认证、用户、小组、游戏、预约等) - JWT 认证和 RBAC 权限控制系统 - Docker 容器化部署支持 - 添加 CLAUDE.md 项目开发指南 - 配置 .gitignore 忽略文件 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
71
src/modules/honors/dto/honor.dto.ts
Normal file
71
src/modules/honors/dto/honor.dto.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
IsString,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsArray,
|
||||
IsDateString,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateHonorDto {
|
||||
@ApiProperty({ description: '小组ID' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '小组ID不能为空' })
|
||||
groupId: string;
|
||||
|
||||
@ApiProperty({ description: '荣誉标题', example: '首次五连胜' })
|
||||
@IsString()
|
||||
@IsNotEmpty({ message: '标题不能为空' })
|
||||
@MaxLength(100)
|
||||
title: string;
|
||||
|
||||
@ApiProperty({ description: '荣誉描述', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ description: '媒体文件URL列表(图片/视频)', required: false })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
mediaUrls?: string[];
|
||||
|
||||
@ApiProperty({ description: '荣誉获得日期', required: false })
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
achievedDate?: Date;
|
||||
}
|
||||
|
||||
export class UpdateHonorDto {
|
||||
@ApiProperty({ description: '荣誉标题', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@MaxLength(100)
|
||||
title?: string;
|
||||
|
||||
@ApiProperty({ description: '荣誉描述', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
description?: string;
|
||||
|
||||
@ApiProperty({ description: '媒体文件URL列表', required: false })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
mediaUrls?: string[];
|
||||
|
||||
@ApiProperty({ description: '事件日期', required: false })
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
eventDate?: Date;
|
||||
}
|
||||
|
||||
export class QueryHonorsDto {
|
||||
@ApiProperty({ description: '小组ID', required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
groupId?: string;
|
||||
|
||||
@ApiProperty({ description: '年份筛选', required: false, example: 2024 })
|
||||
@IsOptional()
|
||||
year?: number;
|
||||
}
|
||||
64
src/modules/honors/honors.controller.ts
Normal file
64
src/modules/honors/honors.controller.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Body,
|
||||
Patch,
|
||||
Param,
|
||||
Delete,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { ApiTags, ApiOperation, ApiBearerAuth } from '@nestjs/swagger';
|
||||
import { HonorsService } from './honors.service';
|
||||
import { CreateHonorDto, UpdateHonorDto, QueryHonorsDto } from './dto/honor.dto';
|
||||
import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard';
|
||||
import { CurrentUser } from '../../common/decorators/current-user.decorator';
|
||||
|
||||
@ApiTags('honors')
|
||||
@Controller('honors')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth()
|
||||
export class HonorsController {
|
||||
constructor(private readonly honorsService: HonorsService) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: '创建荣誉记录' })
|
||||
create(@CurrentUser() user, @Body() createDto: CreateHonorDto) {
|
||||
return this.honorsService.create(user.id, createDto);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@ApiOperation({ summary: '查询荣誉列表' })
|
||||
findAll(@Query() query: QueryHonorsDto) {
|
||||
return this.honorsService.findAll(query);
|
||||
}
|
||||
|
||||
@Get('timeline/:groupId')
|
||||
@ApiOperation({ summary: '获取小组荣誉时间轴' })
|
||||
getTimeline(@Param('groupId') groupId: string) {
|
||||
return this.honorsService.getTimeline(groupId);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '查询单个荣誉记录' })
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.honorsService.findOne(id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@ApiOperation({ summary: '更新荣誉记录' })
|
||||
update(
|
||||
@CurrentUser() user,
|
||||
@Param('id') id: string,
|
||||
@Body() updateDto: UpdateHonorDto,
|
||||
) {
|
||||
return this.honorsService.update(user.id, id, updateDto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@ApiOperation({ summary: '删除荣誉记录' })
|
||||
remove(@CurrentUser() user, @Param('id') id: string) {
|
||||
return this.honorsService.remove(user.id, id);
|
||||
}
|
||||
}
|
||||
15
src/modules/honors/honors.module.ts
Normal file
15
src/modules/honors/honors.module.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { HonorsController } from './honors.controller';
|
||||
import { HonorsService } from './honors.service';
|
||||
import { Honor } from '../../entities/honor.entity';
|
||||
import { Group } from '../../entities/group.entity';
|
||||
import { GroupMember } from '../../entities/group-member.entity';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([Honor, Group, GroupMember])],
|
||||
controllers: [HonorsController],
|
||||
providers: [HonorsService],
|
||||
exports: [HonorsService],
|
||||
})
|
||||
export class HonorsModule {}
|
||||
313
src/modules/honors/honors.service.spec.ts
Normal file
313
src/modules/honors/honors.service.spec.ts
Normal file
@@ -0,0 +1,313 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { HonorsService } from './honors.service';
|
||||
import { Honor } from '../../entities/honor.entity';
|
||||
import { Group } from '../../entities/group.entity';
|
||||
import { GroupMember } from '../../entities/group-member.entity';
|
||||
import { GroupMemberRole } from '../../common/enums';
|
||||
import { NotFoundException, ForbiddenException } from '@nestjs/common';
|
||||
|
||||
describe('HonorsService', () => {
|
||||
let service: HonorsService;
|
||||
let honorRepository: Repository<Honor>;
|
||||
let groupRepository: Repository<Group>;
|
||||
let groupMemberRepository: Repository<GroupMember>;
|
||||
|
||||
const mockHonor = {
|
||||
id: 'honor-1',
|
||||
groupId: 'group-1',
|
||||
title: '冠军荣誉',
|
||||
description: '获得比赛冠军',
|
||||
eventDate: new Date('2025-01-01'),
|
||||
media: ['image1.jpg'],
|
||||
createdBy: 'user-1',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
const mockGroup = {
|
||||
id: 'group-1',
|
||||
name: '测试小组',
|
||||
ownerId: 'user-1',
|
||||
};
|
||||
|
||||
const mockGroupMember = {
|
||||
id: 'member-1',
|
||||
userId: 'user-1',
|
||||
groupId: 'group-1',
|
||||
role: GroupMemberRole.ADMIN,
|
||||
};
|
||||
|
||||
const mockQueryBuilder = {
|
||||
leftJoinAndSelect: jest.fn().mockReturnThis(),
|
||||
andWhere: jest.fn().mockReturnThis(),
|
||||
orderBy: jest.fn().mockReturnThis(),
|
||||
getMany: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
HonorsService,
|
||||
{
|
||||
provide: getRepositoryToken(Honor),
|
||||
useValue: {
|
||||
create: jest.fn(),
|
||||
save: jest.fn(),
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
remove: jest.fn(),
|
||||
createQueryBuilder: jest.fn(() => mockQueryBuilder),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(Group),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(GroupMember),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<HonorsService>(HonorsService);
|
||||
honorRepository = module.get<Repository<Honor>>(getRepositoryToken(Honor));
|
||||
groupRepository = module.get<Repository<Group>>(getRepositoryToken(Group));
|
||||
groupMemberRepository = module.get<Repository<GroupMember>>(getRepositoryToken(GroupMember));
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
|
||||
describe('create', () => {
|
||||
it('应该成功创建荣誉记录(管理员)', async () => {
|
||||
const createDto = {
|
||||
groupId: 'group-1',
|
||||
title: '冠军荣誉',
|
||||
description: '获得比赛冠军',
|
||||
eventDate: new Date('2025-01-01'),
|
||||
media: ['image1.jpg'],
|
||||
};
|
||||
|
||||
jest.spyOn(groupRepository, 'findOne').mockResolvedValue(mockGroup as any);
|
||||
jest.spyOn(groupMemberRepository, 'findOne').mockResolvedValue(mockGroupMember as any);
|
||||
jest.spyOn(honorRepository, 'create').mockReturnValue(mockHonor as any);
|
||||
jest.spyOn(honorRepository, 'save').mockResolvedValue(mockHonor as any);
|
||||
jest.spyOn(honorRepository, 'findOne').mockResolvedValue(mockHonor as any);
|
||||
|
||||
const result = await service.create('user-1', createDto);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(honorRepository.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('小组不存在时应该抛出异常', async () => {
|
||||
const createDto = {
|
||||
groupId: 'group-1',
|
||||
title: '冠军荣誉',
|
||||
eventDate: new Date('2025-01-01'),
|
||||
};
|
||||
|
||||
jest.spyOn(groupRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(service.create('user-1', createDto)).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('非管理员创建时应该抛出异常', async () => {
|
||||
const createDto = {
|
||||
groupId: 'group-1',
|
||||
title: '冠军荣誉',
|
||||
eventDate: new Date('2025-01-01'),
|
||||
};
|
||||
|
||||
jest.spyOn(groupRepository, 'findOne').mockResolvedValue(mockGroup as any);
|
||||
jest.spyOn(groupMemberRepository, 'findOne').mockResolvedValue({
|
||||
...mockGroupMember,
|
||||
role: GroupMemberRole.MEMBER,
|
||||
} as any);
|
||||
|
||||
await expect(service.create('user-1', createDto)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it('组长应该可以创建荣誉记录', async () => {
|
||||
const createDto = {
|
||||
groupId: 'group-1',
|
||||
title: '冠军荣誉',
|
||||
eventDate: new Date('2025-01-01'),
|
||||
};
|
||||
|
||||
jest.spyOn(groupRepository, 'findOne').mockResolvedValue(mockGroup as any);
|
||||
jest.spyOn(groupMemberRepository, 'findOne').mockResolvedValue({
|
||||
...mockGroupMember,
|
||||
role: GroupMemberRole.OWNER,
|
||||
} as any);
|
||||
jest.spyOn(honorRepository, 'create').mockReturnValue(mockHonor as any);
|
||||
jest.spyOn(honorRepository, 'save').mockResolvedValue(mockHonor as any);
|
||||
jest.spyOn(honorRepository, 'findOne').mockResolvedValue(mockHonor as any);
|
||||
|
||||
const result = await service.create('user-1', createDto);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('findAll', () => {
|
||||
it('应该返回荣誉列表', async () => {
|
||||
mockQueryBuilder.getMany.mockResolvedValue([mockHonor]);
|
||||
|
||||
const result = await service.findAll({ groupId: 'group-1' });
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(honorRepository.createQueryBuilder).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTimeline', () => {
|
||||
it('应该返回按年份分组的时间轴', async () => {
|
||||
const mockHonors = [
|
||||
{ ...mockHonor, eventDate: new Date('2025-01-01') },
|
||||
{ ...mockHonor, id: 'honor-2', eventDate: new Date('2024-06-01') },
|
||||
];
|
||||
|
||||
jest.spyOn(honorRepository, 'find').mockResolvedValue(mockHonors as any);
|
||||
|
||||
const result = await service.getTimeline('group-1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result[2025]).toHaveLength(1);
|
||||
expect(result[2024]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('空荣誉列表应该返回空对象', async () => {
|
||||
jest.spyOn(honorRepository, 'find').mockResolvedValue([]);
|
||||
|
||||
const result = await service.getTimeline('group-1');
|
||||
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('findOne', () => {
|
||||
it('应该返回单个荣誉记录', async () => {
|
||||
jest.spyOn(honorRepository, 'findOne').mockResolvedValue(mockHonor as any);
|
||||
|
||||
const result = await service.findOne('honor-1');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.id).toBe('honor-1');
|
||||
});
|
||||
|
||||
it('记录不存在时应该抛出异常', async () => {
|
||||
jest.spyOn(honorRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(service.findOne('non-existent')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
it('创建者应该可以更新荣誉记录', async () => {
|
||||
const updateDto = {
|
||||
title: '更新后的标题',
|
||||
};
|
||||
|
||||
jest.spyOn(honorRepository, 'findOne').mockResolvedValue(mockHonor as any);
|
||||
jest.spyOn(groupRepository, 'findOne').mockResolvedValue(mockGroup as any);
|
||||
jest.spyOn(groupMemberRepository, 'findOne').mockResolvedValue(mockGroupMember as any);
|
||||
jest.spyOn(honorRepository, 'save').mockResolvedValue({
|
||||
...mockHonor,
|
||||
...updateDto,
|
||||
} as any);
|
||||
|
||||
const result = await service.update('user-1', 'honor-1', updateDto);
|
||||
|
||||
expect(result.title).toBe('更新后的标题');
|
||||
});
|
||||
|
||||
it('管理员应该可以更新任何荣誉记录', async () => {
|
||||
const updateDto = {
|
||||
title: '更新后的标题',
|
||||
};
|
||||
|
||||
jest.spyOn(honorRepository, 'findOne').mockResolvedValue({
|
||||
...mockHonor,
|
||||
createdBy: 'other-user',
|
||||
} as any);
|
||||
jest.spyOn(groupRepository, 'findOne').mockResolvedValue(mockGroup as any);
|
||||
jest.spyOn(groupMemberRepository, 'findOne').mockResolvedValue(mockGroupMember as any);
|
||||
jest.spyOn(honorRepository, 'save').mockResolvedValue({
|
||||
...mockHonor,
|
||||
...updateDto,
|
||||
} as any);
|
||||
|
||||
const result = await service.update('user-1', 'honor-1', updateDto);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('无权限时应该抛出异常', async () => {
|
||||
const updateDto = {
|
||||
title: '更新后的标题',
|
||||
};
|
||||
|
||||
jest.spyOn(honorRepository, 'findOne').mockResolvedValue({
|
||||
...mockHonor,
|
||||
createdBy: 'other-user',
|
||||
} as any);
|
||||
jest.spyOn(groupRepository, 'findOne').mockResolvedValue(mockGroup as any);
|
||||
jest.spyOn(groupMemberRepository, 'findOne').mockResolvedValue({
|
||||
...mockGroupMember,
|
||||
role: GroupMemberRole.MEMBER,
|
||||
} as any);
|
||||
|
||||
await expect(service.update('user-1', 'honor-1', updateDto)).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('创建者应该可以删除自己的荣誉记录', async () => {
|
||||
jest.spyOn(honorRepository, 'findOne').mockResolvedValue(mockHonor as any);
|
||||
jest.spyOn(groupRepository, 'findOne').mockResolvedValue(mockGroup as any);
|
||||
jest.spyOn(groupMemberRepository, 'findOne').mockResolvedValue(mockGroupMember as any);
|
||||
jest.spyOn(honorRepository, 'remove').mockResolvedValue(mockHonor as any);
|
||||
|
||||
const result = await service.remove('user-1', 'honor-1');
|
||||
|
||||
expect(result.message).toBe('删除成功');
|
||||
expect(honorRepository.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('管理员应该可以删除任何荣誉记录', async () => {
|
||||
jest.spyOn(honorRepository, 'findOne').mockResolvedValue({
|
||||
...mockHonor,
|
||||
createdBy: 'other-user',
|
||||
} as any);
|
||||
jest.spyOn(groupRepository, 'findOne').mockResolvedValue(mockGroup as any);
|
||||
jest.spyOn(groupMemberRepository, 'findOne').mockResolvedValue(mockGroupMember as any);
|
||||
jest.spyOn(honorRepository, 'remove').mockResolvedValue(mockHonor as any);
|
||||
|
||||
const result = await service.remove('user-1', 'honor-1');
|
||||
|
||||
expect(result.message).toBe('删除成功');
|
||||
});
|
||||
|
||||
it('无权限时应该抛出异常', async () => {
|
||||
jest.spyOn(honorRepository, 'findOne').mockResolvedValue({
|
||||
...mockHonor,
|
||||
createdBy: 'other-user',
|
||||
} as any);
|
||||
jest.spyOn(groupRepository, 'findOne').mockResolvedValue(mockGroup as any);
|
||||
jest.spyOn(groupMemberRepository, 'findOne').mockResolvedValue({
|
||||
...mockGroupMember,
|
||||
role: GroupMemberRole.MEMBER,
|
||||
} as any);
|
||||
|
||||
await expect(service.remove('user-1', 'honor-1')).rejects.toThrow(ForbiddenException);
|
||||
});
|
||||
});
|
||||
});
|
||||
198
src/modules/honors/honors.service.ts
Normal file
198
src/modules/honors/honors.service.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository, Between } from 'typeorm';
|
||||
import { Honor } from '../../entities/honor.entity';
|
||||
import { Group } from '../../entities/group.entity';
|
||||
import { GroupMember } from '../../entities/group-member.entity';
|
||||
import { CreateHonorDto, UpdateHonorDto, QueryHonorsDto } from './dto/honor.dto';
|
||||
import { GroupMemberRole } from '../../common/enums';
|
||||
import {
|
||||
ErrorCode,
|
||||
ErrorMessage,
|
||||
} from '../../common/interfaces/response.interface';
|
||||
|
||||
@Injectable()
|
||||
export class HonorsService {
|
||||
constructor(
|
||||
@InjectRepository(Honor)
|
||||
private honorRepository: Repository<Honor>,
|
||||
@InjectRepository(Group)
|
||||
private groupRepository: Repository<Group>,
|
||||
@InjectRepository(GroupMember)
|
||||
private groupMemberRepository: Repository<GroupMember>,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建荣誉记录
|
||||
*/
|
||||
async create(userId: string, createDto: CreateHonorDto) {
|
||||
const { groupId, ...rest } = createDto;
|
||||
|
||||
// 验证小组存在
|
||||
const group = await this.groupRepository.findOne({ where: { id: groupId } });
|
||||
if (!group) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.GROUP_NOT_FOUND,
|
||||
message: ErrorMessage[ErrorCode.GROUP_NOT_FOUND],
|
||||
});
|
||||
}
|
||||
|
||||
// 验证用户权限(需要是小组管理员或组长)
|
||||
const membership = await this.groupMemberRepository.findOne({
|
||||
where: { groupId, userId },
|
||||
});
|
||||
|
||||
if (
|
||||
!membership ||
|
||||
(membership.role !== GroupMemberRole.ADMIN &&
|
||||
membership.role !== GroupMemberRole.OWNER)
|
||||
) {
|
||||
throw new ForbiddenException({
|
||||
code: ErrorCode.NO_PERMISSION,
|
||||
message: '需要管理员权限',
|
||||
});
|
||||
}
|
||||
|
||||
const honor = this.honorRepository.create({
|
||||
...rest,
|
||||
groupId,
|
||||
creatorId: userId,
|
||||
});
|
||||
|
||||
await this.honorRepository.save(honor);
|
||||
|
||||
return this.findOne(honor.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询荣誉列表
|
||||
*/
|
||||
async findAll(query: QueryHonorsDto) {
|
||||
const qb = this.honorRepository
|
||||
.createQueryBuilder('honor')
|
||||
.leftJoinAndSelect('honor.group', 'group')
|
||||
.leftJoinAndSelect('honor.creator', 'creator');
|
||||
|
||||
if (query.groupId) {
|
||||
qb.andWhere('honor.groupId = :groupId', { groupId: query.groupId });
|
||||
}
|
||||
|
||||
if (query.year) {
|
||||
const startDate = new Date(`${query.year}-01-01`);
|
||||
const endDate = new Date(`${query.year}-12-31`);
|
||||
qb.andWhere('honor.eventDate BETWEEN :startDate AND :endDate', {
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
}
|
||||
|
||||
qb.orderBy('honor.eventDate', 'DESC');
|
||||
|
||||
const honors = await qb.getMany();
|
||||
|
||||
return honors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时间轴数据(按年份分组)
|
||||
*/
|
||||
async getTimeline(groupId: string) {
|
||||
const honors = await this.honorRepository.find({
|
||||
where: { groupId },
|
||||
relations: ['creator'],
|
||||
order: { eventDate: 'DESC' },
|
||||
});
|
||||
|
||||
// 按年份分组
|
||||
const timeline = honors.reduce((acc, honor) => {
|
||||
const year = new Date(honor.eventDate).getFullYear();
|
||||
if (!acc[year]) {
|
||||
acc[year] = [];
|
||||
}
|
||||
acc[year].push(honor);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return timeline;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询单个荣誉记录
|
||||
*/
|
||||
async findOne(id: string) {
|
||||
const honor = await this.honorRepository.findOne({
|
||||
where: { id },
|
||||
relations: ['group', 'creator'],
|
||||
});
|
||||
|
||||
if (!honor) {
|
||||
throw new NotFoundException({
|
||||
code: ErrorCode.HONOR_NOT_FOUND,
|
||||
message: '荣誉记录不存在',
|
||||
});
|
||||
}
|
||||
|
||||
return honor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新荣誉记录
|
||||
*/
|
||||
async update(userId: string, id: string, updateDto: UpdateHonorDto) {
|
||||
const honor = await this.findOne(id);
|
||||
|
||||
// 验证权限
|
||||
const membership = await this.groupMemberRepository.findOne({
|
||||
where: { groupId: honor.groupId, userId },
|
||||
});
|
||||
|
||||
if (
|
||||
honor.creatorId !== userId &&
|
||||
(!membership ||
|
||||
(membership.role !== GroupMemberRole.ADMIN &&
|
||||
membership.role !== GroupMemberRole.OWNER))
|
||||
) {
|
||||
throw new ForbiddenException({
|
||||
code: ErrorCode.NO_PERMISSION,
|
||||
message: ErrorMessage[ErrorCode.NO_PERMISSION],
|
||||
});
|
||||
}
|
||||
|
||||
Object.assign(honor, updateDto);
|
||||
await this.honorRepository.save(honor);
|
||||
|
||||
return this.findOne(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除荣誉记录
|
||||
*/
|
||||
async remove(userId: string, id: string) {
|
||||
const honor = await this.findOne(id);
|
||||
|
||||
// 验证权限
|
||||
const membership = await this.groupMemberRepository.findOne({
|
||||
where: { groupId: honor.groupId, userId },
|
||||
});
|
||||
|
||||
if (
|
||||
honor.creatorId !== userId &&
|
||||
(!membership ||
|
||||
(membership.role !== GroupMemberRole.ADMIN &&
|
||||
membership.role !== GroupMemberRole.OWNER))
|
||||
) {
|
||||
throw new ForbiddenException({
|
||||
code: ErrorCode.NO_PERMISSION,
|
||||
message: ErrorMessage[ErrorCode.NO_PERMISSION],
|
||||
});
|
||||
}
|
||||
|
||||
await this.honorRepository.remove(honor);
|
||||
|
||||
return { message: '删除成功' };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user