From 8f7e13bf2bf8c67e7fa80d4dc243901c3e2a8b6a Mon Sep 17 00:00:00 2001 From: EL <1175065040@qq.com> Date: Thu, 2 Apr 2026 05:19:52 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9EC=E7=AB=AF=E6=82=A3=E8=80=85m?= =?UTF-8?q?e=E6=8E=A5=E5=8F=A3=E5=B9=B6=E8=A1=A5=E5=85=85=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/auth.md | 4 +- docs/frontend-api-integration.md | 2 + docs/patients.md | 7 +- .../c-patients/c-patients.controller.ts | 16 ++- src/patients/c-patients/c-patients.service.ts | 93 ++++++++++++- src/patients/dto/c-patient-me-response.dto.ts | 126 ++++++++++++++++++ 6 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 src/patients/dto/c-patient-me-response.dto.ts diff --git a/docs/auth.md b/docs/auth.md index a17f720..9047506 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -2,7 +2,7 @@ ## 1. 目标 -- 提供系统管理员创建、院内账号密码登录、B 端小程序手机号登录、C 端小程序手机号登录、`/me` 身份查询。 +- 提供系统管理员创建、院内账号密码登录、B 端小程序手机号登录、C 端小程序手机号登录、身份查询。 - 使用 JWT 做认证,院内账号与 C 端小程序账号走两套守卫。 ## 2. 核心接口 @@ -14,6 +14,7 @@ - `POST /auth/miniapp/b/phone-login/confirm`:B 端同手机号多账号确认登录 - `POST /auth/miniapp/c/phone-login`:C 端小程序手机号登录 - `GET /auth/me`:返回当前院内登录用户上下文 +- `GET /c/patients/me`:返回当前 C 端登录账号绑定的患者基础信息 ## 3. 院内账号密码登录流程 @@ -72,4 +73,5 @@ - 若目标院内账号已绑定其他微信号,仍需先在用户管理中清空该账号的 `openId` 再重新绑定。 - C 端账号独立存放在 `FamilyMiniAppAccount`。 - C 端手机号必须唯一命中患者档案,否则拒绝登录。 +- C 端登录后读取“我的信息”请调用 `GET /c/patients/me`,不要复用 `GET /auth/me`。 - `serviceUid` 仅预留字段,本次不提供绑定接口。 diff --git a/docs/frontend-api-integration.md b/docs/frontend-api-integration.md index b77ebb5..da8c1c5 100644 --- a/docs/frontend-api-integration.md +++ b/docs/frontend-api-integration.md @@ -34,6 +34,8 @@ ## 2. C 端生命周期 +- 登录成功后可先调用:`GET /c/patients/me` +- 返回当前 C 端账号信息与当前手机号唯一命中的患者基础档案 - 登录成功后调用:`GET /c/patients/my-lifecycle` - 不再需要传 `phone` 或 `idCard` - Bearer Token 使用 C 端患者登录返回的 `accessToken` diff --git a/docs/patients.md b/docs/patients.md index ba13499..96ff8cc 100644 --- a/docs/patients.md +++ b/docs/patients.md @@ -14,16 +14,18 @@ ## 3. C 端能力 - 患者本人通过小程序手机号登录 +- `GET /c/patients/me` - `GET /c/patients/my-lifecycle` - 查询口径:按 `FamilyMiniAppAccount.phone` 唯一命中 `Patient.phone` -- 返回内容:顶层患者信息 + 手术事件/调压事件时间线 +- `me` 返回内容:当前 C 端账号信息 + 当前手机号命中的患者基础档案 +- `my-lifecycle` 返回内容:顶层患者信息 + 手术事件/调压事件时间线 ## 4. 当前规则 - 同一个手机号在 C 端只允许命中 1 份患者档案。 - 若同一个手机号命中多份患者档案,登录阶段直接返回冲突错误。 - C 端手机号来源于患者手术/档案中维护的联系电话。 -- 仅已登录的 C 端小程序账号可访问 `my-lifecycle`。 +- 仅已登录的 C 端小程序账号可访问 `me` 与 `my-lifecycle`。 - C 端登录账号不存在或 token 无效时返回 `401`。 - 手机号下无患者档案时,登录阶段直接拦截,不进入生命周期查询。 @@ -32,4 +34,5 @@ - `GET /b/patients` - `POST /b/patients` - `POST /b/patients/:id/surgeries` +- `GET /c/patients/me` - `GET /c/patients/my-lifecycle` diff --git a/src/patients/c-patients/c-patients.controller.ts b/src/patients/c-patients/c-patients.controller.ts index 0596cf8..380eeff 100644 --- a/src/patients/c-patients/c-patients.controller.ts +++ b/src/patients/c-patients/c-patients.controller.ts @@ -9,6 +9,7 @@ import { import { CurrentFamilyActor } from '../../auth/current-family-actor.decorator.js'; import { FamilyAccessTokenGuard } from '../../auth/family-access/family-access.guard.js'; import type { FamilyActorContext } from '../../common/family-actor-context.js'; +import { CPatientMeResponseDto } from '../dto/c-patient-me-response.dto.js'; import { CPatientLifecycleResponseDto, PATIENT_LIFECYCLE_SWAGGER_MODELS, @@ -16,7 +17,7 @@ import { import { CPatientsService } from './c-patients.service.js'; /** - * C 端患者控制器:患者本人按登录手机号查询自己的生命周期。 + * C 端患者控制器:患者本人按登录手机号查询自己的档案与生命周期。 */ @ApiTags('患者管理(C端)') @ApiBearerAuth('bearer') @@ -25,6 +26,19 @@ import { CPatientsService } from './c-patients.service.js'; export class CPatientsController { constructor(private readonly patientsService: CPatientsService) {} + /** + * 读取当前登录患者的基础信息。 + */ + @Get('me') + @ApiOperation({ summary: '获取当前登录患者信息' }) + @ApiOkResponse({ + description: '返回当前 C 端登录账号绑定的患者档案与账号信息', + type: CPatientMeResponseDto, + }) + me(@CurrentFamilyActor() actor: FamilyActorContext) { + return this.patientsService.getMeByAccount(actor.id); + } + /** * 根据当前登录手机号查询单患者生命周期。 */ diff --git a/src/patients/c-patients/c-patients.service.ts b/src/patients/c-patients/c-patients.service.ts index 9c3eeae..d11116d 100644 --- a/src/patients/c-patients/c-patients.service.ts +++ b/src/patients/c-patients/c-patients.service.ts @@ -10,28 +10,76 @@ import { PATIENT_LIFECYCLE_INCLUDE, } from '../patient-lifecycle.util.js'; +const C_PATIENT_ME_INCLUDE = { + hospital: { + select: { + id: true, + name: true, + }, + }, + doctor: { + select: { + id: true, + name: true, + role: true, + }, + }, +} as const; + @Injectable() export class CPatientsService { constructor(private readonly prisma: PrismaService) {} async getFamilyLifecycleByAccount(accountId: number) { + const account = await this.getFamilyAccountById(accountId); + const patient = await this.findUniqueLifecyclePatientByPhone(account.phone); + + return { + phone: account.phone, + ...buildPatientLifecyclePayload(patient), + }; + } + + /** + * 返回当前 C 端登录账号绑定的患者基础信息。 + */ + async getMeByAccount(accountId: number) { + const account = await this.getFamilyAccountById(accountId); + const patient = await this.findUniqueMePatientByPhone(account.phone); + + return { + familyAccount: account, + patient, + }; + } + + private async getFamilyAccountById(accountId: number) { const account = await this.prisma.familyMiniAppAccount.findUnique({ where: { id: accountId }, select: { id: true, phone: true, + openId: true, + serviceUid: true, + lastLoginAt: true, }, }); + if (!account) { throw new NotFoundException(MESSAGES.AUTH.FAMILY_ACCOUNT_NOT_FOUND); } + return account; + } + + private async findUniqueLifecyclePatientByPhone(phone: string) { const patients = await this.prisma.patient.findMany({ - where: { - phone: account.phone, - }, + where: { phone }, include: PATIENT_LIFECYCLE_INCLUDE, + take: 2, + orderBy: { id: 'asc' }, }); + if (patients.length === 0) { throw new NotFoundException(MESSAGES.PATIENT.LIFE_CYCLE_NOT_FOUND); } @@ -41,11 +89,46 @@ export class CPatientsService { ); } + return patients[0]; + } + + private async findUniqueMePatientByPhone(phone: string) { + const patients = await this.prisma.patient.findMany({ + where: { phone }, + include: C_PATIENT_ME_INCLUDE, + take: 2, + orderBy: { id: 'asc' }, + }); + + if (patients.length === 0) { + throw new NotFoundException( + MESSAGES.AUTH.FAMILY_PHONE_NOT_LINKED_PATIENT, + ); + } + if (patients.length > 1) { + throw new ConflictException( + MESSAGES.AUTH.FAMILY_PHONE_LINKED_MULTI_PATIENTS, + ); + } + const [patient] = patients; return { - phone: account.phone, - ...buildPatientLifecyclePayload(patient), + id: patient.id, + name: patient.name, + phone: patient.phone, + idCard: patient.idCard, + inpatientNo: patient.inpatientNo, + projectName: patient.projectName, + hospitalId: patient.hospitalId, + doctorId: patient.doctorId, + createdAt: patient.createdAt, + hospital: patient.hospital, + doctor: { + id: patient.doctor.id, + name: patient.doctor.name, + role: patient.doctor.role, + }, }; } } diff --git a/src/patients/dto/c-patient-me-response.dto.ts b/src/patients/dto/c-patient-me-response.dto.ts new file mode 100644 index 0000000..1c09971 --- /dev/null +++ b/src/patients/dto/c-patient-me-response.dto.ts @@ -0,0 +1,126 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Role } from '../../generated/prisma/enums.js'; + +export class CPatientMeFamilyAccountDto { + @ApiProperty({ description: 'C 端账号 ID', example: 1 }) + id!: number; + + @ApiProperty({ description: '当前登录手机号', example: '13800002002' }) + phone!: string; + + @ApiProperty({ + description: '微信 openId', + example: 'oAbcDefGhIjKlMn', + nullable: true, + type: String, + }) + openId!: string | null; + + @ApiProperty({ + description: '客服体系用户标识', + example: null, + nullable: true, + type: String, + }) + serviceUid!: string | null; + + @ApiProperty({ + description: '最近登录时间', + example: '2026-04-02T08:00:00.000Z', + format: 'date-time', + }) + lastLoginAt!: string; +} + +export class CPatientMeHospitalDto { + @ApiProperty({ description: '医院 ID', example: 1 }) + id!: number; + + @ApiProperty({ description: '医院名称', example: '珠江医院' }) + name!: string; +} + +export class CPatientMeDoctorDto { + @ApiProperty({ description: '归属医生 ID', example: 10001 }) + id!: number; + + @ApiProperty({ description: '归属医生姓名', example: '张医生' }) + name!: string; + + @ApiProperty({ description: '归属医生角色', enum: Role }) + role!: Role; +} + +export class CPatientMePatientDto { + @ApiProperty({ description: '患者 ID', example: 8 }) + id!: number; + + @ApiProperty({ description: '患者姓名', example: '张三' }) + name!: string; + + @ApiProperty({ description: '患者手机号', example: '13800002002' }) + phone!: string; + + @ApiProperty({ description: '患者身份证号', example: '440102199901010011' }) + idCard!: string; + + @ApiProperty({ + description: '住院号', + example: 'ZY20260402001', + nullable: true, + type: String, + }) + inpatientNo!: string | null; + + @ApiProperty({ + description: '项目名称', + example: '脑积水随访项目', + nullable: true, + type: String, + }) + projectName!: string | null; + + @ApiProperty({ description: '所属医院 ID', example: 1 }) + hospitalId!: number; + + @ApiProperty({ description: '归属医生 ID', example: 10001 }) + doctorId!: number; + + @ApiProperty({ + description: '建档时间', + example: '2026-04-02T08:00:00.000Z', + format: 'date-time', + }) + createdAt!: string; + + @ApiProperty({ description: '所属医院', type: CPatientMeHospitalDto }) + hospital!: CPatientMeHospitalDto; + + @ApiProperty({ description: '归属医生', type: CPatientMeDoctorDto }) + doctor!: CPatientMeDoctorDto; +} + +export class CPatientMeDataDto { + @ApiProperty({ + description: '当前登录的 C 端账号信息', + type: CPatientMeFamilyAccountDto, + }) + familyAccount!: CPatientMeFamilyAccountDto; + + @ApiProperty({ + description: '当前手机号唯一命中的患者档案', + type: CPatientMePatientDto, + }) + patient!: CPatientMePatientDto; +} + +export class CPatientMeResponseDto { + @ApiProperty({ description: '业务状态码', example: 0 }) + code!: number; + + @ApiProperty({ description: '响应消息', example: '成功' }) + msg!: string; + + @ApiProperty({ description: '当前登录患者信息', type: CPatientMeDataDto }) + data!: CPatientMeDataDto; +}