Files
gamegroup/src/modules/groups/groups.service.ts
UGREEN USER 575a29ac8f chore: 代码风格统一和项目文档添加
主要变更:

1. 代码风格统一
   - 统一使用双引号替代单引号
   - 保持项目代码风格一致性
   - 涵盖所有模块、配置、实体和服务文件

2. 项目文档
   - 新增 SECURITY_FIXES_SUMMARY.md - 安全修复总结文档
   - 新增 项目问题评估报告.md - 项目问题评估文档

3. 包含修改的文件类别
   - 配置文件:app, database, jwt, redis, cache, performance
   - 实体文件:所有 TypeORM 实体
   - 模块文件:所有业务模块
   - 公共模块:guards, decorators, interceptors, filters, utils
   - 测试文件:单元测试和 E2E 测试

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

459 lines
12 KiB
TypeScript
Raw Permalink 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],
});
}
}
}