feat(health-profiles): 添加健康档案模块,支持健康史记录、家庭健康管理和档案概览功能

This commit is contained in:
richarjiang
2025-12-04 17:15:11 +08:00
parent 03bd0b041e
commit 2d7e067888
20 changed files with 1949 additions and 0 deletions

View 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(),
};
}
}