新增C端患者me接口并补充文档说明
This commit is contained in:
parent
ab17204739
commit
8f7e13bf2b
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 1. 目标
|
## 1. 目标
|
||||||
|
|
||||||
- 提供系统管理员创建、院内账号密码登录、B 端小程序手机号登录、C 端小程序手机号登录、`/me` 身份查询。
|
- 提供系统管理员创建、院内账号密码登录、B 端小程序手机号登录、C 端小程序手机号登录、身份查询。
|
||||||
- 使用 JWT 做认证,院内账号与 C 端小程序账号走两套守卫。
|
- 使用 JWT 做认证,院内账号与 C 端小程序账号走两套守卫。
|
||||||
|
|
||||||
## 2. 核心接口
|
## 2. 核心接口
|
||||||
@ -14,6 +14,7 @@
|
|||||||
- `POST /auth/miniapp/b/phone-login/confirm`:B 端同手机号多账号确认登录
|
- `POST /auth/miniapp/b/phone-login/confirm`:B 端同手机号多账号确认登录
|
||||||
- `POST /auth/miniapp/c/phone-login`:C 端小程序手机号登录
|
- `POST /auth/miniapp/c/phone-login`:C 端小程序手机号登录
|
||||||
- `GET /auth/me`:返回当前院内登录用户上下文
|
- `GET /auth/me`:返回当前院内登录用户上下文
|
||||||
|
- `GET /c/patients/me`:返回当前 C 端登录账号绑定的患者基础信息
|
||||||
|
|
||||||
## 3. 院内账号密码登录流程
|
## 3. 院内账号密码登录流程
|
||||||
|
|
||||||
@ -72,4 +73,5 @@
|
|||||||
- 若目标院内账号已绑定其他微信号,仍需先在用户管理中清空该账号的 `openId` 再重新绑定。
|
- 若目标院内账号已绑定其他微信号,仍需先在用户管理中清空该账号的 `openId` 再重新绑定。
|
||||||
- C 端账号独立存放在 `FamilyMiniAppAccount`。
|
- C 端账号独立存放在 `FamilyMiniAppAccount`。
|
||||||
- C 端手机号必须唯一命中患者档案,否则拒绝登录。
|
- C 端手机号必须唯一命中患者档案,否则拒绝登录。
|
||||||
|
- C 端登录后读取“我的信息”请调用 `GET /c/patients/me`,不要复用 `GET /auth/me`。
|
||||||
- `serviceUid` 仅预留字段,本次不提供绑定接口。
|
- `serviceUid` 仅预留字段,本次不提供绑定接口。
|
||||||
|
|||||||
@ -34,6 +34,8 @@
|
|||||||
|
|
||||||
## 2. C 端生命周期
|
## 2. C 端生命周期
|
||||||
|
|
||||||
|
- 登录成功后可先调用:`GET /c/patients/me`
|
||||||
|
- 返回当前 C 端账号信息与当前手机号唯一命中的患者基础档案
|
||||||
- 登录成功后调用:`GET /c/patients/my-lifecycle`
|
- 登录成功后调用:`GET /c/patients/my-lifecycle`
|
||||||
- 不再需要传 `phone` 或 `idCard`
|
- 不再需要传 `phone` 或 `idCard`
|
||||||
- Bearer Token 使用 C 端患者登录返回的 `accessToken`
|
- Bearer Token 使用 C 端患者登录返回的 `accessToken`
|
||||||
|
|||||||
@ -14,16 +14,18 @@
|
|||||||
## 3. C 端能力
|
## 3. C 端能力
|
||||||
|
|
||||||
- 患者本人通过小程序手机号登录
|
- 患者本人通过小程序手机号登录
|
||||||
|
- `GET /c/patients/me`
|
||||||
- `GET /c/patients/my-lifecycle`
|
- `GET /c/patients/my-lifecycle`
|
||||||
- 查询口径:按 `FamilyMiniAppAccount.phone` 唯一命中 `Patient.phone`
|
- 查询口径:按 `FamilyMiniAppAccount.phone` 唯一命中 `Patient.phone`
|
||||||
- 返回内容:顶层患者信息 + 手术事件/调压事件时间线
|
- `me` 返回内容:当前 C 端账号信息 + 当前手机号命中的患者基础档案
|
||||||
|
- `my-lifecycle` 返回内容:顶层患者信息 + 手术事件/调压事件时间线
|
||||||
|
|
||||||
## 4. 当前规则
|
## 4. 当前规则
|
||||||
|
|
||||||
- 同一个手机号在 C 端只允许命中 1 份患者档案。
|
- 同一个手机号在 C 端只允许命中 1 份患者档案。
|
||||||
- 若同一个手机号命中多份患者档案,登录阶段直接返回冲突错误。
|
- 若同一个手机号命中多份患者档案,登录阶段直接返回冲突错误。
|
||||||
- C 端手机号来源于患者手术/档案中维护的联系电话。
|
- C 端手机号来源于患者手术/档案中维护的联系电话。
|
||||||
- 仅已登录的 C 端小程序账号可访问 `my-lifecycle`。
|
- 仅已登录的 C 端小程序账号可访问 `me` 与 `my-lifecycle`。
|
||||||
- C 端登录账号不存在或 token 无效时返回 `401`。
|
- C 端登录账号不存在或 token 无效时返回 `401`。
|
||||||
- 手机号下无患者档案时,登录阶段直接拦截,不进入生命周期查询。
|
- 手机号下无患者档案时,登录阶段直接拦截,不进入生命周期查询。
|
||||||
|
|
||||||
@ -32,4 +34,5 @@
|
|||||||
- `GET /b/patients`
|
- `GET /b/patients`
|
||||||
- `POST /b/patients`
|
- `POST /b/patients`
|
||||||
- `POST /b/patients/:id/surgeries`
|
- `POST /b/patients/:id/surgeries`
|
||||||
|
- `GET /c/patients/me`
|
||||||
- `GET /c/patients/my-lifecycle`
|
- `GET /c/patients/my-lifecycle`
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import {
|
|||||||
import { CurrentFamilyActor } from '../../auth/current-family-actor.decorator.js';
|
import { CurrentFamilyActor } from '../../auth/current-family-actor.decorator.js';
|
||||||
import { FamilyAccessTokenGuard } from '../../auth/family-access/family-access.guard.js';
|
import { FamilyAccessTokenGuard } from '../../auth/family-access/family-access.guard.js';
|
||||||
import type { FamilyActorContext } from '../../common/family-actor-context.js';
|
import type { FamilyActorContext } from '../../common/family-actor-context.js';
|
||||||
|
import { CPatientMeResponseDto } from '../dto/c-patient-me-response.dto.js';
|
||||||
import {
|
import {
|
||||||
CPatientLifecycleResponseDto,
|
CPatientLifecycleResponseDto,
|
||||||
PATIENT_LIFECYCLE_SWAGGER_MODELS,
|
PATIENT_LIFECYCLE_SWAGGER_MODELS,
|
||||||
@ -16,7 +17,7 @@ import {
|
|||||||
import { CPatientsService } from './c-patients.service.js';
|
import { CPatientsService } from './c-patients.service.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* C 端患者控制器:患者本人按登录手机号查询自己的生命周期。
|
* C 端患者控制器:患者本人按登录手机号查询自己的档案与生命周期。
|
||||||
*/
|
*/
|
||||||
@ApiTags('患者管理(C端)')
|
@ApiTags('患者管理(C端)')
|
||||||
@ApiBearerAuth('bearer')
|
@ApiBearerAuth('bearer')
|
||||||
@ -25,6 +26,19 @@ import { CPatientsService } from './c-patients.service.js';
|
|||||||
export class CPatientsController {
|
export class CPatientsController {
|
||||||
constructor(private readonly patientsService: CPatientsService) {}
|
constructor(private readonly patientsService: CPatientsService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取当前登录患者的基础信息。
|
||||||
|
*/
|
||||||
|
@Get('me')
|
||||||
|
@ApiOperation({ summary: '获取当前登录患者信息' })
|
||||||
|
@ApiOkResponse({
|
||||||
|
description: '返回当前 C 端登录账号绑定的患者档案与账号信息',
|
||||||
|
type: CPatientMeResponseDto,
|
||||||
|
})
|
||||||
|
me(@CurrentFamilyActor() actor: FamilyActorContext) {
|
||||||
|
return this.patientsService.getMeByAccount(actor.id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据当前登录手机号查询单患者生命周期。
|
* 根据当前登录手机号查询单患者生命周期。
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -10,28 +10,76 @@ import {
|
|||||||
PATIENT_LIFECYCLE_INCLUDE,
|
PATIENT_LIFECYCLE_INCLUDE,
|
||||||
} from '../patient-lifecycle.util.js';
|
} 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()
|
@Injectable()
|
||||||
export class CPatientsService {
|
export class CPatientsService {
|
||||||
constructor(private readonly prisma: PrismaService) {}
|
constructor(private readonly prisma: PrismaService) {}
|
||||||
|
|
||||||
async getFamilyLifecycleByAccount(accountId: number) {
|
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({
|
const account = await this.prisma.familyMiniAppAccount.findUnique({
|
||||||
where: { id: accountId },
|
where: { id: accountId },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
phone: true,
|
phone: true,
|
||||||
|
openId: true,
|
||||||
|
serviceUid: true,
|
||||||
|
lastLoginAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
throw new NotFoundException(MESSAGES.AUTH.FAMILY_ACCOUNT_NOT_FOUND);
|
throw new NotFoundException(MESSAGES.AUTH.FAMILY_ACCOUNT_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return account;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findUniqueLifecyclePatientByPhone(phone: string) {
|
||||||
const patients = await this.prisma.patient.findMany({
|
const patients = await this.prisma.patient.findMany({
|
||||||
where: {
|
where: { phone },
|
||||||
phone: account.phone,
|
|
||||||
},
|
|
||||||
include: PATIENT_LIFECYCLE_INCLUDE,
|
include: PATIENT_LIFECYCLE_INCLUDE,
|
||||||
|
take: 2,
|
||||||
|
orderBy: { id: 'asc' },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (patients.length === 0) {
|
if (patients.length === 0) {
|
||||||
throw new NotFoundException(MESSAGES.PATIENT.LIFE_CYCLE_NOT_FOUND);
|
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;
|
const [patient] = patients;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
phone: account.phone,
|
id: patient.id,
|
||||||
...buildPatientLifecyclePayload(patient),
|
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,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
126
src/patients/dto/c-patient-me-response.dto.ts
Normal file
126
src/patients/dto/c-patient-me-response.dto.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user