Files
gamegroup/src/modules/groups/groups.service.ts
UGREEN USER b25aa5b143 初始化游戏小组管理系统后端项目
- 基于 NestJS + TypeScript + MySQL + Redis 架构
- 完整的模块化设计(认证、用户、小组、游戏、预约等)
- JWT 认证和 RBAC 权限控制系统
- Docker 容器化部署支持
- 添加 CLAUDE.md 项目开发指南
- 配置 .gitignore 忽略文件

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-28 10:42:06 +08:00

442 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
Injectable,
NotFoundException,
BadRequestException,
ForbiddenException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Group } from '../../entities/group.entity';
import { GroupMember } from '../../entities/group-member.entity';
import { User } from '../../entities/user.entity';
import { CreateGroupDto, UpdateGroupDto, JoinGroupDto } from './dto/group.dto';
import { GroupMemberRole } from '../../common/enums';
import { ErrorCode, ErrorMessage } from '../../common/interfaces/response.interface';
import { CacheService } from '../../common/services/cache.service';
@Injectable()
export class GroupsService {
private readonly CACHE_PREFIX = 'group';
private readonly CACHE_TTL = 300; // 5 minutes
constructor(
@InjectRepository(Group)
private groupRepository: Repository<Group>,
@InjectRepository(GroupMember)
private groupMemberRepository: Repository<GroupMember>,
@InjectRepository(User)
private userRepository: Repository<User>,
private cacheService: CacheService,
) {}
/**
* 创建小组
*/
async create(userId: string, createGroupDto: CreateGroupDto) {
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
throw new NotFoundException({
code: ErrorCode.USER_NOT_FOUND,
message: ErrorMessage[ErrorCode.USER_NOT_FOUND],
});
}
// 检查用户创建的小组数量
const ownedGroupsCount = await this.groupRepository.count({
where: { ownerId: userId },
});
if (!user.isMember && ownedGroupsCount >= 1) {
throw new BadRequestException({
code: ErrorCode.GROUP_LIMIT_EXCEEDED,
message: '非会员最多只能创建1个小组',
});
}
if (user.isMember && ownedGroupsCount >= 10) {
throw new BadRequestException({
code: ErrorCode.GROUP_LIMIT_EXCEEDED,
message: '会员最多只能创建10个小组',
});
}
// 如果是创建子组,检查父组是否存在且用户是否为会员
if (createGroupDto.parentId) {
if (!user.isMember) {
throw new ForbiddenException({
code: ErrorCode.NO_PERMISSION,
message: '非会员不能创建子组',
});
}
const parentGroup = await this.groupRepository.findOne({
where: { id: createGroupDto.parentId },
});
if (!parentGroup) {
throw new NotFoundException({
code: ErrorCode.GROUP_NOT_FOUND,
message: '父组不存在',
});
}
}
// 创建小组
const group = this.groupRepository.create({
...createGroupDto,
ownerId: userId,
maxMembers: createGroupDto.maxMembers || 50,
});
await this.groupRepository.save(group);
// 将创建者添加为小组成员(角色为 owner
const member = this.groupMemberRepository.create({
groupId: group.id,
userId: userId,
role: GroupMemberRole.OWNER,
});
await this.groupMemberRepository.save(member);
return this.findOne(group.id);
}
/**
* 加入小组(使用原子更新防止并发竞态条件)
*/
async join(userId: string, joinGroupDto: JoinGroupDto) {
const { groupId, nickname } = joinGroupDto;
const user = await this.userRepository.findOne({ where: { id: userId } });
if (!user) {
throw new NotFoundException({
code: ErrorCode.USER_NOT_FOUND,
message: ErrorMessage[ErrorCode.USER_NOT_FOUND],
});
}
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 existingMember = await this.groupMemberRepository.findOne({
where: { groupId, userId },
});
if (existingMember) {
throw new BadRequestException({
code: ErrorCode.ALREADY_IN_GROUP,
message: ErrorMessage[ErrorCode.ALREADY_IN_GROUP],
});
}
// 检查用户加入的小组数量
const joinedGroupsCount = await this.groupMemberRepository.count({
where: { userId },
});
if (!user.isMember && joinedGroupsCount >= 3) {
throw new BadRequestException({
code: ErrorCode.JOIN_GROUP_LIMIT_EXCEEDED,
message: ErrorMessage[ErrorCode.JOIN_GROUP_LIMIT_EXCEEDED],
});
}
// 使用原子更新:只有当当前成员数小于最大成员数时才成功
const updateResult = await this.groupRepository
.createQueryBuilder()
.update(Group)
.set({
currentMembers: () => 'currentMembers + 1',
})
.where('id = :id', { id: groupId })
.andWhere('currentMembers < maxMembers')
.execute();
// 如果影响的行数为0说明小组已满
if (updateResult.affected === 0) {
throw new BadRequestException({
code: ErrorCode.GROUP_FULL,
message: ErrorMessage[ErrorCode.GROUP_FULL],
});
}
// 添加成员记录
const member = this.groupMemberRepository.create({
groupId,
userId,
nickname,
role: GroupMemberRole.MEMBER,
});
await this.groupMemberRepository.save(member);
return this.findOne(groupId);
}
/**
* 退出小组
*/
async leave(userId: string, groupId: string) {
const member = await this.groupMemberRepository.findOne({
where: { groupId, userId },
});
if (!member) {
throw new NotFoundException({
code: ErrorCode.NOT_IN_GROUP,
message: ErrorMessage[ErrorCode.NOT_IN_GROUP],
});
}
// 组长不能直接退出
if (member.role === GroupMemberRole.OWNER) {
throw new BadRequestException({
code: ErrorCode.NO_PERMISSION,
message: '组长不能退出小组,请先转让组长或解散小组',
});
}
await this.groupMemberRepository.remove(member);
// 更新小组成员数
const group = await this.groupRepository.findOne({ where: { id: groupId } });
if (group) {
group.currentMembers = Math.max(0, group.currentMembers - 1);
await this.groupRepository.save(group);
}
return { message: '退出成功' };
}
/**
* 获取小组详情
*/
async findOne(id: string) {
// 尝试从缓存获取
const cached = this.cacheService.get<any>(id, {
prefix: this.CACHE_PREFIX,
});
if (cached) {
return cached;
}
const group = await this.groupRepository.findOne({
where: { id },
relations: ['owner', 'members', 'members.user'],
});
if (!group) {
throw new NotFoundException({
code: ErrorCode.GROUP_NOT_FOUND,
message: ErrorMessage[ErrorCode.GROUP_NOT_FOUND],
});
}
const result = {
...group,
members: group.members.map((member) => ({
id: member.id,
userId: member.userId,
username: member.user.username,
avatar: member.user.avatar,
nickname: member.nickname,
role: member.role,
joinedAt: member.joinedAt,
})),
};
// 缓存结果
this.cacheService.set(id, result, {
prefix: this.CACHE_PREFIX,
ttl: this.CACHE_TTL,
});
return result;
}
/**
* 获取用户的小组列表
*/
async findUserGroups(userId: string) {
const members = await this.groupMemberRepository.find({
where: { userId },
relations: ['group', 'group.owner'],
});
return members.map((member) => ({
...member.group,
myRole: member.role,
myNickname: member.nickname,
}));
}
/**
* 更新小组信息
*/
async update(userId: string, groupId: string, updateGroupDto: UpdateGroupDto) {
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],
});
}
// 检查权限(只有组长和管理员可以修改)
await this.checkPermission(userId, groupId, [
GroupMemberRole.OWNER,
GroupMemberRole.ADMIN,
]);
Object.assign(group, updateGroupDto);
await this.groupRepository.save(group);
// 清除缓存
this.cacheService.del(groupId, { prefix: this.CACHE_PREFIX });
return this.findOne(groupId);
}
/**
* 设置成员角色
*/
async updateMemberRole(
userId: string,
groupId: string,
targetUserId: string,
role: GroupMemberRole,
) {
// 只有组长可以设置管理员
await this.checkPermission(userId, groupId, [GroupMemberRole.OWNER]);
const member = await this.groupMemberRepository.findOne({
where: { groupId, userId: targetUserId },
});
if (!member) {
throw new NotFoundException({
code: ErrorCode.NOT_IN_GROUP,
message: '该用户不在小组中',
});
}
// 不能修改组长角色
if (member.role === GroupMemberRole.OWNER) {
throw new BadRequestException({
code: ErrorCode.NO_PERMISSION,
message: '不能修改组长角色',
});
}
member.role = role;
await this.groupMemberRepository.save(member);
return { message: '角色设置成功' };
}
/**
* 踢出成员
*/
async kickMember(userId: string, groupId: string, targetUserId: string) {
// 组长和管理员可以踢人
await this.checkPermission(userId, groupId, [
GroupMemberRole.OWNER,
GroupMemberRole.ADMIN,
]);
const member = await this.groupMemberRepository.findOne({
where: { groupId, userId: targetUserId },
});
if (!member) {
throw new NotFoundException({
code: ErrorCode.NOT_IN_GROUP,
message: '该用户不在小组中',
});
}
// 不能踢出组长
if (member.role === GroupMemberRole.OWNER) {
throw new BadRequestException({
code: ErrorCode.NO_PERMISSION,
message: '不能踢出组长',
});
}
await this.groupMemberRepository.remove(member);
// 更新小组成员数
const group = await this.groupRepository.findOne({ where: { id: groupId } });
if (group) {
group.currentMembers = Math.max(0, group.currentMembers - 1);
await this.groupRepository.save(group);
}
return { message: '成员已移除' };
}
/**
* 解散小组
*/
async disband(userId: string, groupId: string) {
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],
});
}
// 只有组长可以解散
if (group.ownerId !== userId) {
throw new ForbiddenException({
code: ErrorCode.NO_PERMISSION,
message: '只有组长可以解散小组',
});
}
group.isActive = false;
await this.groupRepository.save(group);
return { message: '小组已解散' };
}
/**
* 检查权限
*/
private async checkPermission(
userId: string,
groupId: string,
allowedRoles: GroupMemberRole[],
) {
const member = await this.groupMemberRepository.findOne({
where: { groupId, userId },
});
if (!member) {
throw new ForbiddenException({
code: ErrorCode.NOT_IN_GROUP,
message: ErrorMessage[ErrorCode.NOT_IN_GROUP],
});
}
if (!allowedRoles.includes(member.role)) {
throw new ForbiddenException({
code: ErrorCode.NO_PERMISSION,
message: ErrorMessage[ErrorCode.NO_PERMISSION],
});
}
}
}