feat(health-profiles): 添加健康档案模块,支持健康史记录、家庭健康管理和档案概览功能
This commit is contained in:
386
src/health-profiles/services/family-health.service.ts
Normal file
386
src/health-profiles/services/family-health.service.ts
Normal file
@@ -0,0 +1,386 @@
|
||||
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 }> {
|
||||
const membership = await this.familyMemberModel.findOne({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new NotFoundException('您还没有家庭组');
|
||||
}
|
||||
|
||||
// 只有 owner 和 admin 可以生成邀请码
|
||||
if (membership.role === FamilyRole.MEMBER) {
|
||||
throw new ForbiddenException('只有管理员可以生成邀请码');
|
||||
}
|
||||
|
||||
const familyGroup = await this.familyGroupModel.findByPk(membership.familyGroupId);
|
||||
if (!familyGroup) {
|
||||
throw new NotFoundException('家庭组不存在');
|
||||
}
|
||||
|
||||
// 生成邀请码
|
||||
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}`, // 可以根据实际需求生成二维码 URL
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 加入家庭组
|
||||
*/
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user