diff --git a/docs/auth.md b/docs/auth.md index 21099a7..a5361cf 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -21,6 +21,7 @@ 2. 若仅匹配到 1 个院内账号,后端直接返回 JWT。 3. 若匹配到多个院内账号,后端返回 `loginTicket + accounts` 候选列表。 4. 前端带 `loginTicket + userId` 调用确认接口获取最终 JWT。 + ## 4. 微信小程序登录流程 ### B 端 @@ -67,7 +68,8 @@ - 密码登录命中多个院内账号时,不再强制前端手填 `hospitalId`,改为后端返回候选账号供选择。 - B 端小程序登录复用 `User` 表,继续使用 `openId`。 - B 端账号未绑定 `openId` 时,首次小程序登录自动绑定。 -- 同一个 `openId` 不能绑定多个院内账号。 +- 同一个 `openId` 可绑定多个院内账号,支持同一微信号切换多角色/多院区账号。 +- 若目标院内账号已绑定其他微信号,仍需先在用户管理中清空该账号的 `openId` 再重新绑定。 - C 端家属账号独立存放在 `FamilyMiniAppAccount`。 - C 端手机号必须先存在于患者档案,否则拒绝登录。 - `serviceUid` 仅预留字段,本次不提供绑定接口。 diff --git a/docs/frontend-api-integration.md b/docs/frontend-api-integration.md index 761357d..46616eb 100644 --- a/docs/frontend-api-integration.md +++ b/docs/frontend-api-integration.md @@ -43,3 +43,4 @@ - 后台管理端与小程序都可以复用 `POST /auth/login` 做账号密码登录 - `GET /auth/me` 仍可读取当前院内账号信息 - 同手机号多账号时,前端必须先让用户选定账号,再提交确认登录 +- 同一个微信号可绑定多个院内账号,切换账号时继续走“小程序登录 -> 候选账号选择”即可 diff --git a/prisma/migrations/20260320160000_allow_multi_user_openid/migration.sql b/prisma/migrations/20260320160000_allow_multi_user_openid/migration.sql new file mode 100644 index 0000000..2ebdb53 --- /dev/null +++ b/prisma/migrations/20260320160000_allow_multi_user_openid/migration.sql @@ -0,0 +1,4 @@ +-- 允许同一个微信 openId 绑定多个院内账号,保留普通索引供查询复用。 +DROP INDEX IF EXISTS "User_openId_key"; + +CREATE INDEX IF NOT EXISTS "User_openId_idx" ON "User"("openId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e47c01d..4c69d4c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -55,7 +55,7 @@ enum UploadAssetType { // 医院主表:多租户顶层实体。 model Hospital { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) name String departments Department[] users User[] @@ -88,6 +88,7 @@ model Group { } // 用户表:支持后台密码登录与小程序 openId。 +// 同一个微信 openId 允许绑定多个院内账号,便于多角色/多院区切换。 model User { id Int @id @default(autoincrement()) name String @@ -96,7 +97,7 @@ model User { passwordHash String? // 该时间点之前签发的 token 一律失效。 tokenValidAfter DateTime @default(now()) - openId String? @unique + openId String? role Role hospitalId Int? departmentId Int? @@ -114,6 +115,7 @@ model User { @@unique([phone, role, hospitalId]) @@index([phone]) + @@index([openId]) @@index([hospitalId, role]) @@index([departmentId, role]) @@index([groupId, role]) @@ -274,18 +276,18 @@ model Device { // 主任务表:记录调压任务主单。 model Task { - id Int @id @default(autoincrement()) - status TaskStatus @default(PENDING) - creatorId Int - engineerId Int? - hospitalId Int - createdAt DateTime @default(now()) + id Int @id @default(autoincrement()) + status TaskStatus @default(PENDING) + creatorId Int + engineerId Int? + hospitalId Int + createdAt DateTime @default(now()) // 工程师完成任务时上传的图片/视频凭证。 completionMaterials Json? - creator User @relation("TaskCreator", fields: [creatorId], references: [id]) - engineer User? @relation("TaskEngineer", fields: [engineerId], references: [id]) - hospital Hospital @relation(fields: [hospitalId], references: [id]) - items TaskItem[] + creator User @relation("TaskCreator", fields: [creatorId], references: [id]) + engineer User? @relation("TaskEngineer", fields: [engineerId], references: [id]) + hospital Hospital @relation(fields: [hospitalId], references: [id]) + items TaskItem[] @@index([hospitalId, status, createdAt]) } diff --git a/prisma/seed.mjs b/prisma/seed.mjs index f280600..5c5b3dc 100644 --- a/prisma/seed.mjs +++ b/prisma/seed.mjs @@ -46,18 +46,21 @@ async function ensureGroup(departmentId, name) { ); } -async function upsertUserByOpenId(openId, data) { +async function upsertUserByScope(data) { return prisma.user.upsert({ - where: { openId }, + where: { + phone_role_hospitalId: { + phone: data.phone, + role: data.role, + hospitalId: data.hospitalId, + }, + }, // 每次重置/补种子时推进失效时间,确保历史 token 无法继续访问。 update: { ...data, tokenValidAfter: new Date(), }, - create: { - ...data, - openId, - }, + create: data, }); } @@ -320,113 +323,121 @@ async function main() { const groupA2 = await ensureGroup(departmentA2.id, 'Shift-A2'); const groupB1 = await ensureGroup(departmentB1.id, 'Shift-B1'); - const systemAdmin = await upsertUserByOpenId('seed-system-admin-openid', { + const systemAdmin = await upsertUserByScope({ name: 'Seed System Admin', phone: '13800001000', passwordHash: seedPasswordHash, + openId: 'seed-system-admin-openid', role: Role.SYSTEM_ADMIN, hospitalId: null, departmentId: null, groupId: null, }); - const hospitalAdminA = await upsertUserByOpenId( - 'seed-hospital-admin-a-openid', - { - name: 'Seed Hospital Admin A', - phone: '13800001001', - passwordHash: seedPasswordHash, - role: Role.HOSPITAL_ADMIN, - hospitalId: hospitalA.id, - departmentId: null, - groupId: null, - }, - ); + const hospitalAdminA = await upsertUserByScope({ + name: 'Seed Hospital Admin A', + phone: '13800001001', + passwordHash: seedPasswordHash, + openId: 'seed-hospital-admin-a-openid', + role: Role.HOSPITAL_ADMIN, + hospitalId: hospitalA.id, + departmentId: null, + groupId: null, + }); - await upsertUserByOpenId('seed-hospital-admin-b-openid', { + await upsertUserByScope({ name: 'Seed Hospital Admin B', phone: '13800001101', passwordHash: seedPasswordHash, + openId: 'seed-hospital-admin-b-openid', role: Role.HOSPITAL_ADMIN, hospitalId: hospitalB.id, departmentId: null, groupId: null, }); - const directorA = await upsertUserByOpenId('seed-director-a-openid', { + const directorA = await upsertUserByScope({ name: 'Seed Director A', phone: '13800001002', passwordHash: seedPasswordHash, + openId: 'seed-director-a-openid', role: Role.DIRECTOR, hospitalId: hospitalA.id, departmentId: departmentA1.id, groupId: null, }); - const leaderA = await upsertUserByOpenId('seed-leader-a-openid', { + const leaderA = await upsertUserByScope({ name: 'Seed Leader A', phone: '13800001003', passwordHash: seedPasswordHash, + openId: 'seed-leader-a-openid', role: Role.LEADER, hospitalId: hospitalA.id, departmentId: departmentA1.id, groupId: groupA1.id, }); - const doctorA = await upsertUserByOpenId('seed-doctor-a-openid', { + const doctorA = await upsertUserByScope({ name: 'Seed Doctor A', phone: '13800001004', passwordHash: seedPasswordHash, + openId: 'seed-doctor-a-openid', role: Role.DOCTOR, hospitalId: hospitalA.id, departmentId: departmentA1.id, groupId: groupA1.id, }); - const doctorA2 = await upsertUserByOpenId('seed-doctor-a2-openid', { + const doctorA2 = await upsertUserByScope({ name: 'Seed Doctor A2', phone: '13800001204', passwordHash: seedPasswordHash, + openId: 'seed-doctor-a2-openid', role: Role.DOCTOR, hospitalId: hospitalA.id, departmentId: departmentA1.id, groupId: groupA1.id, }); - const doctorA3 = await upsertUserByOpenId('seed-doctor-a3-openid', { + const doctorA3 = await upsertUserByScope({ name: 'Seed Doctor A3', phone: '13800001304', passwordHash: seedPasswordHash, + openId: 'seed-doctor-a3-openid', role: Role.DOCTOR, hospitalId: hospitalA.id, departmentId: departmentA2.id, groupId: groupA2.id, }); - const doctorB = await upsertUserByOpenId('seed-doctor-b-openid', { + const doctorB = await upsertUserByScope({ name: 'Seed Doctor B', phone: '13800001104', passwordHash: seedPasswordHash, + openId: 'seed-doctor-b-openid', role: Role.DOCTOR, hospitalId: hospitalB.id, departmentId: departmentB1.id, groupId: groupB1.id, }); - const engineerA = await upsertUserByOpenId('seed-engineer-a-openid', { + const engineerA = await upsertUserByScope({ name: 'Seed Engineer A', phone: '13800001005', passwordHash: seedPasswordHash, + openId: 'seed-engineer-a-openid', role: Role.ENGINEER, hospitalId: hospitalA.id, departmentId: null, groupId: null, }); - const engineerB = await upsertUserByOpenId('seed-engineer-b-openid', { + const engineerB = await upsertUserByScope({ name: 'Seed Engineer B', phone: '13800001105', passwordHash: seedPasswordHash, + openId: 'seed-engineer-b-openid', role: Role.ENGINEER, hospitalId: hospitalB.id, departmentId: null, diff --git a/src/auth/dto/create-system-admin.dto.ts b/src/auth/dto/create-system-admin.dto.ts index 75b75a9..2feda19 100644 --- a/src/auth/dto/create-system-admin.dto.ts +++ b/src/auth/dto/create-system-admin.dto.ts @@ -15,7 +15,7 @@ export class CreateSystemAdminDto { password!: string; @ApiPropertyOptional({ - description: '可选微信 openId', + description: '可选微信 openId(院内账号间可复用)', example: 'o123abcxyz', }) @IsOptional() diff --git a/src/common/messages.ts b/src/common/messages.ts index 793d649..8c13a53 100644 --- a/src/common/messages.ts +++ b/src/common/messages.ts @@ -36,7 +36,7 @@ export const MESSAGES = { MINIAPP_NO_MATCHED_USER: '手机号未匹配到院内账号', MINIAPP_LOGIN_TICKET_INVALID: '账号选择票据无效或已过期', MINIAPP_ACCOUNT_SELECTION_INVALID: '请选择有效的候选账号', - MINIAPP_OPEN_ID_BOUND_OTHER_USER: '当前微信账号已绑定其他院内账号', + MINIAPP_OPEN_ID_BOUND_OTHER_USER: '当前院内账号已绑定其他微信账号', MINIAPP_OPEN_ID_BOUND_OTHER_FAMILY: '当前微信账号已绑定其他家属账号', FAMILY_PHONE_NOT_LINKED_PATIENT: '当前手机号未关联患者档案', FAMILY_ACCOUNT_NOT_FOUND: '家属登录账号不存在,请重新登录', @@ -44,7 +44,6 @@ export const MESSAGES = { USER: { NOT_FOUND: '用户不存在', - DUPLICATE_OPEN_ID: 'openId 已被注册', DUPLICATE_PHONE_ROLE_SCOPE: '同医院下该角色手机号已存在', INVALID_ROLE: '角色不合法', INVALID_PHONE: '手机号格式不合法', diff --git a/src/patients/b-patients/b-patients.service.ts b/src/patients/b-patients/b-patients.service.ts index 10f7ad4..669080a 100644 --- a/src/patients/b-patients/b-patients.service.ts +++ b/src/patients/b-patients/b-patients.service.ts @@ -40,6 +40,7 @@ const PATIENT_LIST_INCLUDE = { id: true, status: true, currentPressure: true, + initialPressure: true, isAbandoned: true, implantModel: true, implantManufacturer: true, @@ -54,6 +55,8 @@ const PATIENT_LIST_INCLUDE = { id: true, surgeryDate: true, surgeryName: true, + primaryDisease: true, + hydrocephalusTypes: true, surgeonId: true, surgeonName: true, }, @@ -156,10 +159,25 @@ export class BPatientsService { return patients.map((patient) => { const { _count, surgeries, ...rest } = patient; + const latestSurgery = surgeries[0] ?? null; + const currentDevice = + patient.devices.find( + (device) => + device.status === DeviceStatus.ACTIVE && !device.isAbandoned, + ) ?? + patient.devices.find((device) => !device.isAbandoned) ?? + patient.devices[0] ?? + null; + return { ...rest, + primaryDisease: latestSurgery?.primaryDisease ?? null, + hydrocephalusTypes: latestSurgery?.hydrocephalusTypes ?? [], + surgeryDate: latestSurgery?.surgeryDate ?? null, + currentPressure: currentDevice?.currentPressure ?? null, + initialPressure: currentDevice?.initialPressure ?? null, shuntSurgeryCount: _count.surgeries, - latestSurgery: surgeries[0] ?? null, + latestSurgery, activeDeviceCount: patient.devices.filter( (device) => device.status === DeviceStatus.ACTIVE && !device.isAbandoned, diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index 87cd83e..f76d630 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -32,7 +32,7 @@ export class CreateUserDto { password?: string; @ApiPropertyOptional({ - description: '微信 openId', + description: '微信 openId(院内账号间可复用)', example: 'wx-open-id-demo', }) @IsOptional() diff --git a/src/users/dto/register-user.dto.ts b/src/users/dto/register-user.dto.ts index 62c5c1b..b7a6bf5 100644 --- a/src/users/dto/register-user.dto.ts +++ b/src/users/dto/register-user.dto.ts @@ -35,7 +35,7 @@ export class RegisterUserDto { role!: Role; @ApiPropertyOptional({ - description: '微信 openId(可选)', + description: '微信 openId(可选,院内账号间可复用)', example: 'wx-open-id-demo', }) @IsOptional() diff --git a/src/users/users.service.spec.ts b/src/users/users.service.spec.ts new file mode 100644 index 0000000..2066af0 --- /dev/null +++ b/src/users/users.service.spec.ts @@ -0,0 +1,66 @@ +import { ConflictException, NotFoundException } from '@nestjs/common'; +import { jest } from '@jest/globals'; +import { UsersService } from './users.service.js'; +import type { PrismaService } from '../prisma.service.js'; + +describe('UsersService.bindOpenIdForMiniAppLogin', () => { + function createService() { + const prisma = { + user: { + findUnique: jest.fn(), + update: jest.fn(), + }, + } as unknown as PrismaService; + + return { + prisma, + service: new UsersService(prisma), + }; + } + + it('允许同一个 openId 绑定多个院内账号', async () => { + const { prisma, service } = createService(); + const sharedOpenId = 'shared-openid'; + + (prisma.user.findUnique as jest.Mock) + .mockResolvedValueOnce({ id: 1, openId: null }) + .mockResolvedValueOnce({ id: 2, openId: null }); + (prisma.user.update as jest.Mock).mockResolvedValue(undefined); + + await service.bindOpenIdForMiniAppLogin(1, sharedOpenId); + await service.bindOpenIdForMiniAppLogin(2, sharedOpenId); + + expect(prisma.user.update).toHaveBeenNthCalledWith(1, { + where: { id: 1 }, + data: { openId: sharedOpenId }, + }); + expect(prisma.user.update).toHaveBeenNthCalledWith(2, { + where: { id: 2 }, + data: { openId: sharedOpenId }, + }); + }); + + it('拒绝用新的微信账号覆盖已绑定院内账号', async () => { + const { prisma, service } = createService(); + + (prisma.user.findUnique as jest.Mock).mockResolvedValue({ + id: 1, + openId: 'bound-openid', + }); + + await expect( + service.bindOpenIdForMiniAppLogin(1, 'other-openid'), + ).rejects.toThrow(new ConflictException('当前院内账号已绑定其他微信账号')); + expect(prisma.user.update).not.toHaveBeenCalled(); + }); + + it('目标院内账号不存在时返回 404', async () => { + const { prisma, service } = createService(); + + (prisma.user.findUnique as jest.Mock).mockResolvedValue(null); + + await expect( + service.bindOpenIdForMiniAppLogin(999, 'shared-openid'), + ).rejects.toThrow(new NotFoundException('用户不存在')); + }); +}); diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 04f99c0..137c265 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -29,6 +29,11 @@ const SAFE_USER_SELECT = { hospitalId: true, departmentId: true, groupId: true, + hospital: { + select:{ + name:true + } + } } as const; const DIRECTOR_VISIBLE_ROLES = [Role.LEADER, Role.DOCTOR] as const; @@ -63,7 +68,6 @@ export class UsersService { Role.SYSTEM_ADMIN, dto.systemAdminBootstrapKey, ); - await this.assertOpenIdUnique(openId); await this.assertPhoneRoleScopeUnique(phone, Role.SYSTEM_ADMIN, null); const passwordHash = await hash(password, 12); @@ -185,6 +189,7 @@ export class UsersService { /** * 小程序登录时绑定 openId。 + * 同一个微信账号允许绑定多个院内账号,但单个院内账号仅允许绑定一个微信账号。 */ async bindOpenIdForMiniAppLogin(userId: number, openId: string) { const normalizedOpenId = this.normalizeRequiredString(openId, 'openId'); @@ -199,15 +204,9 @@ export class UsersService { throw new NotFoundException(MESSAGES.USER.NOT_FOUND); } if (current.openId && current.openId !== normalizedOpenId) { - throw new ConflictException(MESSAGES.AUTH.MINIAPP_OPEN_ID_BOUND_OTHER_USER); - } - - const existing = await this.prisma.user.findUnique({ - where: { openId: normalizedOpenId }, - select: { id: true }, - }); - if (existing && existing.id !== current.id) { - throw new ConflictException(MESSAGES.AUTH.MINIAPP_OPEN_ID_BOUND_OTHER_USER); + throw new ConflictException( + MESSAGES.AUTH.MINIAPP_OPEN_ID_BOUND_OTHER_USER, + ); } if (!current.openId) { @@ -260,7 +259,6 @@ export class UsersService { scoped.departmentId, scoped.groupId, ); - await this.assertOpenIdUnique(openId); await this.assertPhoneRoleScopeUnique(phone, role, scoped.hospitalId); return this.prisma.user.create({ @@ -393,7 +391,6 @@ export class UsersService { updateUserDto.openId !== undefined ? this.normalizeOptionalString(updateUserDto.openId) : current.openId; - await this.assertOpenIdUnique(nextOpenId, userId); const nextPhone = updateUserDto.phone !== undefined ? this.normalizePhone(updateUserDto.phone) @@ -525,19 +522,17 @@ export class UsersService { /** * 统一构造院内账号登录响应。 */ - private buildUserLoginResponse( - user: { - id: number; - name: string; - phone: string; - openId: string | null; - role: Role; - hospitalId: number | null; - departmentId: number | null; - groupId: number | null; - passwordHash?: string | null; - }, - ) { + private buildUserLoginResponse(user: { + id: number; + name: string; + phone: string; + openId: string | null; + role: Role; + hospitalId: number | null; + departmentId: number | null; + groupId: number | null; + passwordHash?: string | null; + }) { const actor: ActorContext = { id: user.id, role: user.role, @@ -598,23 +593,6 @@ export class UsersService { } } - /** - * 校验 openId 唯一性。 - */ - private async assertOpenIdUnique(openId: string | null, selfId?: number) { - if (!openId) { - return; - } - - const exists = await this.prisma.user.findUnique({ - where: { openId }, - select: { id: true }, - }); - if (exists && exists.id !== selfId) { - throw new ConflictException(MESSAGES.USER.DUPLICATE_OPEN_ID); - } - } - /** * 校验角色与组织归属关系是否合法。 */ @@ -1027,7 +1005,9 @@ export class UsersService { typeof payload !== 'object' || payload.purpose !== 'PASSWORD_LOGIN_TICKET' || !Array.isArray(payload.userIds) || - payload.userIds.some((item) => typeof item !== 'number' || !Number.isInteger(item)) + payload.userIds.some( + (item) => typeof item !== 'number' || !Number.isInteger(item), + ) ) { throw new UnauthorizedException( MESSAGES.AUTH.PASSWORD_LOGIN_TICKET_INVALID, diff --git a/test/e2e/helpers/e2e-fixtures.helper.ts b/test/e2e/helpers/e2e-fixtures.helper.ts index d7f582b..6582096 100644 --- a/test/e2e/helpers/e2e-fixtures.helper.ts +++ b/test/e2e/helpers/e2e-fixtures.helper.ts @@ -1095,7 +1095,7 @@ async function requireUserScope( prisma: PrismaService, openId: string, ): Promise { - const user = await prisma.user.findUnique({ + const user = await prisma.user.findFirst({ where: { openId }, select: { id: true, diff --git a/test/e2e/specs/auth.e2e-spec.ts b/test/e2e/specs/auth.e2e-spec.ts index 2cad441..79ab754 100644 --- a/test/e2e/specs/auth.e2e-spec.ts +++ b/test/e2e/specs/auth.e2e-spec.ts @@ -178,7 +178,9 @@ describe('AuthController (e2e)', () => { const firstStage = await request(ctx.app.getHttpServer()) .post('/auth/miniapp/b/phone-login') - .send(buildMiniAppMockPayload(sharedPhone, uniqueSeedValue('multi-openid'))); + .send( + buildMiniAppMockPayload(sharedPhone, uniqueSeedValue('multi-openid')), + ); expectSuccessEnvelope(firstStage, 201); expect(firstStage.body.data.needSelect).toBe(true); @@ -195,10 +197,87 @@ describe('AuthController (e2e)', () => { expect(confirmResponse.body.data.user.id).toBe(firstUser.body.data.id); }); + it('成功:同一微信 openId 可切换绑定多个院内账号', async () => { + const sharedPhone = uniquePhone(); + const sharedOpenId = uniqueSeedValue('shared-openid'); + const firstUser = await request(ctx.app.getHttpServer()) + .post('/users') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ + name: uniqueSeedValue('MiniApp 切换账号医生'), + phone: sharedPhone, + role: Role.DOCTOR, + hospitalId: ctx.fixtures.hospitalAId, + departmentId: ctx.fixtures.departmentA1Id, + groupId: ctx.fixtures.groupA1Id, + }); + expectSuccessEnvelope(firstUser, 201); + + const secondUser = await request(ctx.app.getHttpServer()) + .post('/users') + .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) + .send({ + name: uniqueSeedValue('MiniApp 切换账号工程师'), + phone: sharedPhone, + role: Role.ENGINEER, + hospitalId: ctx.fixtures.hospitalAId, + }); + expectSuccessEnvelope(secondUser, 201); + + const firstStageForDoctor = await request(ctx.app.getHttpServer()) + .post('/auth/miniapp/b/phone-login') + .send(buildMiniAppMockPayload(sharedPhone, sharedOpenId)); + expectSuccessEnvelope(firstStageForDoctor, 201); + + const doctorConfirm = await request(ctx.app.getHttpServer()) + .post('/auth/miniapp/b/phone-login/confirm') + .send({ + loginTicket: firstStageForDoctor.body.data.loginTicket, + userId: firstUser.body.data.id, + }); + expectSuccessEnvelope(doctorConfirm, 201); + + const firstStageForEngineer = await request(ctx.app.getHttpServer()) + .post('/auth/miniapp/b/phone-login') + .send(buildMiniAppMockPayload(sharedPhone, sharedOpenId)); + expectSuccessEnvelope(firstStageForEngineer, 201); + + const engineerConfirm = await request(ctx.app.getHttpServer()) + .post('/auth/miniapp/b/phone-login/confirm') + .send({ + loginTicket: firstStageForEngineer.body.data.loginTicket, + userId: secondUser.body.data.id, + }); + expectSuccessEnvelope(engineerConfirm, 201); + + const users = await ctx.prisma.user.findMany({ + where: { + id: { + in: [firstUser.body.data.id, secondUser.body.data.id], + }, + }, + select: { + id: true, + openId: true, + }, + orderBy: { id: 'asc' }, + }); + + expect(users).toEqual([ + { id: firstUser.body.data.id, openId: sharedOpenId }, + { id: secondUser.body.data.id, openId: sharedOpenId }, + ]); + }); + it('失败:手机号未匹配院内账号返回 404', async () => { const response = await request(ctx.app.getHttpServer()) .post('/auth/miniapp/b/phone-login') - .send(buildMiniAppMockPayload(uniquePhone(), uniqueSeedValue('no-user-openid'))); + .send( + buildMiniAppMockPayload( + uniquePhone(), + uniqueSeedValue('no-user-openid'), + ), + ); expectErrorEnvelope(response, 404, '手机号未匹配到院内账号'); }); @@ -218,7 +297,12 @@ describe('AuthController (e2e)', () => { it('失败:手机号未关联患者档案返回 404', async () => { const response = await request(ctx.app.getHttpServer()) .post('/auth/miniapp/c/phone-login') - .send(buildMiniAppMockPayload(uniquePhone(), uniqueSeedValue('family-openid'))); + .send( + buildMiniAppMockPayload( + uniquePhone(), + uniqueSeedValue('family-openid'), + ), + ); expectErrorEnvelope(response, 404, '当前手机号未关联患者档案'); }); diff --git a/test/e2e/specs/patients.e2e-spec.ts b/test/e2e/specs/patients.e2e-spec.ts index b48aa77..5729b3f 100644 --- a/test/e2e/specs/patients.e2e-spec.ts +++ b/test/e2e/specs/patients.e2e-spec.ts @@ -90,6 +90,23 @@ describe('Patients Controllers (e2e)', () => { ]), ); + const patientA1 = ( + systemAdminResponse.body.data as Array<{ + id: number; + primaryDisease: string | null; + hydrocephalusTypes: string[]; + surgeryDate: string | null; + currentPressure: string | null; + initialPressure: string | null; + }> + ).find((item) => item.id === ctx.fixtures.patients.patientA1Id); + expect(patientA1).toBeDefined(); + expect(patientA1?.primaryDisease).toBeTruthy(); + expect(Array.isArray(patientA1?.hydrocephalusTypes)).toBe(true); + expect(patientA1?.surgeryDate).toBeTruthy(); + expect(patientA1?.currentPressure).toBeTruthy(); + expect(patientA1?.initialPressure).toBeTruthy(); + const hospitalAdminResponse = await request(ctx.app.getHttpServer()) .get('/b/patients') .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);