405 lines
12 KiB
TypeScript
405 lines
12 KiB
TypeScript
import { Injectable, Logger, NotFoundException, ForbiddenException, BadRequestException } from '@nestjs/common';
|
|
import { InjectModel, InjectConnection } from '@nestjs/sequelize';
|
|
import { Sequelize } from 'sequelize-typescript';
|
|
import { Op } from 'sequelize';
|
|
import { v4 as uuidv4 } from 'uuid';
|
|
import * as dayjs from 'dayjs';
|
|
import { FamilyGroup } from '../models/family-group.model';
|
|
import { FamilyMember } from '../models/family-member.model';
|
|
import { User } from '../../users/models/user.model';
|
|
import { FamilyRole } from '../enums/health-profile.enum';
|
|
import {
|
|
CreateFamilyGroupDto,
|
|
GenerateInviteCodeDto,
|
|
JoinFamilyGroupDto,
|
|
UpdateFamilyMemberDto,
|
|
FamilyGroupResponseDto,
|
|
FamilyMemberResponseDto,
|
|
} from '../dto/family-health.dto';
|
|
|
|
@Injectable()
|
|
export class FamilyHealthService {
|
|
private readonly logger = new Logger(FamilyHealthService.name);
|
|
|
|
constructor(
|
|
@InjectModel(FamilyGroup)
|
|
private readonly familyGroupModel: typeof FamilyGroup,
|
|
@InjectModel(FamilyMember)
|
|
private readonly familyMemberModel: typeof FamilyMember,
|
|
@InjectModel(User)
|
|
private readonly userModel: typeof User,
|
|
@InjectConnection()
|
|
private readonly sequelize: Sequelize,
|
|
) {}
|
|
|
|
/**
|
|
* 获取用户的家庭组
|
|
*/
|
|
async getFamilyGroup(userId: string): Promise<FamilyGroupResponseDto | null> {
|
|
// 先查找用户所属的家庭成员记录
|
|
const membership = await this.familyMemberModel.findOne({
|
|
where: { userId },
|
|
});
|
|
|
|
if (!membership) {
|
|
return null;
|
|
}
|
|
|
|
const familyGroup = await this.familyGroupModel.findOne({
|
|
where: { id: membership.familyGroupId },
|
|
include: [
|
|
{
|
|
model: FamilyMember,
|
|
as: 'members',
|
|
include: [{ model: User, as: 'user' }],
|
|
},
|
|
],
|
|
});
|
|
|
|
if (!familyGroup) {
|
|
return null;
|
|
}
|
|
|
|
return this.mapGroupToResponse(familyGroup);
|
|
}
|
|
|
|
/**
|
|
* 创建家庭组
|
|
*/
|
|
async createFamilyGroup(userId: string, createDto: CreateFamilyGroupDto): Promise<FamilyGroupResponseDto> {
|
|
// 检查用户是否已经有家庭组
|
|
const existingMembership = await this.familyMemberModel.findOne({
|
|
where: { userId },
|
|
});
|
|
|
|
if (existingMembership) {
|
|
throw new BadRequestException('您已经是一个家庭组的成员,请先退出当前家庭组');
|
|
}
|
|
|
|
const transaction = await this.sequelize.transaction();
|
|
|
|
try {
|
|
// 创建家庭组
|
|
const familyGroup = await this.familyGroupModel.create(
|
|
{
|
|
id: uuidv4(),
|
|
ownerId: userId,
|
|
name: createDto.name || '我的家庭',
|
|
},
|
|
{ transaction },
|
|
);
|
|
|
|
// 将创建者添加为 owner 成员
|
|
await this.familyMemberModel.create(
|
|
{
|
|
id: uuidv4(),
|
|
familyGroupId: familyGroup.id,
|
|
userId,
|
|
role: FamilyRole.OWNER,
|
|
canViewHealthData: true,
|
|
canManageHealthData: true,
|
|
receiveAlerts: true,
|
|
},
|
|
{ transaction },
|
|
);
|
|
|
|
await transaction.commit();
|
|
|
|
this.logger.log(`用户 ${userId} 创建家庭组 ${familyGroup.id} 成功`);
|
|
|
|
// 重新查询以获取完整数据
|
|
return this.getFamilyGroup(userId) as Promise<FamilyGroupResponseDto>;
|
|
} catch (error) {
|
|
await transaction.rollback();
|
|
this.logger.error(`创建家庭组失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取或生成邀请码
|
|
* 如果用户没有家庭组,自动创建一个
|
|
* 如果已有有效邀请码,直接返回
|
|
*/
|
|
async generateInviteCode(
|
|
userId: string,
|
|
dto: GenerateInviteCodeDto,
|
|
): Promise<{ familyGroupId: string; inviteCode: string; expiresAt: string; qrCodeUrl: string }> {
|
|
let membership = await this.familyMemberModel.findOne({
|
|
where: { userId },
|
|
});
|
|
|
|
// 如果用户没有家庭组,自动创建一个
|
|
if (!membership) {
|
|
this.logger.log(`用户 ${userId} 没有家庭组,自动创建`);
|
|
await this.createFamilyGroup(userId, { name: '我的家庭' });
|
|
membership = await this.familyMemberModel.findOne({
|
|
where: { userId },
|
|
});
|
|
}
|
|
|
|
// 只有 owner 和 admin 可以生成邀请码
|
|
if (membership!.role === FamilyRole.MEMBER) {
|
|
throw new ForbiddenException('只有管理员可以生成邀请码');
|
|
}
|
|
|
|
const familyGroup = await this.familyGroupModel.findByPk(membership!.familyGroupId);
|
|
if (!familyGroup) {
|
|
throw new NotFoundException('家庭组不存在');
|
|
}
|
|
|
|
// 如果已有有效邀请码,直接返回
|
|
if (familyGroup.inviteCode && familyGroup.inviteCodeExpiresAt && dayjs(familyGroup.inviteCodeExpiresAt).isAfter(dayjs())) {
|
|
this.logger.log(`用户 ${userId} 获取家庭组 ${familyGroup.id} 的现有邀请码 ${familyGroup.inviteCode}`);
|
|
return {
|
|
familyGroupId: familyGroup.id,
|
|
inviteCode: familyGroup.inviteCode,
|
|
expiresAt: familyGroup.inviteCodeExpiresAt.toISOString(),
|
|
qrCodeUrl: `outlive://family/join?code=${familyGroup.inviteCode}`,
|
|
};
|
|
}
|
|
|
|
// 生成新邀请码
|
|
const inviteCode = this.generateUniqueInviteCode();
|
|
const expiresAt = dayjs().add(dto.expiresInHours || 24, 'hour').toDate();
|
|
|
|
familyGroup.inviteCode = inviteCode;
|
|
familyGroup.inviteCodeExpiresAt = expiresAt;
|
|
await familyGroup.save();
|
|
|
|
this.logger.log(`用户 ${userId} 为家庭组 ${familyGroup.id} 生成邀请码 ${inviteCode}`);
|
|
|
|
return {
|
|
familyGroupId: familyGroup.id,
|
|
inviteCode,
|
|
expiresAt: expiresAt.toISOString(),
|
|
qrCodeUrl: `outlive://family/join?code=${inviteCode}`,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 加入家庭组
|
|
*/
|
|
async joinFamilyGroup(userId: string, dto: JoinFamilyGroupDto): Promise<FamilyGroupResponseDto> {
|
|
// 检查用户是否已经有家庭组
|
|
const existingMembership = await this.familyMemberModel.findOne({
|
|
where: { userId },
|
|
});
|
|
|
|
if (existingMembership) {
|
|
throw new BadRequestException('您已经是一个家庭组的成员,请先退出当前家庭组');
|
|
}
|
|
|
|
// 查找邀请码对应的家庭组
|
|
const familyGroup = await this.familyGroupModel.findOne({
|
|
where: {
|
|
inviteCode: dto.inviteCode,
|
|
inviteCodeExpiresAt: { [Op.gt]: new Date() },
|
|
},
|
|
});
|
|
|
|
if (!familyGroup) {
|
|
throw new BadRequestException('邀请码无效或已过期');
|
|
}
|
|
|
|
// 检查成员数量
|
|
const memberCount = await this.familyMemberModel.count({
|
|
where: { familyGroupId: familyGroup.id },
|
|
});
|
|
|
|
if (memberCount >= familyGroup.maxMembers) {
|
|
throw new BadRequestException('家庭组已满员');
|
|
}
|
|
|
|
// 添加成员
|
|
await this.familyMemberModel.create({
|
|
id: uuidv4(),
|
|
familyGroupId: familyGroup.id,
|
|
userId,
|
|
role: FamilyRole.MEMBER,
|
|
canViewHealthData: true,
|
|
canManageHealthData: false,
|
|
receiveAlerts: true,
|
|
});
|
|
|
|
this.logger.log(`用户 ${userId} 加入家庭组 ${familyGroup.id}`);
|
|
|
|
return this.getFamilyGroup(userId) as Promise<FamilyGroupResponseDto>;
|
|
}
|
|
|
|
/**
|
|
* 获取家庭成员列表
|
|
*/
|
|
async getFamilyMembers(userId: string): Promise<FamilyMemberResponseDto[]> {
|
|
const membership = await this.familyMemberModel.findOne({
|
|
where: { userId },
|
|
});
|
|
|
|
if (!membership) {
|
|
throw new NotFoundException('您还没有家庭组');
|
|
}
|
|
|
|
const members = await this.familyMemberModel.findAll({
|
|
where: { familyGroupId: membership.familyGroupId },
|
|
include: [{ model: User, as: 'user' }],
|
|
});
|
|
|
|
return members.map(this.mapMemberToResponse);
|
|
}
|
|
|
|
/**
|
|
* 更新成员权限
|
|
*/
|
|
async updateFamilyMember(
|
|
userId: string,
|
|
memberId: string,
|
|
updateDto: UpdateFamilyMemberDto,
|
|
): Promise<FamilyMemberResponseDto> {
|
|
const currentMembership = await this.familyMemberModel.findOne({
|
|
where: { userId },
|
|
});
|
|
|
|
if (!currentMembership) {
|
|
throw new NotFoundException('您还没有家庭组');
|
|
}
|
|
|
|
// 只有 owner 和 admin 可以修改成员权限
|
|
if (currentMembership.role === FamilyRole.MEMBER) {
|
|
throw new ForbiddenException('只有管理员可以修改成员权限');
|
|
}
|
|
|
|
const targetMember = await this.familyMemberModel.findOne({
|
|
where: { id: memberId, familyGroupId: currentMembership.familyGroupId },
|
|
include: [{ model: User, as: 'user' }],
|
|
});
|
|
|
|
if (!targetMember) {
|
|
throw new NotFoundException('成员不存在');
|
|
}
|
|
|
|
// 不能修改 owner 的权限
|
|
if (targetMember.role === FamilyRole.OWNER && currentMembership.role !== FamilyRole.OWNER) {
|
|
throw new ForbiddenException('不能修改创建者的权限');
|
|
}
|
|
|
|
// 更新权限
|
|
if (updateDto.canViewHealthData !== undefined) targetMember.canViewHealthData = updateDto.canViewHealthData;
|
|
if (updateDto.canManageHealthData !== undefined) targetMember.canManageHealthData = updateDto.canManageHealthData;
|
|
if (updateDto.receiveAlerts !== undefined) targetMember.receiveAlerts = updateDto.receiveAlerts;
|
|
if (updateDto.relationship !== undefined) targetMember.relationship = updateDto.relationship;
|
|
|
|
await targetMember.save();
|
|
|
|
this.logger.log(`用户 ${userId} 更新成员 ${memberId} 的权限`);
|
|
|
|
return this.mapMemberToResponse(targetMember);
|
|
}
|
|
|
|
/**
|
|
* 移除家庭成员
|
|
*/
|
|
async removeFamilyMember(userId: string, memberId: string): Promise<void> {
|
|
const currentMembership = await this.familyMemberModel.findOne({
|
|
where: { userId },
|
|
});
|
|
|
|
if (!currentMembership) {
|
|
throw new NotFoundException('您还没有家庭组');
|
|
}
|
|
|
|
const targetMember = await this.familyMemberModel.findOne({
|
|
where: { id: memberId, familyGroupId: currentMembership.familyGroupId },
|
|
});
|
|
|
|
if (!targetMember) {
|
|
throw new NotFoundException('成员不存在');
|
|
}
|
|
|
|
// 不能移除 owner
|
|
if (targetMember.role === FamilyRole.OWNER) {
|
|
throw new ForbiddenException('不能移除创建者');
|
|
}
|
|
|
|
// 只有 owner 和 admin 可以移除成员,或者成员自己退出
|
|
if (
|
|
currentMembership.role === FamilyRole.MEMBER &&
|
|
currentMembership.id !== memberId
|
|
) {
|
|
throw new ForbiddenException('只有管理员可以移除成员');
|
|
}
|
|
|
|
await targetMember.destroy();
|
|
|
|
this.logger.log(`成员 ${memberId} 已从家庭组移除`);
|
|
}
|
|
|
|
/**
|
|
* 退出家庭组
|
|
*/
|
|
async leaveFamilyGroup(userId: string): Promise<void> {
|
|
const membership = await this.familyMemberModel.findOne({
|
|
where: { userId },
|
|
});
|
|
|
|
if (!membership) {
|
|
throw new NotFoundException('您还没有家庭组');
|
|
}
|
|
|
|
// owner 不能直接退出,需要先转让或解散
|
|
if (membership.role === FamilyRole.OWNER) {
|
|
throw new BadRequestException('创建者不能直接退出,请先转让管理权或解散家庭组');
|
|
}
|
|
|
|
await membership.destroy();
|
|
|
|
this.logger.log(`用户 ${userId} 退出家庭组`);
|
|
}
|
|
|
|
/**
|
|
* 生成唯一邀请码
|
|
*/
|
|
private generateUniqueInviteCode(): string {
|
|
const chars = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
|
|
let code = '';
|
|
for (let i = 0; i < 6; i++) {
|
|
code += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
}
|
|
return code;
|
|
}
|
|
|
|
/**
|
|
* 映射家庭组到响应 DTO
|
|
*/
|
|
private mapGroupToResponse(group: FamilyGroup): FamilyGroupResponseDto {
|
|
return {
|
|
id: group.id,
|
|
ownerId: group.ownerId,
|
|
name: group.name,
|
|
inviteCode: group.inviteCode || undefined,
|
|
inviteCodeExpiresAt: group.inviteCodeExpiresAt?.toISOString() || undefined,
|
|
maxMembers: group.maxMembers,
|
|
members: group.members?.map(this.mapMemberToResponse) || [],
|
|
createdAt: group.createdAt.toISOString(),
|
|
updatedAt: group.updatedAt.toISOString(),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 映射成员到响应 DTO
|
|
*/
|
|
private mapMemberToResponse(member: FamilyMember): FamilyMemberResponseDto {
|
|
return {
|
|
id: member.id,
|
|
userId: member.userId,
|
|
userName: member.user?.name || '未知用户',
|
|
userAvatar: member.user?.avatar || undefined,
|
|
role: member.role,
|
|
relationship: member.relationship || undefined,
|
|
canViewHealthData: member.canViewHealthData,
|
|
canManageHealthData: member.canManageHealthData,
|
|
receiveAlerts: member.receiveAlerts,
|
|
joinedAt: member.joinedAt.toISOString(),
|
|
};
|
|
}
|
|
}
|