diff --git a/drizzle/0000_stormy_falcon.sql b/drizzle/0000_stormy_falcon.sql new file mode 100644 index 0000000..8319f76 --- /dev/null +++ b/drizzle/0000_stormy_falcon.sql @@ -0,0 +1,67 @@ +CREATE TABLE "department" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar NOT NULL, + "hospital" uuid NOT NULL, + "description" varchar, + "isActive" boolean DEFAULT true NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "doctor" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar NOT NULL, + "username" varchar NOT NULL, + "password" varchar NOT NULL, + "phone" varchar NOT NULL, + "hospitalId" uuid NOT NULL, + "departmentId" uuid, + "groupId" uuid, + "roleId" uuid NOT NULL, + "isDoctor" boolean DEFAULT true NOT NULL, + "isActive" boolean DEFAULT true NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "doctor_username_unique" UNIQUE("username") +); +--> statement-breakpoint +CREATE TABLE "group" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar NOT NULL, + "departmentId" uuid NOT NULL, + "description" varchar, + "isActive" boolean DEFAULT true NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "hospital" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar NOT NULL, + "description" text, + "isActive" boolean DEFAULT true NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "patient" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar NOT NULL, + "chiefDoctorId" uuid NOT NULL, + "sharedWith" text, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "role" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "name" varchar NOT NULL, + "code" varchar NOT NULL, + "description" text, + "permissions" text, + "isActive" boolean DEFAULT true NOT NULL, + "createdAt" timestamp DEFAULT now() NOT NULL, + "updatedAt" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "role_name_unique" UNIQUE("name"), + CONSTRAINT "role_code_unique" UNIQUE("code") +); diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..2a33432 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,426 @@ +{ + "id": "89cec236-6382-4cab-b163-f1fbd0cfdddb", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.department": { + "name": "department", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "hospital": { + "name": "hospital", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.doctor": { + "name": "doctor", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "phone": { + "name": "phone", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "hospitalId": { + "name": "hospitalId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "departmentId": { + "name": "departmentId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "groupId": { + "name": "groupId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "roleId": { + "name": "roleId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "isDoctor": { + "name": "isDoctor", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "doctor_username_unique": { + "name": "doctor_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.group": { + "name": "group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "departmentId": { + "name": "departmentId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.hospital": { + "name": "hospital", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.patient": { + "name": "patient", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "chiefDoctorId": { + "name": "chiefDoctorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "sharedWith": { + "name": "sharedWith", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.role": { + "name": "role", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "isActive": { + "name": "isActive", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "role_name_unique": { + "name": "role_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + }, + "role_code_unique": { + "name": "role_code_unique", + "nullsNotDistinct": false, + "columns": [ + "code" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..0482ef7 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1768760286048, + "tag": "0000_stormy_falcon", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/controllers/patient.ts b/src/controllers/patient.ts index f9a65e2..1f01b40 100644 --- a/src/controllers/patient.ts +++ b/src/controllers/patient.ts @@ -1,4 +1,4 @@ -import { eq, inArray } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { db } from "../index"; import { patientTable } from "../modules"; @@ -47,7 +47,7 @@ export const list = async ({ doctorId }: { doctorId?: string } = {}) => { return patients; } - // 科室主任可以看到本科室所有医生的患者 + // 科室主任可以看到本科室所有医生的患者 + 分享给自己的患者 if (currentDoctor.role?.code === "DIRECTOR" && currentDoctor.departmentId) { // 获取本科室所有医生 const departmentDoctors = await db.query.doctorTable.findMany({ @@ -57,7 +57,8 @@ export const list = async ({ doctorId }: { doctorId?: string } = {}) => { if (doctorIds.length === 0) return []; - const patients = await db.query.patientTable.findMany({ + // 获取本科室所有医生创建的患者 + const allPatients = await db.query.patientTable.findMany({ with: { chiefDoctor: { with: { @@ -65,13 +66,39 @@ export const list = async ({ doctorId }: { doctorId?: string } = {}) => { }, }, }, - where: (table, { inArray }) => inArray(table.chiefDoctorId, doctorIds), orderBy: (table, { asc }) => asc(table.createdAt), }); - return patients; + + // 过滤出本科室医生创建的患者 + 分享给自己的患者 + const filteredPatients = allPatients.filter((patient: any) => { + // 本科室医生创建的患者 + if (doctorIds.includes(patient.chiefDoctorId)) return true; + + // 分享给自己的患者 + if (patient.sharedWith) { + try { + const sharedWith = JSON.parse(patient.sharedWith); + return sharedWith.includes(doctorId); + } catch (error) { + console.error("解析 sharedWith 失败:", patient.sharedWith, error); + return false; + } + } + + return false; + }); + + console.log("科室主任看到的患者数量:", { + total: allPatients.length, + filtered: filteredPatients.length, + doctorId, + departmentId: currentDoctor.departmentId, + }); + + return filteredPatients; } - // 组长可以看到本小组所有医生的患者 + // 组长可以看到本小组所有医生的患者 + 分享给自己的患者 if (currentDoctor.role?.code === "GROUP_LEADER" && currentDoctor.groupId) { // 获取本小组所有医生 const groupDoctors = await db.query.doctorTable.findMany({ @@ -81,7 +108,8 @@ export const list = async ({ doctorId }: { doctorId?: string } = {}) => { if (doctorIds.length === 0) return []; - const patients = await db.query.patientTable.findMany({ + // 获取本小组所有医生创建的患者 + const allPatients = await db.query.patientTable.findMany({ with: { chiefDoctor: { with: { @@ -89,14 +117,40 @@ export const list = async ({ doctorId }: { doctorId?: string } = {}) => { }, }, }, - where: (table, { inArray }) => inArray(table.chiefDoctorId, doctorIds), orderBy: (table, { asc }) => asc(table.createdAt), }); - return patients; + + // 过滤出本小组医生创建的患者 + 分享给自己的患者 + const filteredPatients = allPatients.filter((patient: any) => { + // 本小组医生创建的患者 + if (doctorIds.includes(patient.chiefDoctorId)) return true; + + // 分享给自己的患者 + if (patient.sharedWith) { + try { + const sharedWith = JSON.parse(patient.sharedWith); + return sharedWith.includes(doctorId); + } catch (error) { + console.error("解析 sharedWith 失败:", patient.sharedWith, error); + return false; + } + } + + return false; + }); + + console.log("组长看到的患者数量:", { + total: allPatients.length, + filtered: filteredPatients.length, + doctorId, + groupId: currentDoctor.groupId, + }); + + return filteredPatients; } - // 普通医生只能看到自己的患者 - const patients = await db.query.patientTable.findMany({ + // 普通医生只能看到自己的患者 + 分享给自己的患者 + const allPatients = await db.query.patientTable.findMany({ with: { chiefDoctor: { with: { @@ -104,10 +158,35 @@ export const list = async ({ doctorId }: { doctorId?: string } = {}) => { }, }, }, - where: (table, { eq }) => eq(table.chiefDoctorId, doctorId), orderBy: (table, { asc }) => asc(table.createdAt), }); - return patients; + + // 过滤出分享给自己的患者 + const filteredPatients = allPatients.filter((patient: any) => { + // 自己创建的患者 + if (patient.chiefDoctorId === doctorId) return true; + + // 分享给自己的患者 + if (patient.sharedWith) { + try { + const sharedWith = JSON.parse(patient.sharedWith); + return sharedWith.includes(doctorId); + } catch (error) { + console.error("解析 sharedWith 失败:", patient.sharedWith, error); + return false; + } + } + + return false; + }); + + console.log("普通医生看到的患者数量:", { + total: allPatients.length, + filtered: filteredPatients.length, + doctorId, + }); + + return filteredPatients; }; // 获取单个患者 @@ -192,3 +271,65 @@ export const remove = async ({ params }: { params: { id: string } }) => { } return patient[0]; }; + +// 分享患者给其他医生 +export const share = async ({ + params, + body, + currentDoctorId, +}: { + params: { id: string }; + body: { sharedWith: string[] }; + currentDoctorId?: string; +}) => { + // 获取患者信息 + const patient = await db.query.patientTable.findFirst({ + where: (table, { eq }) => eq(table.id, params.id), + with: { + chiefDoctor: true, + }, + }); + + if (!patient) { + throw new Error("患者不存在"); + } + + // 验证权限:只有患者的主刀医生才能分享 + if (patient.chiefDoctorId !== currentDoctorId) { + throw new Error("无权限分享此患者"); + } + + // 验证:只能分享给同医院的医生 + const currentDoctor = await db.query.doctorTable.findFirst({ + where: (table, { eq }) => eq(table.id, currentDoctorId!), + }); + + if (!currentDoctor) { + throw new Error("当前医生不存在"); + } + + // 验证所有要分享的医生都在同一家医院 + if (body.sharedWith && body.sharedWith.length > 0) { + const targetDoctors = await db.query.doctorTable.findMany({ + where: (table, { inArray }) => inArray(table.id, body.sharedWith), + }); + + for (const targetDoctor of targetDoctors) { + if (targetDoctor.hospitalId !== currentDoctor.hospitalId) { + throw new Error(`只能分享给同医院的医生:${targetDoctor.name} 不在同一家医院`); + } + } + } + + // 将分享的医生ID列表转换为JSON字符串存储 + const updatedPatient = await db + .update(patientTable) + .set({ + sharedWith: JSON.stringify(body.sharedWith), + updatedAt: new Date(), + }) + .where(eq(patientTable.id, params.id)) + .returning(); + + return updatedPatient[0]; +}; diff --git a/src/modules/patient.ts b/src/modules/patient.ts index 1d8fd0e..eed9675 100644 --- a/src/modules/patient.ts +++ b/src/modules/patient.ts @@ -1,9 +1,10 @@ -import { uuid, varchar, timestamp } from "drizzle-orm/pg-core"; +import { uuid, varchar, timestamp, text } from "drizzle-orm/pg-core"; export const Patient = { id: uuid().primaryKey().defaultRandom(), // 主键,UUID name: varchar().notNull(), // 患者姓名 chiefDoctorId: uuid().notNull(), // 主刀医生ID,虚拟外键 + sharedWith: text(), // 分享给其他医生的ID列表,JSON数组格式存储 createdAt: timestamp().notNull().defaultNow(), // 创建时间 updatedAt: timestamp().notNull().defaultNow(), // 更新时间 }; diff --git a/src/routes/patient.ts b/src/routes/patient.ts index 3bff225..a9f6db3 100644 --- a/src/routes/patient.ts +++ b/src/routes/patient.ts @@ -45,4 +45,31 @@ export const patientRoutes = new Elysia({ prefix: "/patient" }) }), }, ) - .delete("/:id", ({ params }) => patientController.remove({ params })); + .delete("/:id", ({ params }) => patientController.remove({ params })) + .post( + "/:id/share", + async ({ params, body, headers, jwt }) => { + // 从 Authorization header 获取 token + const authHeader = headers.authorization; + const token = authHeader?.replace("Bearer ", ""); + + let currentDoctorId; + if (token) { + try { + const payload = await jwt.verify(token); + if (payload && typeof payload === "object" && "userId" in payload) { + currentDoctorId = payload.userId as string; + } + } catch (error) { + throw new Error("无效的token"); + } + } + + return patientController.share({ params, body, currentDoctorId }); + }, + { + body: t.Object({ + sharedWith: t.Array(t.String()), + }), + } + ); diff --git a/test.ts b/test.ts index c23cc8d..be1a464 100644 --- a/test.ts +++ b/test.ts @@ -6,7 +6,7 @@ const client = treaty("localhost:3000"); const res = await client.api.patient.get({ headers: { Authorization: - "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiJiMzcwMzgwNi1jNDRjLTRkMTEtYWMzYy03NDAwYzViODYyNmEiLCJyb2xlSWQiOiIzOTA0YmQ0OC01MzdhLTQ0MzgtODE4Yi01NDJlYjcyNjA4YmUiLCJyb2xlQ29kZSI6IkdST1VQX0xFQURFUiIsImlhdCI6MTc2ODc1OTM1Mn0.IkoYXCQy44HFG2Y7dZWHGJAmieYEuCSqAZE0oG46z40", + "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI3MDE5OGMyZS1lM2EyLTRhZTUtOTdjMC01YWJlNGE0MmU3ZWQiLCJyb2xlSWQiOiI5MWU4MDE4Mi05MTcwLTRjMDEtYmYxNC1hNDQwM2FjZTAyMGIiLCJyb2xlQ29kZSI6IkRPQ1RPUiIsImlhdCI6MTc2ODc2MDY5MH0.TAjbfbP5nszzw9-keufpBcjbI45UnZm5hHJp5sHStRg", }, });