From 2bfe8ac8c830494b690793a360c98de05a81eb93 Mon Sep 17 00:00:00 2001 From: EL <1175065040@qq.com> Date: Fri, 20 Mar 2026 04:35:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E4=B8=8A=E4=BC=A0=E8=B5=84?= =?UTF-8?q?=E4=BA=A7=E6=A8=A1=E5=9E=8B=E4=B8=8E=E8=BF=81=E7=A7=BB=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=20IMAGE=E3=80=81VIDEO=E3=80=81FILE=20?= =?UTF-8?q?=E4=B8=89=E7=B1=BB=E8=B5=84=E4=BA=A7=E7=AE=A1=E7=90=86=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20B=20=E7=AB=AF=E4=B8=8A=E4=BC=A0=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E4=B8=8E=E5=88=97=E8=A1=A8=E6=8E=A5=E5=8F=A3=EF=BC=8C?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=92=8C?= =?UTF-8?q?=E5=88=86=E9=A1=B5=E6=9F=A5=E8=AF=A2=E8=83=BD=E5=8A=9B=20?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=83=BD=E5=8A=9B=E6=94=AF=E6=8C=81=E5=8C=BB?= =?UTF-8?q?=E9=99=A2=E7=BA=A7=E6=95=B0=E6=8D=AE=E9=9A=94=E7=A6=BB=EF=BC=9A?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E7=AE=A1=E7=90=86=E5=91=98=E9=9C=80=E6=98=BE?= =?UTF-8?q?=E5=BC=8F=E6=8C=87=E5=AE=9A=E5=8C=BB=E9=99=A2=EF=BC=8C=E9=99=A2?= =?UTF-8?q?=E5=86=85=E8=A7=92=E8=89=B2=E6=8C=89=E7=99=BB=E5=BD=95=E5=8C=BB?= =?UTF-8?q?=E9=99=A2=E8=87=AA=E5=8A=A8=E9=9A=94=E7=A6=BB=20=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E4=B8=8A=E4=BC=A0=E8=87=AA=E5=8A=A8=E5=8E=8B=E7=BC=A9?= =?UTF-8?q?=E5=B9=B6=E8=BD=AC=E4=B8=BA=20webp=EF=BC=8C=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E8=87=AA=E5=8A=A8=E8=BD=AC=E7=A0=81=E5=B9=B6?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E4=B8=BA=20mp4=EF=BC=8C=E6=99=AE=E9=80=9A?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E6=8C=89=E5=8E=9F=E5=A7=8B=E7=B1=BB=E5=9E=8B?= =?UTF-8?q?=E5=AD=98=E5=82=A8=20=E5=A2=9E=E5=8A=A0=E4=B8=8A=E4=BC=A0?= =?UTF-8?q?=E7=9B=AE=E5=BD=95=E4=B8=8E=E5=85=AC=E5=BC=80=E8=AE=BF=E9=97=AE?= =?UTF-8?q?=E8=83=BD=E5=8A=9B=EF=BC=8C=E7=BB=9F=E4=B8=80=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E5=8F=AF=E7=9B=B4=E6=8E=A5=E9=A2=84=E8=A7=88=E7=9A=84=E8=AE=BF?= =?UTF-8?q?=E9=97=AE=E5=9C=B0=E5=9D=80=20=E5=89=8D=E7=AB=AF=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=BD=B1=E5=83=8F=E5=BA=93=E9=A1=B5=E9=9D=A2=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=8C=89=E7=B1=BB=E5=9E=8B=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E3=80=81=E5=85=B3=E9=94=AE=E5=AD=97=E6=A3=80=E7=B4=A2=E3=80=81?= =?UTF-8?q?=E5=88=86=E9=A1=B5=E6=B5=8F=E8=A7=88=E3=80=81=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E9=A2=84=E8=A7=88=E4=B8=8E=E5=8E=9F=E6=96=87=E4=BB=B6=E8=AE=BF?= =?UTF-8?q?=E9=97=AE=20=E5=89=8D=E7=AB=AF=E6=96=B0=E5=A2=9E=E9=80=9A?= =?UTF-8?q?=E7=94=A8=E4=B8=8A=E4=BC=A0=E7=BB=84=E4=BB=B6=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=9C=A8=E9=A1=B5=E9=9D=A2=E5=86=85=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E5=B9=B6=E8=BF=94=E5=9B=9E=E4=B8=8A=E4=BC=A0=E7=BB=93=E6=9E=9C?= =?UTF-8?q?=20=E7=AE=A1=E7=90=86=E5=90=8E=E5=8F=B0=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=BD=B1=E5=83=8F=E5=BA=93=E8=8F=9C=E5=8D=95=E4=B8=8E=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=EF=BC=8C=E5=B9=B6=E8=A1=A5=E5=85=85=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E7=BA=A7=E8=A7=92=E8=89=B2=E6=9D=83=E9=99=90=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=20=E6=82=A3=E8=80=85=E6=89=8B=E6=9C=AF=E7=9B=B8=E5=85=B3?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=E6=8E=A5=E5=85=A5=E4=B8=8A=E4=BC=A0=E5=A4=8D?= =?UTF-8?q?=E7=94=A8=E8=83=BD=E5=8A=9B=EF=BC=8C=E6=94=AF=E6=8C=81=E6=9C=AF?= =?UTF-8?q?=E5=89=8D=E8=B5=84=E6=96=99=E4=B8=8E=E8=AE=BE=E5=A4=87=E6=A0=87?= =?UTF-8?q?=E7=AD=BE=E4=B8=8A=E4=BC=A0=E5=9B=9E=E5=A1=AB=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E4=B8=8A=E4=BC=A0=E6=A8=A1=E5=9D=97=20e2e=20=E7=94=A8?= =?UTF-8?q?=E4=BE=8B=EF=BC=8C=E8=A6=86=E7=9B=96=E6=88=90=E5=8A=9F=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E3=80=81=E6=9D=83=E9=99=90=E7=9F=A9=E9=98=B5=E4=B8=8E?= =?UTF-8?q?=E5=85=B3=E9=94=AE=E5=A4=B1=E8=B4=A5=E5=9C=BA=E6=99=AF=20?= =?UTF-8?q?=E8=A1=A5=E5=85=85=E4=B8=8A=E4=BC=A0=E6=A8=A1=E5=9D=97=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E4=B8=8E=E5=AE=89=E8=A3=85=E4=BE=9D=E8=B5=96=E8=AF=B4?= =?UTF-8?q?=E6=98=8E=EF=BC=8C=E5=AE=8C=E5=96=84=E5=B7=A5=E7=A8=8B=E5=86=85?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/devices.md | 14 +- docs/e2e-testing.md | 27 +- docs/frontend-api-integration.md | 39 +- docs/patients.md | 22 +- docs/tasks.md | 32 +- docs/uploads.md | 71 ++ docs/users.md | 24 +- package.json | 6 +- pnpm-lock.yaml | 388 ++++++++++ pnpm-workspace.yaml | 3 + .../migration.sql | 19 + .../migration.sql | 5 + .../migration.sql | 19 + .../migration.sql | 34 + prisma/schema.prisma | 81 +- prisma/seed.mjs | 361 ++++----- src/app.module.ts | 2 + src/common/messages.ts | 30 +- src/common/pressure-level.util.ts | 56 ++ src/departments/departments.controller.ts | 18 +- src/departments/departments.service.ts | 23 +- src/devices/devices.service.ts | 71 +- src/devices/dto/create-device.dto.ts | 6 +- src/devices/dto/create-implant-catalog.dto.ts | 15 +- src/devices/dto/device-query.dto.ts | 5 +- src/groups/groups.controller.ts | 22 +- src/groups/groups.service.ts | 53 +- src/main.ts | 9 +- src/patients/b-patients/b-patients.service.ts | 91 +-- src/patients/c-patients/c-patients.service.ts | 12 +- .../dto/create-patient-surgery.dto.ts | 7 - src/patients/dto/create-surgery-device.dto.ts | 18 +- src/tasks/b-tasks/b-tasks.controller.ts | 92 ++- .../dto/assignable-engineer-query.dto.ts | 20 + src/tasks/dto/publish-task.dto.ts | 18 +- src/tasks/dto/task-record-query.dto.ts | 63 ++ src/tasks/task.service.ts | 442 ++++++++--- src/uploads/b-uploads/b-uploads.controller.ts | 133 ++++ src/uploads/dto/upload-asset-query.dto.ts | 63 ++ src/uploads/upload-path.util.ts | 18 + src/uploads/uploads.module.ts | 12 + src/uploads/uploads.service.spec.ts | 18 + src/uploads/uploads.service.ts | 383 ++++++++++ src/users/users.controller.ts | 8 +- src/users/users.service.ts | 118 +-- .../0bdc181b-5a5f-4911-89ca-d5c56bee0626.png | Bin 0 -> 137 bytes .../1f5bd907-8f7f-44e8-b1ed-499f6c5fd604.png | Bin 0 -> 68 bytes .../23530285-2dce-47e6-bb9a-9b0a5a5a9e4d.png | Bin 0 -> 137 bytes .../39d2aeb7-9e1a-4576-8e52-d278aeabe6e0.png | 1 + .../59692c02-df26-4da0-bffe-0142feb634e0.png | Bin 0 -> 68 bytes .../6bb38cd8-3ccf-4d43-8114-32759924c246.png | Bin 0 -> 68 bytes .../a789898a-215a-4ab6-b8dc-5bab6d040b90.png | Bin 0 -> 68 bytes .../b5c90100-1466-47f7-b765-063e05b0b32c.png | Bin 0 -> 137 bytes .../be5cd629-4d00-4542-82b0-b1b43ce693fe.png | Bin 0 -> 68 bytes .../ce5177bc-3dfd-4874-b672-4bd90865841a.png | Bin 0 -> 137 bytes .../d1551d21-3fb1-4478-9b86-9f33cd5ad724.png | 1 + .../e3b5edd7-6087-4f8a-a059-af9f720c26ef.png | 1 + .../ff32fdc5-a0d4-4652-a853-e3bc6115d8d6.png | 1 + .../05761514-b28b-4daf-a1c6-5c07d4bc85a9.webp | Bin 0 -> 161480 bytes .../476981fb-fc28-4e10-bb96-0d827288d308.webp | Bin 0 -> 124096 bytes .../7f6001a9-0680-42a2-a439-acac7be66b42.png | Bin 0 -> 1973673 bytes .../2026/03/20/20260320031813-ct-image.webp | Bin 0 -> 72 bytes .../2026/03/20/20260320031813-ct-video.mp4 | Bin 0 -> 3636 bytes .../2026/03/20/20260320031813-director.webp | Bin 0 -> 72 bytes .../03/20/20260320031813-hospital_admin.webp | Bin 0 -> 72 bytes .../2026/03/20/20260320031813-seed-image.webp | Bin 0 -> 72 bytes .../2026/03/20/20260320031814-doctor.webp | Bin 0 -> 72 bytes .../2026/03/20/20260320031814-leader.webp | Bin 0 -> 72 bytes ...s;Gate [Streaming] 2026_3_15 20_18_36.webp | Bin 0 -> 161344 bytes ...0320041422-NewBie-Image-Exp0.1_00001_.webp | Bin 0 -> 152116 bytes .../11b9c47a-1295-4d11-b5ac-0dd50b8c9a55.webp | Bin 0 -> 72 bytes .../132830ed-779c-4401-a873-8ea8ef750cac.png | 1 + .../1f9b399d-f40e-439b-9cfe-c23d7a7c742c.png | 1 + .../31bd4f52-1505-4d6b-9096-0c36eaca75f1.webp | Bin 0 -> 72 bytes .../35e269ed-fc22-461d-b24b-af4fc2c1b1c1.png | 1 + .../3de8e702-34d2-4dcb-8f91-34aa7911f4bc.webp | Bin 0 -> 72 bytes .../4c993c14-6bbd-489f-a6d3-f131c35f4894.mp4 | 1 + .../4e934960-2c0c-42ab-8bf7-3c3b1d3aed77.png | 1 + .../73638602-5151-453b-97b4-d1ed9fab70b7.png | 1 + .../79be072b-4733-48d5-85b5-db921ddf24b2.mp4 | Bin 0 -> 3636 bytes .../7d1ecf5f-1787-4c34-bf39-d70a8aa74b54.webp | Bin 0 -> 72 bytes .../8132d627-0d0c-4642-abea-fb9183881427.png | 1 + .../8d450301-8064-4920-b831-f47b1867758a.webp | Bin 0 -> 72 bytes .../8ef30d54-34a5-4c64-b107-a3c2df7a4b67.png | 1 + .../92d0fda1-467b-4728-89ea-ab3d745c618b.png | 1 + .../939d2b20-9a10-4e0a-84f6-642d0149243e.mp4 | Bin 0 -> 3636 bytes .../a01f1065-a4c7-4d9f-8662-ea31f5904211.png | 1 + .../c1c8446b-3a45-4d22-aadc-6172f34f3d68.mp4 | 1 + .../ca3f8d30-3b6c-4da4-8d6d-93a2809a0096.png | 1 + .../e28ac240-15a6-4f29-8d99-efbfc4063897.webp | Bin 0 -> 72 bytes test/e2e/helpers/e2e-auth.helper.ts | 19 +- test/e2e/helpers/e2e-context.helper.ts | 6 +- test/e2e/helpers/e2e-fixtures.helper.ts | 692 +++++++++++++++++- test/e2e/specs/devices.e2e-spec.ts | 27 +- test/e2e/specs/organization.e2e-spec.ts | 36 +- test/e2e/specs/patients.e2e-spec.ts | 94 +-- test/e2e/specs/tasks.e2e-spec.ts | 273 +++++-- test/e2e/specs/uploads.e2e-spec.ts | 225 ++++++ test/e2e/specs/users.e2e-spec.ts | 188 +++-- test/jest-e2e.config.cjs | 1 + tyt-admin/components.d.ts | 7 +- tyt-admin/src/api/tasks.js | 12 +- tyt-admin/src/api/uploads.js | 19 + .../src/components/AssetUploadButton.vue | 62 ++ tyt-admin/src/constants/role-permissions.js | 13 +- tyt-admin/src/layouts/AdminLayout.vue | 16 +- tyt-admin/src/router/index.js | 10 + tyt-admin/src/views/Dashboard.vue | 7 +- tyt-admin/src/views/devices/Devices.vue | 100 ++- .../src/views/dictionaries/Dictionaries.vue | 171 +++-- tyt-admin/src/views/organization/OrgTree.vue | 15 +- tyt-admin/src/views/patients/Patients.vue | 612 +++++++++++++--- .../components/SurgeryFormSection.vue | 135 +++- tyt-admin/src/views/tasks/Tasks.vue | 571 +++++++-------- tyt-admin/src/views/uploads/MediaLibrary.vue | 340 +++++++++ tyt-admin/src/views/users/Users.vue | 184 ++--- tyt-admin/vite.config.js | 4 + 117 files changed, 5252 insertions(+), 1574 deletions(-) create mode 100644 docs/uploads.md create mode 100644 pnpm-workspace.yaml create mode 100644 prisma/migrations/20260319153000_add_patient_creator_fields/migration.sql create mode 100644 prisma/migrations/20260319164000_remove_device_sn_code/migration.sql create mode 100644 prisma/migrations/20260319164701_pressure_label_strings_and_surgeon_refactor/migration.sql create mode 100644 prisma/migrations/20260319184610_add_upload_assets_library/migration.sql create mode 100644 src/common/pressure-level.util.ts create mode 100644 src/tasks/dto/assignable-engineer-query.dto.ts create mode 100644 src/tasks/dto/task-record-query.dto.ts create mode 100644 src/uploads/b-uploads/b-uploads.controller.ts create mode 100644 src/uploads/dto/upload-asset-query.dto.ts create mode 100644 src/uploads/upload-path.util.ts create mode 100644 src/uploads/uploads.module.ts create mode 100644 src/uploads/uploads.service.spec.ts create mode 100644 src/uploads/uploads.service.ts create mode 100644 storage/tmp-uploads/0bdc181b-5a5f-4911-89ca-d5c56bee0626.png create mode 100644 storage/tmp-uploads/1f5bd907-8f7f-44e8-b1ed-499f6c5fd604.png create mode 100644 storage/tmp-uploads/23530285-2dce-47e6-bb9a-9b0a5a5a9e4d.png create mode 100644 storage/tmp-uploads/39d2aeb7-9e1a-4576-8e52-d278aeabe6e0.png create mode 100644 storage/tmp-uploads/59692c02-df26-4da0-bffe-0142feb634e0.png create mode 100644 storage/tmp-uploads/6bb38cd8-3ccf-4d43-8114-32759924c246.png create mode 100644 storage/tmp-uploads/a789898a-215a-4ab6-b8dc-5bab6d040b90.png create mode 100644 storage/tmp-uploads/b5c90100-1466-47f7-b765-063e05b0b32c.png create mode 100644 storage/tmp-uploads/be5cd629-4d00-4542-82b0-b1b43ce693fe.png create mode 100644 storage/tmp-uploads/ce5177bc-3dfd-4874-b672-4bd90865841a.png create mode 100644 storage/tmp-uploads/d1551d21-3fb1-4478-9b86-9f33cd5ad724.png create mode 100644 storage/tmp-uploads/e3b5edd7-6087-4f8a-a059-af9f720c26ef.png create mode 100644 storage/tmp-uploads/ff32fdc5-a0d4-4652-a853-e3bc6115d8d6.png create mode 100644 storage/uploads/1/2026/03/20/05761514-b28b-4daf-a1c6-5c07d4bc85a9.webp create mode 100644 storage/uploads/1/2026/03/20/476981fb-fc28-4e10-bb96-0d827288d308.webp create mode 100644 storage/uploads/1/2026/03/20/7f6001a9-0680-42a2-a439-acac7be66b42.png create mode 100644 storage/uploads/2026/03/20/20260320031813-ct-image.webp create mode 100644 storage/uploads/2026/03/20/20260320031813-ct-video.mp4 create mode 100644 storage/uploads/2026/03/20/20260320031813-director.webp create mode 100644 storage/uploads/2026/03/20/20260320031813-hospital_admin.webp create mode 100644 storage/uploads/2026/03/20/20260320031813-seed-image.webp create mode 100644 storage/uploads/2026/03/20/20260320031814-doctor.webp create mode 100644 storage/uploads/2026/03/20/20260320031814-leader.webp create mode 100644 storage/uploads/2026/03/20/20260320034659-Steins;Gate [Streaming] 2026_3_15 20_18_36.webp create mode 100644 storage/uploads/2026/03/20/20260320041422-NewBie-Image-Exp0.1_00001_.webp create mode 100644 storage/uploads/3/2026/03/20/11b9c47a-1295-4d11-b5ac-0dd50b8c9a55.webp create mode 100644 storage/uploads/3/2026/03/20/132830ed-779c-4401-a873-8ea8ef750cac.png create mode 100644 storage/uploads/3/2026/03/20/1f9b399d-f40e-439b-9cfe-c23d7a7c742c.png create mode 100644 storage/uploads/3/2026/03/20/31bd4f52-1505-4d6b-9096-0c36eaca75f1.webp create mode 100644 storage/uploads/3/2026/03/20/35e269ed-fc22-461d-b24b-af4fc2c1b1c1.png create mode 100644 storage/uploads/3/2026/03/20/3de8e702-34d2-4dcb-8f91-34aa7911f4bc.webp create mode 100644 storage/uploads/3/2026/03/20/4c993c14-6bbd-489f-a6d3-f131c35f4894.mp4 create mode 100644 storage/uploads/3/2026/03/20/4e934960-2c0c-42ab-8bf7-3c3b1d3aed77.png create mode 100644 storage/uploads/3/2026/03/20/73638602-5151-453b-97b4-d1ed9fab70b7.png create mode 100644 storage/uploads/3/2026/03/20/79be072b-4733-48d5-85b5-db921ddf24b2.mp4 create mode 100644 storage/uploads/3/2026/03/20/7d1ecf5f-1787-4c34-bf39-d70a8aa74b54.webp create mode 100644 storage/uploads/3/2026/03/20/8132d627-0d0c-4642-abea-fb9183881427.png create mode 100644 storage/uploads/3/2026/03/20/8d450301-8064-4920-b831-f47b1867758a.webp create mode 100644 storage/uploads/3/2026/03/20/8ef30d54-34a5-4c64-b107-a3c2df7a4b67.png create mode 100644 storage/uploads/3/2026/03/20/92d0fda1-467b-4728-89ea-ab3d745c618b.png create mode 100644 storage/uploads/3/2026/03/20/939d2b20-9a10-4e0a-84f6-642d0149243e.mp4 create mode 100644 storage/uploads/3/2026/03/20/a01f1065-a4c7-4d9f-8662-ea31f5904211.png create mode 100644 storage/uploads/3/2026/03/20/c1c8446b-3a45-4d22-aadc-6172f34f3d68.mp4 create mode 100644 storage/uploads/3/2026/03/20/ca3f8d30-3b6c-4da4-8d6d-93a2809a0096.png create mode 100644 storage/uploads/3/2026/03/20/e28ac240-15a6-4f29-8d99-efbfc4063897.webp create mode 100644 test/e2e/specs/uploads.e2e-spec.ts create mode 100644 tyt-admin/src/api/uploads.js create mode 100644 tyt-admin/src/components/AssetUploadButton.vue create mode 100644 tyt-admin/src/views/uploads/MediaLibrary.vue diff --git a/docs/devices.md b/docs/devices.md index 53492c3..d96385c 100644 --- a/docs/devices.md +++ b/docs/devices.md @@ -13,20 +13,20 @@ 核心字段: -- `snCode`:设备唯一标识 - `patientId`:归属患者 - `surgeryId`:归属手术,可为空 - `implantCatalogId`:型号字典 ID,可为空 - `implantModel` / `implantManufacturer` / `implantName`:历史快照 - `isPressureAdjustable`:是否可调压 - `isAbandoned`:是否弃用 -- `currentPressure`:当前压力 +- `currentPressure`:当前压力挡位标签 - `status`:设备状态 补充: - `currentPressure` 不允许在创建/编辑设备实例时手工指定。 -- 新植入设备默认以 `initialPressure`(或系统默认值)作为当前压力起点,后续只允许在调压任务完成时更新。 +- 新植入设备默认以 `initialPressure`(或系统默认值 `0`)作为当前压力起点,后续只允许在调压任务完成时更新。 +- 发布调压任务时不会立刻修改 `currentPressure`,只有任务完成后才会把目标挡位回写到设备。 ## 3. 植入物目录 @@ -35,7 +35,7 @@ - `modelCode`:型号编码,唯一 - `manufacturer`:厂商 - `name`:名称 -- `pressureLevels`:可调压器械的挡位列表 +- `pressureLevels`:可调压器械的挡位字符串标签列表 - `isPressureAdjustable`:是否可调压 - `notes`:目录备注 @@ -45,6 +45,11 @@ - 仅 `SYSTEM_ADMIN` 可做目录 CRUD - 目录是全局共享的,不按医院隔离 +说明: + +- 挡位列表按字符串标签保存,例如 `["0.5", "1", "1.5"]` 或 `["10", "20", "30"]`。 +- 保存前会自动标准化并去重排序,例如 `["01.0", "1.50", "1"]` 最终会整理为 `["1", "1.5"]`。 + ## 4. 接口 设备实例: @@ -65,7 +70,6 @@ ## 5. 约束 - 设备必须绑定到一个患者。 -- 设备 SN 在全库唯一,服务端会统一转成大写后再校验。 - 删除已被任务明细引用的设备会返回 `409`。 - 删除已被患者手术引用的植入物目录会返回 `409`。 - 可调压植入物若配置了 `pressureLevels`,患者手术录入和任务调压时的压力值必须命中该挡位列表。 diff --git a/docs/e2e-testing.md b/docs/e2e-testing.md index bb17bd2..c6dfc69 100644 --- a/docs/e2e-testing.md +++ b/docs/e2e-testing.md @@ -4,17 +4,17 @@ - 覆盖 `src/**/*controller.ts` 当前全部 30 个业务接口。 - 采用 `supertest + @nestjs/testing` 进行真实 HTTP E2E 测试。 -- 测试前固定执行数据库重置与 seed,确保结果可重复。 +- 测试前固定执行数据库重置,并通过真实接口全流程建数,确保结果可重复。 ## 2. 风险提示 `pnpm test:e2e` 会执行: 1. `prisma migrate reset --force` -2. `node prisma/seed.mjs` +2. 启动 Jest 后,由测试用例通过真实 HTTP 接口完成基础夹具创建 这会清空 `.env` 中 `DATABASE_URL` 指向数据库的全部数据,请仅在测试库执行。 -另外,seed 账号会刷新 `tokenValidAfter`,所以重置前签发的旧 token 会全部失效,需要重新登录获取新 token。 +另外,接口引导创建的测试账号会刷新 `tokenValidAfter`,所以重置前签发的旧 token 会全部失效,需要重新登录获取新 token。 ## 3. 运行命令 @@ -22,7 +22,7 @@ pnpm test:e2e ``` -仅重置数据库并注入 seed: +仅重置数据库并重新生成 Prisma Client: ```bash pnpm test:e2e:prepare @@ -34,7 +34,7 @@ pnpm test:e2e:prepare pnpm test:e2e:watch ``` -## 4. 种子账号(默认密码:`Seed@1234`) +## 4. 接口引导夹具(默认密码:`Seed@1234`) - 系统管理员:`13800001000` - 院管(医院 A):`13800001001` @@ -42,20 +42,37 @@ pnpm test:e2e:watch - 组长(医院 A):`13800001003` - 医生(医院 A):`13800001004` - 工程师(医院 A):`13800001005` +- 院管(医院 B):`13800001011` +- 工程师(医院 B):`13800001015` + +说明: + +- 这些账号不再由 `prisma/seed.mjs` 直写生成。 +- 每次执行 E2E 时,会先创建系统管理员,再通过后台接口依次创建医院、院管、医生、工程师、目录、患者、手术和调压任务。 +- 因为夹具是通过真实业务接口生成的,所以权限、作用域、删除保护和调压链路都能在同一套测试里被覆盖。 ## 5. 用例结构 - `test/e2e/specs/auth.e2e-spec.ts` - `test/e2e/specs/users.e2e-spec.ts` - `test/e2e/specs/organization.e2e-spec.ts` +- `test/e2e/specs/dictionaries.e2e-spec.ts` +- `test/e2e/specs/devices.e2e-spec.ts` - `test/e2e/specs/tasks.e2e-spec.ts` - `test/e2e/specs/patients.e2e-spec.ts` +- `test/e2e/specs/auth-token-revocation.e2e-spec.ts` ## 6. 覆盖策略 - 受保护接口(27 个):每个接口覆盖 6 角色访问结果 + 未登录 401。 - 非受保护接口(3 个):每个接口至少 1 个成功 + 1 个失败。 - 关键行为额外覆盖: + - 从创建系统管理员开始的完整接口建数链路 - 任务状态机冲突(409) + - 调压任务发布后不改当前压力,完成任务后才回写设备当前压力 + - 主刀医生自动跟随患者归属医生,且历史手术保留快照 - 患者 B 端角色可见性 + - 患者创建人返回与展示 + - 跨院工程师隔离 - 组织域院管作用域限制与删除冲突 + - 目录、设备、组织、用户的删除保护 diff --git a/docs/frontend-api-integration.md b/docs/frontend-api-integration.md index 87389a0..445111b 100644 --- a/docs/frontend-api-integration.md +++ b/docs/frontend-api-integration.md @@ -5,10 +5,11 @@ - 登录页:`/auth/login`,支持可选 `hospitalId`。 - 首页看板:按角色拉取组织与患者统计。 - 设备页:新增管理员专用设备 CRUD,复用真实设备接口。 -- 任务页:接入 `publish/accept/complete/cancel` 四个真实任务接口。 +- 任务页:改为只读调压记录页,接入真实任务列表接口。 - 用户页:修复用户列表响应结构、组织字段联动、工程师分配医院参数。 - 患者页:接入真实患者字段与生命周期查询参数(`phone + idCard`), - 后端直接保存身份证号原文,不再做哈希转换。 + 后端直接保存身份证号原文,不再做哈希转换;调压任务入口迁到患者页。 +- 新增影像库页:接入真实上传接口,支持图片/视频/文件上传与分页查看。 ## 2. 接口契约对齐点 @@ -16,23 +17,27 @@ - `PATCH /b/users/:id/assign-engineer-hospital` 参数为单个 `hospitalId`,非数组。 - `GET /b/patients` 返回数组,前端已改为本地分页与筛选。 - `GET /b/devices` 已支持服务端分页与筛选,前端直接透传 `page/pageSize`。 +- `GET /b/tasks` 返回 `{ list, total, page, pageSize }`,供任务页只读展示调压记录。 +- `POST /b/uploads` 使用 `multipart/form-data` 上传文件,返回上传资产元数据。 +- `GET /b/uploads` 返回 `{ list, total, page, pageSize }`,仅供系统管理员/医院管理员影像库分页展示。 +- `GET /b/tasks/engineers` 返回可选接收工程师列表,患者页发布调压任务前需要先拉取。 - `GET /c/patients/lifecycle` 必须同时传 `phone` 和 `idCard`。 - 患者表单中的 `idCard` 字段直接传身份证号; 服务端只会做去空格与 `x/X` 标准化,不会转哈希。 -- 任务模块暂无任务列表接口,前端改为“表单操作 + 最近结果”模式。 +- 患者手术、调压任务、设备目录中的压力值全部按字符串挡位标签传输,例如 `0.5`、`1`、`1.5`、`10`。 ## 3. 角色权限提示 - 任务接口权限: - - `DOCTOR/DIRECTOR/LEADER`:发布、取消(仅可取消自己创建的任务) - - `ENGINEER`:接收、完成 + - `SYSTEM_ADMIN/HOSPITAL_ADMIN/DOCTOR/DIRECTOR/LEADER`:发布时必须指定接收工程师;可取消自己创建的任务 + - `ENGINEER`:仅可完成分配给自己的任务 - 患者列表权限: - `SYSTEM_ADMIN` 查询时必须传 `hospitalId` - 用户管理接口: - - `SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR` 可访问列表与创建 - - `DIRECTOR` 页面语义调整为“医生管理”,仅管理本科室医生 + - `SYSTEM_ADMIN/HOSPITAL_ADMIN` 可创建、编辑、删除 + - `DIRECTOR` 可只读查看本科室下级医生/组长 - 工程师绑定医院仅 `SYSTEM_ADMIN` - - 删除:`SYSTEM_ADMIN` 可删除任意无关联用户;`DIRECTOR` 可删除本科室无关联医生 + - 删除:`SYSTEM_ADMIN` 可删除任意无关联用户;`HOSPITAL_ADMIN` 可删除本院无关联、且非管理员用户 ## 3.1 结构图页面交互调整 @@ -46,27 +51,29 @@ `SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DIRECTOR`、`LEADER` 可访问 - `organization/departments`: 仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问 -- `users`:`SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DIRECTOR` 可访问 +- `users`:`SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可管理;`DIRECTOR` 可只读查看 - `devices`:仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问 +- `uploads`:仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 可访问 - `organization/hospitals` - 仅 `SYSTEM_ADMIN` 可访问 - `tasks` - - 仅 `DOCTOR`、`DIRECTOR`、`LEADER`、`ENGINEER` 可访问 + - `SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DOCTOR`、`DIRECTOR`、`LEADER`、`ENGINEER` 可访问 - `patients` - `SYSTEM_ADMIN`、`HOSPITAL_ADMIN`、`DIRECTOR`、`LEADER`、`DOCTOR` 可访问 +患者页负责发起调压任务并指定接收人,任务页仅查看调压记录,不再提供发布或接收入口。 + +患者手术表单中的主刀医生不再单独选择,直接跟随患者归属医生展示和保存。 + 前端已在路由守卫和侧边栏菜单同时做权限控制,无权限角色会被拦截并跳转到首页,避免进入页面后触发接口 `403`。 ## 3.3 主任/组长组织管理范围 - `DIRECTOR` - - 可查看组织架构、小组列表(限定本科室范围) - - 可创建/编辑/删除本科室下小组 - - 可进入“医生管理”页,创建/维护本科室医生 + - 仅保留业务使用和患者管理能力,不再承担科室/小组/医生管理 - `LEADER` - - 可查看组织架构、小组列表(限定本科室/本小组范围) - - 可编辑本小组名称 -- 主任/组长不再显示独立“科室管理”页面。 + - 仅保留业务使用和患者管理能力,不再承担科室/小组/医生管理 +- 主任/组长不再显示“科室管理”“小组管理”“用户管理”页面。 - 负责人设置(设主任/设组长)入口仍仅 `SYSTEM_ADMIN`、`HOSPITAL_ADMIN` 显示。 ## 4. 本地运行 diff --git a/docs/patients.md b/docs/patients.md index 311e931..e6e632f 100644 --- a/docs/patients.md +++ b/docs/patients.md @@ -29,7 +29,8 @@ - `surgeryDate`:手术日期 - `surgeryName`:手术名称 -- `surgeonName`:主刀医生 +- `surgeonId`:主刀医生账号 ID(自动等于患者归属医生) +- `surgeonName`:主刀医生姓名快照 - `preOpPressure`:术前测压,可为空 - `primaryDisease`:原发病 - `hydrocephalusTypes`:脑积水类型,多选 @@ -43,6 +44,12 @@ - `activeDeviceCount`:本次手术仍在用设备数 - `abandonedDeviceCount`:本次手术已弃用设备数 +说明: + +- 新增/修改手术时,前端不再单独提交主刀医生。 +- 后端会直接使用患者当前的 `doctorId` 作为 `surgeonId`,并保存当时的 `surgeonName` 快照。 +- 如果后续患者归属医生发生变化,只会影响后续新建手术;历史手术仍保留创建当时的主刀医生快照。 + ## 4. 植入设备 设备表仍沿用 `Device`,但语义改为“患者手术下植入的设备实例”。 @@ -57,7 +64,7 @@ - `proximalPunctureAreas`:近端穿刺区域,最多 2 个 - `valvePlacementSites`:阀门植入部位,最多 2 个 - `distalShuntDirection`:远端分流方向 -- `initialPressure`:初始压力,可为空 +- `initialPressure`:初始压力挡位标签,可为空 - `implantNotes`:植入物备注,可为空 - `labelImageUrl`:植入物标签图片地址,可为空 @@ -68,8 +75,10 @@ - 旧设备弃用后,`TaskItem` 历史不会删除。 - 任务发布只允许选择 `ACTIVE + isPressureAdjustable=true + isAbandoned=false` 的设备。 - 同一患者一次手术可录入多个设备,因此支持“两个可调压仪器同时佩戴”。 -- 如果植入物目录配置了挡位,`initialPressure` 必须取挡位中的值。 -- 手术创建时不允许手工录入 `currentPressure`,设备当前压力默认继承 `initialPressure`,后续仅能由调压任务完成后更新。 +- 如果植入物目录配置了挡位,`initialPressure` 必须取挡位中的字符串标签值,例如 `0.5 / 1 / 1.5 / 10 / 20 / 30`。 +- 挡位标签保存前会自动标准化,例如 `01.0 -> 1`、`1.50 -> 1.5`。 +- 手术创建时不允许手工录入 `currentPressure`,设备当前压力默认继承 `initialPressure`;发布调压任务时不会立刻改当前压力,只有工程师完成任务后才会更新。 +- 术前资料与植入物标签现在支持直接上传;上传成功后,患者详情会按图片/视频做预览,不再只是裸链接。 ## 5. B 端可见性 @@ -93,6 +102,7 @@ - `PATCH /b/patients/:id` 不直接修改手术,手术必须走新增手术接口。 - 新增二次手术时可传 `abandonedDeviceIds`,后端会将这些旧设备标记为 `INACTIVE + isAbandoned=true`。 +- 患者建档时会自动记录 `creator`,列表和详情都会返回创建人信息。 ## 7. C 端生命周期聚合 @@ -111,6 +121,10 @@ - `SURGERY` - `TASK_PRESSURE_ADJUSTMENT` +说明: + +- 生命周期中的 `initialPressure / currentPressure / oldPressure / targetPressure` 均返回字符串挡位标签。 + ## 8. 响应结构 全部接口统一返回: diff --git a/docs/tasks.md b/docs/tasks.md index 4f51bb9..109607e 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -7,34 +7,47 @@ ## 2. 状态机 -- `PENDING -> ACCEPTED -> COMPLETED` -- `PENDING/ACCEPTED -> CANCELLED` +- 当前发布流程:`ACCEPTED -> COMPLETED` +- 当前取消流程:`ACCEPTED -> CANCELLED` +- 历史兼容:保留 `PENDING` 状态枚举,便于兼容旧数据与旧任务记录 非法流转会返回 `409` 冲突错误(中文消息)。 ## 3. 角色权限 -- 医生/主任/组长:发布任务、取消自己创建的任务 -- 工程师:接收任务、完成自己接收的任务 +- 系统管理员/医院管理员/医生/主任/组长:发布任务时必须直接指定接收工程师,并且只能取消自己创建的任务 +- 工程师:不能再执行“接收”动作,只能完成指派给自己的任务 - 其他角色:默认拒绝 补充: +- `GET /b/tasks/engineers`:返回当前角色可选的接收工程师列表,系统管理员可按医院筛选。 +- `GET /b/tasks`:返回当前角色可见的调压记录列表,系统管理员可按医院筛选。 - `POST /b/tasks/cancel` 现支持可选 `reason` 字段,便于前端保留取消原因输入。 - 当前取消原因仅透传到事件层,数据库暂未持久化该字段。 -## 4. 事件触发 +## 4. 记录列表 + +- 后台任务页不再承担手工发布入口,只展示调压记录。 +- 记录维度按 `TaskItem` 展开,每条记录会携带: + - 任务状态 + - 患者信息 + - 手术名称 + - 设备信息 + - 旧压力 / 目标压力 / 当前压力(均为字符串挡位标签) + - 创建人 / 接收人 / 发布时间 + +## 5. 事件触发 状态变化后会发出事件: - `task.published` -- `task.accepted` - `task.completed` - `task.cancelled` 用于后续接入微信通知或消息中心。 -## 5. 完成任务时的设备同步 +## 6. 完成任务时的设备同步 `completeTask` 在单事务中执行: @@ -43,3 +56,8 @@ 3. 批量更新关联 `Device.currentPressure` 确保任务状态与设备压力一致性。 + +补充: + +- `publishTask` 只负责生成任务和目标挡位,不会立刻修改设备当前压力。 +- 只有工程师完成任务后,目标挡位才会回写到设备实例。 diff --git a/docs/uploads.md b/docs/uploads.md new file mode 100644 index 0000000..ca9d9ff --- /dev/null +++ b/docs/uploads.md @@ -0,0 +1,71 @@ +# 上传资产模块说明(`src/uploads`) + +## 1. 目标 + +- 提供图片、视频、文件的统一上传入口。 +- 为 B 端“影像库/视频库/文件库”页面提供分页查询。 +- 为患者手术表单中的术前资料、植入物标签上传提供复用能力。 + +## 2. 数据模型 + +新增 `UploadAsset` 表,保存上传文件元数据: + +- `hospitalId`:医院归属 +- `creatorId`:上传人 +- `type`:`IMAGE / VIDEO / FILE` +- `originalName`:原始文件名 +- `fileName`:服务端生成文件名 +- `storagePath`:相对存储路径 +- `url`:公开访问地址,前端直接用于预览 +- `mimeType`:文件 MIME 类型 +- `fileSize`:文件大小(字节) + +文件本体默认落盘到: + +- 公开目录:`storage/uploads` +- 临时目录:`storage/tmp-uploads` +- 最终目录规则:`storage/uploads/YYYY/MM/DD` +- 最终文件名规则:`YYYYMMDDHHmmss-原文件名` + - 图片压缩后扩展名统一为 `.webp` + - 视频压缩后扩展名统一为 `.mp4` + - 如同一秒内出现同名文件,会自动追加 `-1`、`-2` 防止覆盖 + +## 3. 接口 + +- `POST /b/uploads` + - 角色:`SYSTEM_ADMIN / HOSPITAL_ADMIN / DIRECTOR / LEADER / DOCTOR` + - 表单字段: + - `file`:二进制文件 + - `hospitalId`:仅 `SYSTEM_ADMIN` 上传时必填 +- `GET /b/uploads` + - 角色:`SYSTEM_ADMIN / HOSPITAL_ADMIN` + - 查询参数: + - `keyword` + - `type` + - `hospitalId`:仅 `SYSTEM_ADMIN` 可选 + - `page` + - `pageSize` + +## 4. 使用说明 + +- 患者手术表单中的“术前 CT 影像/资料”支持直接上传,上传成功后自动回填 `type/name/url`。 +- 设备表单中的“植入物标签”支持直接上传图片,上传成功后自动回填 `labelImageUrl`。 +- 患者详情页会直接预览术前图片、视频和设备标签。 +- 单独新增“影像库”页面,按图片/视频/文件分页查看所有上传资产。 + - 页面访问权限仅 `SYSTEM_ADMIN / HOSPITAL_ADMIN` + +## 5. 压缩策略 + +- 图片上传后会自动压缩并统一转成 `webp`: + - 自动纠正旋转方向 + - 最大边限制为 `2560` + - 返回的 `mimeType` 为 `image/webp` +- 视频上传后会自动压缩并统一转成 `mp4`: + - 最大边限制为 `1280` + - 视频编码为 `H.264` + - 音频编码为 `AAC` + - 返回的 `mimeType` 为 `video/mp4` +- 普通文件类型不做转码,按原文件保存。 +- 如果本地 `pnpm install` 屏蔽了依赖安装脚本,`ffmpeg-static` 二进制不会自动落盘,视频压缩会失败。 + - 这种情况下手动执行: + - `node node_modules/.pnpm/ffmpeg-static@5.3.0/node_modules/ffmpeg-static/install.js` diff --git a/docs/users.md b/docs/users.md index 83b17cc..6ccd0f0 100644 --- a/docs/users.md +++ b/docs/users.md @@ -18,8 +18,10 @@ - 医院内数据按 `hospitalId` 强隔离。 - 仅 `SYSTEM_ADMIN` 可执行工程师绑定医院。 -- `DIRECTOR/LEADER` 可读取用户列表,但仅返回当前科室可见用户。 -- `DIRECTOR` 可创建、查看、编辑、删除本科室医生,但不能跨科室操作,也不能把医生改成其他角色。 +- `SYSTEM_ADMIN/HOSPITAL_ADMIN` 可执行用户创建、编辑、删除。 +- `DIRECTOR` 仅可只读查看本科室下级医生/组长。 +- `LEADER` 仅可只读查看本小组医生列表。 +- `HOSPITAL_ADMIN` 仅可操作本院非管理员账号。 - 用户组织字段校验: - 院管/医生/工程师等需有医院归属; - 主任/组长需有科室/小组等必要归属; @@ -32,12 +34,22 @@ - `GET /users`、`GET /users/:id`、`PATCH /users/:id`、`DELETE /users/:id` - `POST /b/users/:id/assign-engineer-hospital` +其中院管侧的常用链路为: + +- `POST /users`:创建本院用户 +- `GET /users/:id`:查看本院用户详情 +- `PATCH /users/:id`:修改本院用户信息 +- `DELETE /users/:id`:删除本院无关联、且非管理员用户 + 其中主任侧的常用链路为: -- `POST /users`:创建本科室医生 -- `GET /users/:id`:查看本科室医生详情 -- `PATCH /users/:id`:修改本科室医生信息 -- `DELETE /users/:id`:删除无关联数据的本科室医生 +- `GET /users`:查看本科室下级医生/组长 +- `GET /users/:id`:查看本科室下级详情 + +其中组长侧的常用链路为: + +- `GET /users`:查看本小组医生列表 +- `GET /users/:id`:查看本小组医生详情 ## 5. 开发改造建议 diff --git a/package.json b/package.json index 23b3d86..2ded8a2 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "start:dev": "nest start --watch", "start:debug": "nest start --debug --watch", "start:prod": "node dist/main", - "test:e2e:prepare": "pnpm prisma migrate reset --force && pnpm prisma generate && node prisma/seed.mjs", + "test:e2e:prepare": "pnpm prisma migrate reset --force && pnpm prisma generate", "test:e2e": "pnpm test:e2e:prepare && NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config ./test/jest-e2e.config.cjs --runInBand", "test:e2e:watch": "NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config ./test/jest-e2e.config.cjs --watch" }, @@ -30,10 +30,13 @@ "class-transformer": "^0.5.1", "class-validator": "^0.15.1", "dotenv": "^17.3.1", + "ffmpeg-static": "^5.3.0", "jsonwebtoken": "^9.0.3", + "multer": "^2.1.1", "pg": "^8.20.0", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1", + "sharp": "^0.34.5", "swagger-ui-express": "^5.0.1" }, "devDependencies": { @@ -44,6 +47,7 @@ "@types/express": "^5.0.0", "@types/jest": "^30.0.0", "@types/jsonwebtoken": "^9.0.10", + "@types/multer": "^2.1.0", "@types/node": "^22.10.7", "@types/supertest": "^6.0.2", "globals": "^16.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c19a0c..b51083e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,9 +44,15 @@ importers: dotenv: specifier: ^17.3.1 version: 17.3.1 + ffmpeg-static: + specifier: ^5.3.0 + version: 5.3.0 jsonwebtoken: specifier: ^9.0.3 version: 9.0.3 + multer: + specifier: ^2.1.1 + version: 2.1.1 pg: specifier: ^8.20.0 version: 8.20.0 @@ -56,6 +62,9 @@ importers: rxjs: specifier: ^7.8.1 version: 7.8.2 + sharp: + specifier: ^0.34.5 + version: 0.34.5 swagger-ui-express: specifier: ^5.0.1 version: 5.0.1(express@5.2.1) @@ -81,6 +90,9 @@ importers: '@types/jsonwebtoken': specifier: ^9.0.10 version: 9.0.10 + '@types/multer': + specifier: ^2.1.0 + version: 2.1.0 '@types/node': specifier: ^22.10.7 version: 22.19.15 @@ -342,6 +354,10 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@derhuerst/http-basic@8.2.4': + resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==} + engines: {node: '>=6.0.0'} + '@electric-sql/pglite-socket@0.0.20': resolution: {integrity: sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==} hasBin: true @@ -371,6 +387,159 @@ packages: peerDependencies: hono: ^4 + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.34.5': + resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.34.5': + resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-darwin-arm64@1.2.4': + resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.2.4': + resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.2.4': + resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.2.4': + resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.2.4': + resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.2.4': + resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.2.4': + resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.2.4': + resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.34.5': + resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.34.5': + resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.34.5': + resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.34.5': + resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.34.5': + resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.34.5': + resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.34.5': + resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.34.5': + resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.34.5': + resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.34.5': + resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.34.5': + resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.34.5': + resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [x64] + os: [win32] + '@inquirer/ansi@1.0.2': resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} engines: {node: '>=18'} @@ -932,6 +1101,12 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/multer@2.1.0': + resolution: {integrity: sha512-zYZb0+nJhOHtPpGDb3vqPjwpdeGlGC157VpkqNQL+UU2qwoacoQ7MpsAmUptI/0Oa127X32JzWDqQVEXp2RcIA==} + + '@types/node@10.17.60': + resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + '@types/node@22.19.15': resolution: {integrity: sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==} @@ -1147,6 +1322,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1368,6 +1547,9 @@ packages: caniuse-lite@1.0.30001778: resolution: {integrity: sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==} + caseless@0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -1573,6 +1755,10 @@ packages: destr@2.0.5: resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + detect-newline@3.1.0: resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} engines: {node: '>=8'} @@ -1633,6 +1819,10 @@ packages: resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} engines: {node: '>=10.13.0'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -1736,6 +1926,10 @@ packages: fb-watchman@2.0.2: resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + ffmpeg-static@5.3.0: + resolution: {integrity: sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==} + engines: {node: '>=16'} + file-type@21.3.0: resolution: {integrity: sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA==} engines: {node: '>=20'} @@ -1896,9 +2090,16 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-response-object@3.0.2: + resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==} + http-status-codes@2.3.0: resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2465,6 +2666,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-cache-control@1.0.1: + resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==} + parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -2639,6 +2843,10 @@ packages: typescript: optional: true + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + proper-lockfile@4.1.2: resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} @@ -2778,6 +2986,10 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + sharp@0.34.5: + resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3491,6 +3703,13 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@derhuerst/http-basic@8.2.4': + dependencies: + caseless: 0.12.0 + concat-stream: 2.0.0 + http-response-object: 3.0.2 + parse-cache-control: 1.0.1 + '@electric-sql/pglite-socket@0.0.20(@electric-sql/pglite@0.3.15)': dependencies: '@electric-sql/pglite': 0.3.15 @@ -3521,6 +3740,102 @@ snapshots: dependencies: hono: 4.11.4 + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.2.4 + optional: true + + '@img/sharp-darwin-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.2.4 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-darwin-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-arm@1.2.4': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.2.4': + optional: true + + '@img/sharp-libvips-linux-s390x@1.2.4': + optional: true + + '@img/sharp-libvips-linux-x64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.2.4': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.2.4': + optional: true + + '@img/sharp-linux-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.2.4 + optional: true + + '@img/sharp-linux-arm@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.2.4 + optional: true + + '@img/sharp-linux-ppc64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.2.4 + optional: true + + '@img/sharp-linux-riscv64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.2.4 + optional: true + + '@img/sharp-linux-s390x@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.2.4 + optional: true + + '@img/sharp-linux-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-arm64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + optional: true + + '@img/sharp-linuxmusl-x64@0.34.5': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + optional: true + + '@img/sharp-wasm32@0.34.5': + dependencies: + '@emnapi/runtime': 1.9.0 + optional: true + + '@img/sharp-win32-arm64@0.34.5': + optional: true + + '@img/sharp-win32-ia32@0.34.5': + optional: true + + '@img/sharp-win32-x64@0.34.5': + optional: true + '@inquirer/ansi@1.0.2': {} '@inquirer/checkbox@4.3.2(@types/node@22.19.15)': @@ -4247,6 +4562,12 @@ snapshots: '@types/ms@2.1.0': {} + '@types/multer@2.1.0': + dependencies: + '@types/express': 5.0.6 + + '@types/node@10.17.60': {} + '@types/node@22.19.15': dependencies: undici-types: 6.21.0 @@ -4452,6 +4773,12 @@ snapshots: acorn@8.16.0: {} + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -4699,6 +5026,8 @@ snapshots: caniuse-lite@1.0.30001778: {} + caseless@0.12.0: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -4864,6 +5193,8 @@ snapshots: destr@2.0.5: {} + detect-libc@2.1.2: {} + detect-newline@3.1.0: {} dezalgo@1.0.4: @@ -4913,6 +5244,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + env-paths@2.2.1: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -5035,6 +5368,15 @@ snapshots: dependencies: bser: 2.1.1 + ffmpeg-static@5.3.0: + dependencies: + '@derhuerst/http-basic': 8.2.4 + env-paths: 2.2.1 + https-proxy-agent: 5.0.1 + progress: 2.0.3 + transitivePeerDependencies: + - supports-color + file-type@21.3.0: dependencies: '@tokenizer/inflate': 0.4.1 @@ -5229,8 +5571,19 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-response-object@3.0.2: + dependencies: + '@types/node': 10.17.60 + http-status-codes@2.3.0: {} + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} iconv-lite@0.7.2: @@ -5926,6 +6279,8 @@ snapshots: dependencies: callsites: 3.1.0 + parse-cache-control@1.0.1: {} + parse-json@5.2.0: dependencies: '@babel/code-frame': 7.29.0 @@ -6076,6 +6431,8 @@ snapshots: - react - react-dom + progress@2.0.3: {} + proper-lockfile@4.1.2: dependencies: graceful-fs: 4.2.11 @@ -6223,6 +6580,37 @@ snapshots: setprototypeof@1.2.0: {} + sharp@0.34.5: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.7.4 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.34.5 + '@img/sharp-darwin-x64': 0.34.5 + '@img/sharp-libvips-darwin-arm64': 1.2.4 + '@img/sharp-libvips-darwin-x64': 1.2.4 + '@img/sharp-libvips-linux-arm': 1.2.4 + '@img/sharp-libvips-linux-arm64': 1.2.4 + '@img/sharp-libvips-linux-ppc64': 1.2.4 + '@img/sharp-libvips-linux-riscv64': 1.2.4 + '@img/sharp-libvips-linux-s390x': 1.2.4 + '@img/sharp-libvips-linux-x64': 1.2.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 + '@img/sharp-libvips-linuxmusl-x64': 1.2.4 + '@img/sharp-linux-arm': 0.34.5 + '@img/sharp-linux-arm64': 0.34.5 + '@img/sharp-linux-ppc64': 0.34.5 + '@img/sharp-linux-riscv64': 0.34.5 + '@img/sharp-linux-s390x': 0.34.5 + '@img/sharp-linux-x64': 0.34.5 + '@img/sharp-linuxmusl-arm64': 0.34.5 + '@img/sharp-linuxmusl-x64': 0.34.5 + '@img/sharp-wasm32': 0.34.5 + '@img/sharp-win32-arm64': 0.34.5 + '@img/sharp-win32-ia32': 0.34.5 + '@img/sharp-win32-x64': 0.34.5 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..f44389a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +onlyBuiltDependencies: + - ffmpeg-static + - sharp diff --git a/prisma/migrations/20260319153000_add_patient_creator_fields/migration.sql b/prisma/migrations/20260319153000_add_patient_creator_fields/migration.sql new file mode 100644 index 0000000..156f297 --- /dev/null +++ b/prisma/migrations/20260319153000_add_patient_creator_fields/migration.sql @@ -0,0 +1,19 @@ +-- AlterTable +ALTER TABLE "Patient" +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "creatorId" INTEGER; + +-- Backfill +UPDATE "Patient" +SET "creatorId" = "doctorId" +WHERE "creatorId" IS NULL; + +-- AlterTable +ALTER TABLE "Patient" +ALTER COLUMN "creatorId" SET NOT NULL; + +-- CreateIndex +CREATE INDEX "Patient_creatorId_idx" ON "Patient"("creatorId"); + +-- AddForeignKey +ALTER TABLE "Patient" ADD CONSTRAINT "Patient_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20260319164000_remove_device_sn_code/migration.sql b/prisma/migrations/20260319164000_remove_device_sn_code/migration.sql new file mode 100644 index 0000000..6e88381 --- /dev/null +++ b/prisma/migrations/20260319164000_remove_device_sn_code/migration.sql @@ -0,0 +1,5 @@ +-- DropIndex +DROP INDEX IF EXISTS "Device_snCode_key"; + +-- AlterTable +ALTER TABLE "Device" DROP COLUMN "snCode"; diff --git a/prisma/migrations/20260319164701_pressure_label_strings_and_surgeon_refactor/migration.sql b/prisma/migrations/20260319164701_pressure_label_strings_and_surgeon_refactor/migration.sql new file mode 100644 index 0000000..c6c1f32 --- /dev/null +++ b/prisma/migrations/20260319164701_pressure_label_strings_and_surgeon_refactor/migration.sql @@ -0,0 +1,19 @@ +-- AlterTable +ALTER TABLE "Device" ALTER COLUMN "currentPressure" SET DATA TYPE TEXT, +ALTER COLUMN "initialPressure" SET DATA TYPE TEXT; + +-- AlterTable +ALTER TABLE "ImplantCatalog" ALTER COLUMN "pressureLevels" SET DATA TYPE TEXT[]; + +-- AlterTable +ALTER TABLE "PatientSurgery" ADD COLUMN "surgeonId" INTEGER; + +-- AlterTable +ALTER TABLE "TaskItem" ALTER COLUMN "oldPressure" SET DATA TYPE TEXT, +ALTER COLUMN "targetPressure" SET DATA TYPE TEXT; + +-- CreateIndex +CREATE INDEX "PatientSurgery_surgeonId_idx" ON "PatientSurgery"("surgeonId"); + +-- AddForeignKey +ALTER TABLE "PatientSurgery" ADD CONSTRAINT "PatientSurgery_surgeonId_fkey" FOREIGN KEY ("surgeonId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20260319184610_add_upload_assets_library/migration.sql b/prisma/migrations/20260319184610_add_upload_assets_library/migration.sql new file mode 100644 index 0000000..a06bccf --- /dev/null +++ b/prisma/migrations/20260319184610_add_upload_assets_library/migration.sql @@ -0,0 +1,34 @@ +-- CreateEnum +CREATE TYPE "UploadAssetType" AS ENUM ('IMAGE', 'VIDEO', 'FILE'); + +-- CreateTable +CREATE TABLE "UploadAsset" ( + "id" SERIAL NOT NULL, + "hospitalId" INTEGER NOT NULL, + "creatorId" INTEGER NOT NULL, + "type" "UploadAssetType" NOT NULL, + "originalName" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "storagePath" TEXT NOT NULL, + "url" TEXT NOT NULL, + "mimeType" TEXT NOT NULL, + "fileSize" INTEGER NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "UploadAsset_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "UploadAsset_storagePath_key" ON "UploadAsset"("storagePath"); + +-- CreateIndex +CREATE INDEX "UploadAsset_hospitalId_type_createdAt_idx" ON "UploadAsset"("hospitalId", "type", "createdAt"); + +-- CreateIndex +CREATE INDEX "UploadAsset_creatorId_createdAt_idx" ON "UploadAsset"("creatorId", "createdAt"); + +-- AddForeignKey +ALTER TABLE "UploadAsset" ADD CONSTRAINT "UploadAsset_hospitalId_fkey" FOREIGN KEY ("hospitalId") REFERENCES "Hospital"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UploadAsset" ADD CONSTRAINT "UploadAsset_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 5233423..38d7018 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -46,6 +46,13 @@ enum DictionaryType { DISTAL_SHUNT_DIRECTION } +// 上传资产类型:用于图库/视频库分类。 +enum UploadAssetType { + IMAGE + VIDEO + FILE +} + // 医院主表:多租户顶层实体。 model Hospital { id Int @id @default(autoincrement()) @@ -54,6 +61,7 @@ model Hospital { users User[] patients Patient[] tasks Task[] + uploads UploadAsset[] } // 科室表:归属于医院。 @@ -81,25 +89,28 @@ model Group { // 用户表:支持后台密码登录与小程序 openId。 model User { - id Int @id @default(autoincrement()) - name String - phone String + id Int @id @default(autoincrement()) + name String + phone String // 后台登录密码哈希(bcrypt)。 - passwordHash String? + passwordHash String? // 该时间点之前签发的 token 一律失效。 - tokenValidAfter DateTime @default(now()) - openId String? @unique - role Role - hospitalId Int? - departmentId Int? - groupId Int? - hospital Hospital? @relation(fields: [hospitalId], references: [id]) - department Department? @relation(fields: [departmentId], references: [id]) + tokenValidAfter DateTime @default(now()) + openId String? @unique + role Role + hospitalId Int? + departmentId Int? + groupId Int? + hospital Hospital? @relation(fields: [hospitalId], references: [id]) + department Department? @relation(fields: [departmentId], references: [id]) // 小组删除必须先清理成员,避免静默把用户 groupId 置空。 - group Group? @relation(fields: [groupId], references: [id], onDelete: Restrict) - doctorPatients Patient[] @relation("DoctorPatients") - createdTasks Task[] @relation("TaskCreator") - acceptedTasks Task[] @relation("TaskEngineer") + group Group? @relation(fields: [groupId], references: [id], onDelete: Restrict) + doctorPatients Patient[] @relation("DoctorPatients") + createdPatients Patient[] @relation("PatientCreator") + createdTasks Task[] @relation("TaskCreator") + acceptedTasks Task[] @relation("TaskEngineer") + surgeonSurgeries PatientSurgery[] @relation("SurgerySurgeon") + createdUploads UploadAsset[] @relation("UploadCreator") @@unique([phone, role, hospitalId]) @@index([phone]) @@ -112,6 +123,7 @@ model User { model Patient { id Int @id @default(autoincrement()) name String + createdAt DateTime @default(now()) // 住院号:用于院内患者检索与病案关联。 inpatientNo String? // 项目名称:用于区分患者所属项目/课题。 @@ -121,14 +133,17 @@ model Patient { idCard String hospitalId Int doctorId Int + creatorId Int hospital Hospital @relation(fields: [hospitalId], references: [id]) doctor User @relation("DoctorPatients", fields: [doctorId], references: [id]) + creator User @relation("PatientCreator", fields: [creatorId], references: [id]) surgeries PatientSurgery[] devices Device[] @@index([phone, idCard]) @@index([hospitalId, doctorId]) @@index([inpatientNo]) + @@index([creatorId]) } // 患者手术表:保存每次分流/复手术档案。 @@ -137,6 +152,7 @@ model PatientSurgery { patientId Int surgeryDate DateTime surgeryName String + surgeonId Int? surgeonName String // 术前测压:部分患者可为空。 preOpPressure Int? @@ -151,9 +167,11 @@ model PatientSurgery { notes String? createdAt DateTime @default(now()) patient Patient @relation(fields: [patientId], references: [id], onDelete: Cascade) + surgeon User? @relation("SurgerySurgeon", fields: [surgeonId], references: [id], onDelete: SetNull) devices Device[] @@index([patientId, surgeryDate]) + @@index([surgeonId]) } // 植入物型号字典:供前端单选型号后自动回填厂家与名称。 @@ -163,7 +181,7 @@ model ImplantCatalog { manufacturer String name String // 可调压器械的可选挡位,由系统管理员维护。 - pressureLevels Int[] @default([]) + pressureLevels String[] @default([]) isPressureAdjustable Boolean @default(true) notes String? createdAt DateTime @default(now()) @@ -185,11 +203,30 @@ model DictionaryItem { @@index([type, enabled, sortOrder]) } +// 上传资产表:保存图片/视频/文件元数据,供图库与患者表单复用。 +model UploadAsset { + id Int @id @default(autoincrement()) + hospitalId Int + creatorId Int + type UploadAssetType + originalName String + fileName String + storagePath String @unique + url String + mimeType String + fileSize Int + createdAt DateTime @default(now()) + hospital Hospital @relation(fields: [hospitalId], references: [id]) + creator User @relation("UploadCreator", fields: [creatorId], references: [id]) + + @@index([hospitalId, type, createdAt]) + @@index([creatorId, createdAt]) +} + // 设备表:每次手术植入的设备实例,保留当前压力与历史调压记录。 model Device { id Int @id @default(autoincrement()) - snCode String @unique - currentPressure Int + currentPressure String status DeviceStatus @default(ACTIVE) patientId Int surgeryId Int? @@ -205,7 +242,7 @@ model Device { proximalPunctureAreas String[] @default([]) valvePlacementSites String[] @default([]) distalShuntDirection String? - initialPressure Int? + initialPressure String? implantNotes String? labelImageUrl String? patient Patient @relation(fields: [patientId], references: [id]) @@ -240,8 +277,8 @@ model TaskItem { id Int @id @default(autoincrement()) taskId Int deviceId Int - oldPressure Int - targetPressure Int + oldPressure String + targetPressure String task Task @relation(fields: [taskId], references: [id], onDelete: Cascade) device Device @relation(fields: [deviceId], references: [id]) diff --git a/prisma/seed.mjs b/prisma/seed.mjs index 03b46b5..fc5eafe 100644 --- a/prisma/seed.mjs +++ b/prisma/seed.mjs @@ -64,6 +64,7 @@ async function upsertUserByOpenId(openId, data) { async function ensurePatient({ hospitalId, doctorId, + creatorId, name, inpatientNo = null, projectName = null, @@ -81,13 +82,14 @@ async function ensurePatient({ if (existing) { if ( existing.doctorId !== doctorId || + existing.creatorId !== creatorId || existing.name !== name || existing.inpatientNo !== inpatientNo || existing.projectName !== projectName ) { return prisma.patient.update({ where: { id: existing.id }, - data: { doctorId, name, inpatientNo, projectName }, + data: { doctorId, creatorId, name, inpatientNo, projectName }, }); } return existing; @@ -97,6 +99,7 @@ async function ensurePatient({ data: { hospitalId, doctorId, + creatorId, name, inpatientNo, projectName, @@ -216,6 +219,63 @@ async function ensurePatientSurgery({ }); } +async function ensureDevice({ + patientId, + surgeryId, + implantCatalogId, + currentPressure, + status, + implantModel, + implantManufacturer, + implantName, + isPressureAdjustable, + isAbandoned, + shuntMode, + proximalPunctureAreas, + valvePlacementSites, + distalShuntDirection, + initialPressure, + implantNotes, + labelImageUrl, +}) { + const existing = await prisma.device.findFirst({ + where: { + patientId, + surgeryId, + implantNotes, + }, + }); + + const data = { + patientId, + surgeryId, + implantCatalogId, + currentPressure, + status, + implantModel, + implantManufacturer, + implantName, + isPressureAdjustable, + isAbandoned, + shuntMode, + proximalPunctureAreas, + valvePlacementSites, + distalShuntDirection, + initialPressure, + implantNotes, + labelImageUrl, + }; + + if (existing) { + return prisma.device.update({ + where: { id: existing.id }, + data, + }); + } + + return prisma.device.create({ data }); +} + async function main() { const seedPasswordHash = await hash(SEED_PASSWORD_PLAIN, 12); @@ -395,6 +455,7 @@ async function main() { const patientA1 = await ensurePatient({ hospitalId: hospitalA.id, doctorId: doctorA.id, + creatorId: doctorA.id, name: 'Seed Patient A1', inpatientNo: 'ZYH-A-0001', projectName: '脑积水随访项目-A', @@ -405,6 +466,7 @@ async function main() { const patientA2 = await ensurePatient({ hospitalId: hospitalA.id, doctorId: doctorA2.id, + creatorId: doctorA2.id, name: 'Seed Patient A2', inpatientNo: 'ZYH-A-0002', projectName: '脑积水随访项目-A', @@ -415,6 +477,7 @@ async function main() { const patientA3 = await ensurePatient({ hospitalId: hospitalA.id, doctorId: doctorA3.id, + creatorId: doctorA3.id, name: 'Seed Patient A3', inpatientNo: 'ZYH-A-0003', projectName: '脑积水随访项目-A', @@ -425,6 +488,7 @@ async function main() { const patientB1 = await ensurePatient({ hospitalId: hospitalB.id, doctorId: doctorB.id, + creatorId: doctorB.id, name: 'Seed Patient B1', inpatientNo: 'ZYH-B-0001', projectName: '脑积水随访项目-B', @@ -510,219 +574,104 @@ async function main() { hydrocephalusTypes: ['高压性'], }); - const deviceA1 = await prisma.device.upsert({ - where: { snCode: 'SEED-SN-A-001' }, - update: { - patientId: patientA1.id, - surgeryId: surgeryA1New.id, - implantCatalogId: adjustableCatalog.id, - currentPressure: 118, - status: DeviceStatus.ACTIVE, - implantModel: adjustableCatalog.modelCode, - implantManufacturer: adjustableCatalog.manufacturer, - implantName: adjustableCatalog.name, - isPressureAdjustable: adjustableCatalog.isPressureAdjustable, - isAbandoned: false, - shuntMode: 'VPS', - proximalPunctureAreas: ['额角'], - valvePlacementSites: ['耳后'], - distalShuntDirection: '腹腔', - initialPressure: 118, - implantNotes: 'Seed A1 当前在用设备', - labelImageUrl: 'https://seed.example.com/labels/a1-001.jpg', - }, - create: { - snCode: 'SEED-SN-A-001', - patientId: patientA1.id, - surgeryId: surgeryA1New.id, - implantCatalogId: adjustableCatalog.id, - currentPressure: 118, - status: DeviceStatus.ACTIVE, - implantModel: adjustableCatalog.modelCode, - implantManufacturer: adjustableCatalog.manufacturer, - implantName: adjustableCatalog.name, - isPressureAdjustable: adjustableCatalog.isPressureAdjustable, - isAbandoned: false, - shuntMode: 'VPS', - proximalPunctureAreas: ['额角'], - valvePlacementSites: ['耳后'], - distalShuntDirection: '腹腔', - initialPressure: 118, - implantNotes: 'Seed A1 当前在用设备', - labelImageUrl: 'https://seed.example.com/labels/a1-001.jpg', - }, + const deviceA1 = await ensureDevice({ + patientId: patientA1.id, + surgeryId: surgeryA1New.id, + implantCatalogId: adjustableCatalog.id, + currentPressure: 118, + status: DeviceStatus.ACTIVE, + implantModel: adjustableCatalog.modelCode, + implantManufacturer: adjustableCatalog.manufacturer, + implantName: adjustableCatalog.name, + isPressureAdjustable: adjustableCatalog.isPressureAdjustable, + isAbandoned: false, + shuntMode: 'VPS', + proximalPunctureAreas: ['额角'], + valvePlacementSites: ['耳后'], + distalShuntDirection: '腹腔', + initialPressure: 118, + implantNotes: 'Seed A1 当前在用设备', + labelImageUrl: 'https://seed.example.com/labels/a1-001.jpg', }); - const deviceA2 = await prisma.device.upsert({ - where: { snCode: 'SEED-SN-A-002' }, - update: { - patientId: patientA2.id, - surgeryId: surgeryA2.id, - implantCatalogId: adjustableCatalog.id, - currentPressure: 112, - status: DeviceStatus.ACTIVE, - implantModel: adjustableCatalog.modelCode, - implantManufacturer: adjustableCatalog.manufacturer, - implantName: adjustableCatalog.name, - isPressureAdjustable: adjustableCatalog.isPressureAdjustable, - isAbandoned: false, - shuntMode: 'VPS', - proximalPunctureAreas: ['枕角'], - valvePlacementSites: ['胸前'], - distalShuntDirection: '腹腔', - initialPressure: 112, - implantNotes: 'Seed A2 当前在用设备', - labelImageUrl: 'https://seed.example.com/labels/a2-002.jpg', - }, - create: { - snCode: 'SEED-SN-A-002', - patientId: patientA2.id, - surgeryId: surgeryA2.id, - implantCatalogId: adjustableCatalog.id, - currentPressure: 112, - status: DeviceStatus.ACTIVE, - implantModel: adjustableCatalog.modelCode, - implantManufacturer: adjustableCatalog.manufacturer, - implantName: adjustableCatalog.name, - isPressureAdjustable: adjustableCatalog.isPressureAdjustable, - isAbandoned: false, - shuntMode: 'VPS', - proximalPunctureAreas: ['枕角'], - valvePlacementSites: ['胸前'], - distalShuntDirection: '腹腔', - initialPressure: 112, - implantNotes: 'Seed A2 当前在用设备', - labelImageUrl: 'https://seed.example.com/labels/a2-002.jpg', - }, + const deviceA2 = await ensureDevice({ + patientId: patientA2.id, + surgeryId: surgeryA2.id, + implantCatalogId: adjustableCatalog.id, + currentPressure: 112, + status: DeviceStatus.ACTIVE, + implantModel: adjustableCatalog.modelCode, + implantManufacturer: adjustableCatalog.manufacturer, + implantName: adjustableCatalog.name, + isPressureAdjustable: adjustableCatalog.isPressureAdjustable, + isAbandoned: false, + shuntMode: 'VPS', + proximalPunctureAreas: ['枕角'], + valvePlacementSites: ['胸前'], + distalShuntDirection: '腹腔', + initialPressure: 112, + implantNotes: 'Seed A2 当前在用设备', + labelImageUrl: 'https://seed.example.com/labels/a2-002.jpg', }); - await prisma.device.upsert({ - where: { snCode: 'SEED-SN-A-003' }, - update: { - patientId: patientA3.id, - surgeryId: surgeryA3.id, - implantCatalogId: adjustableCatalog.id, - currentPressure: 109, - status: DeviceStatus.ACTIVE, - implantModel: adjustableCatalog.modelCode, - implantManufacturer: adjustableCatalog.manufacturer, - implantName: adjustableCatalog.name, - isPressureAdjustable: adjustableCatalog.isPressureAdjustable, - isAbandoned: false, - shuntMode: 'LPS', - proximalPunctureAreas: ['腰穿'], - valvePlacementSites: ['腰背部'], - distalShuntDirection: '腹腔', - initialPressure: 109, - implantNotes: 'Seed A3 当前在用设备', - labelImageUrl: 'https://seed.example.com/labels/a3-003.jpg', - }, - create: { - snCode: 'SEED-SN-A-003', - patientId: patientA3.id, - surgeryId: surgeryA3.id, - implantCatalogId: adjustableCatalog.id, - currentPressure: 109, - status: DeviceStatus.ACTIVE, - implantModel: adjustableCatalog.modelCode, - implantManufacturer: adjustableCatalog.manufacturer, - implantName: adjustableCatalog.name, - isPressureAdjustable: adjustableCatalog.isPressureAdjustable, - isAbandoned: false, - shuntMode: 'LPS', - proximalPunctureAreas: ['腰穿'], - valvePlacementSites: ['腰背部'], - distalShuntDirection: '腹腔', - initialPressure: 109, - implantNotes: 'Seed A3 当前在用设备', - labelImageUrl: 'https://seed.example.com/labels/a3-003.jpg', - }, + await ensureDevice({ + patientId: patientA3.id, + surgeryId: surgeryA3.id, + implantCatalogId: adjustableCatalog.id, + currentPressure: 109, + status: DeviceStatus.ACTIVE, + implantModel: adjustableCatalog.modelCode, + implantManufacturer: adjustableCatalog.manufacturer, + implantName: adjustableCatalog.name, + isPressureAdjustable: adjustableCatalog.isPressureAdjustable, + isAbandoned: false, + shuntMode: 'LPS', + proximalPunctureAreas: ['腰穿'], + valvePlacementSites: ['腰背部'], + distalShuntDirection: '腹腔', + initialPressure: 109, + implantNotes: 'Seed A3 当前在用设备', + labelImageUrl: 'https://seed.example.com/labels/a3-003.jpg', }); - const deviceB1 = await prisma.device.upsert({ - where: { snCode: 'SEED-SN-B-001' }, - update: { - patientId: patientB1.id, - surgeryId: surgeryB1.id, - implantCatalogId: adjustableCatalog.id, - currentPressure: 121, - status: DeviceStatus.ACTIVE, - implantModel: adjustableCatalog.modelCode, - implantManufacturer: adjustableCatalog.manufacturer, - implantName: adjustableCatalog.name, - isPressureAdjustable: adjustableCatalog.isPressureAdjustable, - isAbandoned: false, - shuntMode: 'VPS', - proximalPunctureAreas: ['额角'], - valvePlacementSites: ['耳后'], - distalShuntDirection: '腹腔', - initialPressure: 121, - implantNotes: 'Seed B1 当前在用设备', - labelImageUrl: 'https://seed.example.com/labels/b1-001.jpg', - }, - create: { - snCode: 'SEED-SN-B-001', - patientId: patientB1.id, - surgeryId: surgeryB1.id, - implantCatalogId: adjustableCatalog.id, - currentPressure: 121, - status: DeviceStatus.ACTIVE, - implantModel: adjustableCatalog.modelCode, - implantManufacturer: adjustableCatalog.manufacturer, - implantName: adjustableCatalog.name, - isPressureAdjustable: adjustableCatalog.isPressureAdjustable, - isAbandoned: false, - shuntMode: 'VPS', - proximalPunctureAreas: ['额角'], - valvePlacementSites: ['耳后'], - distalShuntDirection: '腹腔', - initialPressure: 121, - implantNotes: 'Seed B1 当前在用设备', - labelImageUrl: 'https://seed.example.com/labels/b1-001.jpg', - }, + const deviceB1 = await ensureDevice({ + patientId: patientB1.id, + surgeryId: surgeryB1.id, + implantCatalogId: adjustableCatalog.id, + currentPressure: 121, + status: DeviceStatus.ACTIVE, + implantModel: adjustableCatalog.modelCode, + implantManufacturer: adjustableCatalog.manufacturer, + implantName: adjustableCatalog.name, + isPressureAdjustable: adjustableCatalog.isPressureAdjustable, + isAbandoned: false, + shuntMode: 'VPS', + proximalPunctureAreas: ['额角'], + valvePlacementSites: ['耳后'], + distalShuntDirection: '腹腔', + initialPressure: 121, + implantNotes: 'Seed B1 当前在用设备', + labelImageUrl: 'https://seed.example.com/labels/b1-001.jpg', }); - await prisma.device.upsert({ - where: { snCode: 'SEED-SN-A-004' }, - update: { - patientId: patientA1.id, - surgeryId: surgeryA1Old.id, - implantCatalogId: adjustableCatalog.id, - currentPressure: 130, - status: DeviceStatus.INACTIVE, - implantModel: adjustableCatalog.modelCode, - implantManufacturer: adjustableCatalog.manufacturer, - implantName: adjustableCatalog.name, - isPressureAdjustable: adjustableCatalog.isPressureAdjustable, - isAbandoned: true, - shuntMode: 'VPS', - proximalPunctureAreas: ['额角'], - valvePlacementSites: ['耳后'], - distalShuntDirection: '腹腔', - initialPressure: 130, - implantNotes: 'Seed A1 弃用历史设备', - labelImageUrl: 'https://seed.example.com/labels/a1-004.jpg', - }, - create: { - snCode: 'SEED-SN-A-004', - patientId: patientA1.id, - surgeryId: surgeryA1Old.id, - implantCatalogId: adjustableCatalog.id, - currentPressure: 130, - status: DeviceStatus.INACTIVE, - implantModel: adjustableCatalog.modelCode, - implantManufacturer: adjustableCatalog.manufacturer, - implantName: adjustableCatalog.name, - isPressureAdjustable: adjustableCatalog.isPressureAdjustable, - isAbandoned: true, - shuntMode: 'VPS', - proximalPunctureAreas: ['额角'], - valvePlacementSites: ['耳后'], - distalShuntDirection: '腹腔', - initialPressure: 130, - implantNotes: 'Seed A1 弃用历史设备', - labelImageUrl: 'https://seed.example.com/labels/a1-004.jpg', - }, + await ensureDevice({ + patientId: patientA1.id, + surgeryId: surgeryA1Old.id, + implantCatalogId: adjustableCatalog.id, + currentPressure: 130, + status: DeviceStatus.INACTIVE, + implantModel: adjustableCatalog.modelCode, + implantManufacturer: adjustableCatalog.manufacturer, + implantName: adjustableCatalog.name, + isPressureAdjustable: adjustableCatalog.isPressureAdjustable, + isAbandoned: true, + shuntMode: 'VPS', + proximalPunctureAreas: ['额角'], + valvePlacementSites: ['耳后'], + distalShuntDirection: '腹腔', + initialPressure: 130, + implantNotes: 'Seed A1 弃用历史设备', + labelImageUrl: 'https://seed.example.com/labels/a1-004.jpg', }); // 清理与种子设备关联的历史任务,保证 seed 可重复执行且生命周期夹具稳定。 diff --git a/src/app.module.ts b/src/app.module.ts index e25179b..504ad66 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,6 +9,7 @@ import { OrganizationModule } from './organization/organization.module.js'; import { NotificationsModule } from './notifications/notifications.module.js'; import { DevicesModule } from './devices/devices.module.js'; import { DictionariesModule } from './dictionaries/dictionaries.module.js'; +import { UploadsModule } from './uploads/uploads.module.js'; @Module({ imports: [ @@ -22,6 +23,7 @@ import { DictionariesModule } from './dictionaries/dictionaries.module.js'; NotificationsModule, DevicesModule, DictionariesModule, + UploadsModule, ], }) export class AppModule {} diff --git a/src/common/messages.ts b/src/common/messages.ts index e5d1d06..3facf45 100644 --- a/src/common/messages.ts +++ b/src/common/messages.ts @@ -58,19 +58,22 @@ export const MESSAGES = { '检测到多个同手机号账号,请传 hospitalId 指定登录医院', CREATE_FORBIDDEN: '当前角色无权限创建该用户', HOSPITAL_ADMIN_SCOPE_FORBIDDEN: '医院管理员仅可操作本院非管理员账号', - DIRECTOR_SCOPE_FORBIDDEN: '科室主任仅可操作本科室医生账号', + DIRECTOR_SCOPE_FORBIDDEN: '科室主任仅可操作本科室医生或组长账号', }, TASK: { ITEMS_REQUIRED: '任务明细 items 不能为空', DEVICE_NOT_FOUND: '存在设备不在当前医院或设备不存在', + DEVICE_MULTI_HOSPITAL: '同一批调压任务中的设备必须属于同一家医院', + ENGINEER_REQUIRED: '接收工程师必选', ENGINEER_INVALID: '工程师必须为当前医院有效工程师', TASK_NOT_FOUND: '任务不存在或不属于当前医院', - ACCEPT_ONLY_PENDING: '仅待接收任务可执行接收', - COMPLETE_ONLY_ACCEPTED: '仅已接收任务可执行完成', - CANCEL_ONLY_PENDING_ACCEPTED: '仅待接收/已接收任务可取消', - ENGINEER_ALREADY_ASSIGNED: '任务已被其他工程师接收', - ENGINEER_ONLY_ASSIGNEE: '仅任务接收工程师可完成任务', + ACCEPT_DISABLED: '当前流程不支持工程师接收,请由创建人直接指定接收工程师', + ACCEPT_ONLY_PENDING: '仅待指派任务可执行接收', + COMPLETE_ONLY_ACCEPTED: '仅已指派任务可执行完成', + CANCEL_ONLY_PENDING_ACCEPTED: '仅待指派/已指派任务可取消', + ENGINEER_ALREADY_ASSIGNED: '任务已指派给其他工程师', + ENGINEER_ONLY_ASSIGNEE: '仅任务接收人可完成任务', CANCEL_ONLY_CREATOR: '仅任务创建者可取消任务', ACTOR_ROLE_FORBIDDEN: '当前角色无权限执行该任务操作', ACTOR_HOSPITAL_REQUIRED: '当前登录上下文缺少医院信息', @@ -99,9 +102,7 @@ export const MESSAGES = { DEVICE: { NOT_FOUND: '设备不存在或无权限访问', - SN_CODE_REQUIRED: 'snCode 不能为空', - SN_CODE_DUPLICATE: '设备 SN 已存在', - CURRENT_PRESSURE_INVALID: 'currentPressure 必须为大于等于 0 的整数', + CURRENT_PRESSURE_INVALID: 'currentPressure 必须是合法挡位标签', STATUS_INVALID: '设备状态不合法', PATIENT_REQUIRED: 'patientId 必填且必须为整数', PATIENT_NOT_FOUND: '归属患者不存在', @@ -123,6 +124,17 @@ export const MESSAGES = { SYSTEM_ADMIN_ONLY_MAINTAIN: '仅系统管理员可维护字典', }, + UPLOAD: { + FILE_REQUIRED: '请先选择要上传的文件', + UNSUPPORTED_FILE_TYPE: '仅支持图片、视频、PDF/Office 文档上传', + ACTOR_HOSPITAL_REQUIRED: '当前登录上下文缺少医院信息,无法上传文件', + SYSTEM_ADMIN_HOSPITAL_REQUIRED: + '系统管理员上传文件时必须显式指定 hospitalId', + INVALID_IMAGE_FILE: '上传的图片无法解析或压缩失败', + INVALID_VIDEO_FILE: '上传的视频无法解析或压缩失败', + FFMPEG_NOT_AVAILABLE: '服务端缺少视频压缩能力', + }, + ORG: { HOSPITAL_NOT_FOUND: '医院不存在', DEPARTMENT_NOT_FOUND: '科室不存在', diff --git a/src/common/pressure-level.util.ts b/src/common/pressure-level.util.ts new file mode 100644 index 0000000..151d64a --- /dev/null +++ b/src/common/pressure-level.util.ts @@ -0,0 +1,56 @@ +import { BadRequestException } from '@nestjs/common'; + +/** + * 挡位标签标准化:将输入统一整理为可稳定比较和展示的字符串。 + */ +export function normalizePressureLabel(value: unknown, fieldName: string) { + const raw = + typeof value === 'string' || typeof value === 'number' + ? String(value).trim() + : ''; + + if (!raw) { + throw new BadRequestException(`${fieldName} 不能为空`); + } + if (!/^\d+(\.\d+)?$/.test(raw)) { + throw new BadRequestException(`${fieldName} 必须是合法挡位标签`); + } + + const [integerPart, fractionPart = ''] = raw.split('.'); + const normalizedInteger = integerPart.replace(/^0+(?=\d)/, '') || '0'; + const normalizedFraction = fractionPart.replace(/0+$/, ''); + + return normalizedFraction + ? `${normalizedInteger}.${normalizedFraction}` + : normalizedInteger; +} + +/** + * 挡位标签比较:按数值大小排序,数值相同再按标准字符串比较。 + */ +export function comparePressureLabel(left: string, right: string) { + const leftNumber = Number(left); + const rightNumber = Number(right); + + if (leftNumber !== rightNumber) { + return leftNumber - rightNumber; + } + + return left.localeCompare(right, 'en'); +} + +/** + * 挡位列表标准化:去重、排序。 + */ +export function normalizePressureLabelList( + values: unknown[] | undefined, + fieldName: string, +) { + if (!Array.isArray(values) || values.length === 0) { + return []; + } + + return Array.from( + new Set(values.map((value) => normalizePressureLabel(value, fieldName))), + ).sort(comparePressureLabel); +} diff --git a/src/departments/departments.controller.ts b/src/departments/departments.controller.ts index 43d0149..7274313 100644 --- a/src/departments/departments.controller.ts +++ b/src/departments/departments.controller.ts @@ -55,7 +55,13 @@ export class DepartmentsController { * 查询科室列表。 */ @Get() - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER) + @Roles( + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DIRECTOR, + Role.LEADER, + Role.DOCTOR, + ) @ApiOperation({ summary: '查询科室列表' }) @ApiQuery({ name: 'hospitalId', required: false, description: '医院 ID' }) findAll( @@ -69,7 +75,13 @@ export class DepartmentsController { * 查询科室详情。 */ @Get(':id') - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER) + @Roles( + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DIRECTOR, + Role.LEADER, + Role.DOCTOR, + ) @ApiOperation({ summary: '查询科室详情' }) @ApiParam({ name: 'id', description: '科室 ID' }) findOne( @@ -83,7 +95,7 @@ export class DepartmentsController { * 更新科室。 */ @Patch(':id') - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) @ApiOperation({ summary: '更新科室' }) update( @CurrentActor() actor: ActorContext, diff --git a/src/departments/departments.service.ts b/src/departments/departments.service.ts index 3bc484d..ded560c 100644 --- a/src/departments/departments.service.ts +++ b/src/departments/departments.service.ts @@ -48,7 +48,7 @@ export class DepartmentsService { } /** - * 查询科室列表:院管限定本院;主任/组长限定本科室。 + * 查询科室列表:院管限定本院。 */ async findAll(actor: ActorContext, query: OrganizationQueryDto) { this.access.assertRole(actor, [ @@ -56,6 +56,7 @@ export class DepartmentsService { Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER, + Role.DOCTOR, ]); const paging = this.access.resolvePaging(query); const where: Prisma.DepartmentWhereInput = {}; @@ -66,7 +67,11 @@ export class DepartmentsService { if (actor.role === Role.HOSPITAL_ADMIN) { where.hospitalId = this.access.requireActorHospitalId(actor); - } else if (actor.role === Role.DIRECTOR || actor.role === Role.LEADER) { + } else if ( + actor.role === Role.DIRECTOR || + actor.role === Role.LEADER || + actor.role === Role.DOCTOR + ) { where.id = this.access.requireActorDepartmentId(actor); } else if (query.hospitalId != null) { where.hospitalId = this.access.toInt( @@ -93,7 +98,7 @@ export class DepartmentsService { } /** - * 查询科室详情:院管仅可查看本院;主任/组长仅可查看本科室。 + * 查询科室详情:院管仅可查看本院。 */ async findOne(actor: ActorContext, id: number) { this.access.assertRole(actor, [ @@ -101,6 +106,7 @@ export class DepartmentsService { Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER, + Role.DOCTOR, ]); const departmentId = this.access.toInt( id, @@ -118,18 +124,21 @@ export class DepartmentsService { } if (actor.role === Role.HOSPITAL_ADMIN) { this.access.assertHospitalScope(actor, department.hospitalId); - } - if (actor.role === Role.DIRECTOR || actor.role === Role.LEADER) { + } else if ( + actor.role === Role.DIRECTOR || + actor.role === Role.LEADER || + actor.role === Role.DOCTOR + ) { const actorDepartmentId = this.access.requireActorDepartmentId(actor); if (department.id !== actorDepartmentId) { - throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN); + throw new ForbiddenException(MESSAGES.ORG.HOSPITAL_ADMIN_SCOPE_INVALID); } } return department; } /** - * 更新科室:院管仅可修改本院;主任/组长仅可修改本科室。 + * 更新科室:院管仅可修改本院。 */ async update(actor: ActorContext, id: number, dto: UpdateDepartmentDto) { const current = await this.findOne(actor, id); diff --git a/src/devices/devices.service.ts b/src/devices/devices.service.ts index 80224b6..f7eec38 100644 --- a/src/devices/devices.service.ts +++ b/src/devices/devices.service.ts @@ -9,6 +9,10 @@ import { Prisma } from '../generated/prisma/client.js'; import { DeviceStatus, Role } from '../generated/prisma/enums.js'; import type { ActorContext } from '../common/actor-context.js'; import { MESSAGES } from '../common/messages.js'; +import { + normalizePressureLabelList, + normalizePressureLabel, +} from '../common/pressure-level.util.js'; import { PrismaService } from '../prisma.service.js'; import { CreateImplantCatalogDto } from './dto/create-implant-catalog.dto.js'; import { CreateDeviceDto } from './dto/create-device.dto.js'; @@ -133,15 +137,12 @@ export class DevicesService { async create(actor: ActorContext, dto: CreateDeviceDto) { this.assertAdmin(actor); - const snCode = this.normalizeSnCode(dto.snCode); const patient = await this.resolveWritablePatient(actor, dto.patientId); - await this.assertSnCodeUnique(snCode); return this.prisma.device.create({ data: { - snCode, // 当前压力只允许由调压任务流转维护,手工创建设备时先置 0。 - currentPressure: 0, + currentPressure: '0', status: dto.status ?? DeviceStatus.ACTIVE, patientId: patient.id, }, @@ -150,17 +151,12 @@ export class DevicesService { } /** - * 更新设备:允许修改 SN、状态和归属患者;当前压力仅由任务完成时更新。 + * 更新设备:允许修改状态和归属患者;当前压力仅由任务完成时更新。 */ async update(actor: ActorContext, id: number, dto: UpdateDeviceDto) { const current = await this.findOne(actor, id); const data: Prisma.DeviceUpdateInput = {}; - if (dto.snCode !== undefined) { - const snCode = this.normalizeSnCode(dto.snCode); - await this.assertSnCodeUnique(snCode, current.id); - data.snCode = snCode; - } if (dto.status !== undefined) { data.status = this.normalizeStatus(dto.status); } @@ -366,12 +362,6 @@ export class DevicesService { if (keyword) { andConditions.push({ OR: [ - { - snCode: { - contains: keyword, - mode: 'insensitive', - }, - }, { implantModel: { contains: keyword, @@ -590,13 +580,6 @@ export class DevicesService { return this.normalizeRequiredString(value, 'modelCode').toUpperCase(); } - /** - * 设备 SN 标准化:统一去空白并转大写,避免大小写重复。 - */ - private normalizeSnCode(value: unknown) { - return this.normalizeRequiredString(value, 'snCode').toUpperCase(); - } - private normalizeRequiredString(value: unknown, fieldName: string) { if (typeof value !== 'string') { throw new BadRequestException(`${fieldName} 必须是字符串`); @@ -622,41 +605,25 @@ export class DevicesService { * 挡位列表标准化:去重、排序,并在非可调压目录下自动清空。 */ private normalizePressureLevels( - pressureLevels: number[] | undefined, + pressureLevels: unknown[] | undefined, isPressureAdjustable: boolean, ) { if (!isPressureAdjustable) { return []; } - if (!Array.isArray(pressureLevels) || pressureLevels.length === 0) { - return []; - } - - return Array.from( - new Set( - pressureLevels.map((level) => { - const normalized = Number(level); - if (!Number.isInteger(normalized) || normalized < 0) { - throw new BadRequestException( - 'pressureLevels 必须为大于等于 0 的整数数组', - ); - } - return normalized; - }), - ), - ).sort((left, right) => left - right); + return normalizePressureLabelList(pressureLevels, 'pressureLevels'); } /** - * 压力值必须是非负整数。 + * 当前压力挡位标签标准化。 */ private normalizePressure(value: unknown) { - const parsed = Number(value); - if (!Number.isInteger(parsed) || parsed < 0) { + try { + return normalizePressureLabel(value, 'currentPressure'); + } catch { throw new BadRequestException(MESSAGES.DEVICE.CURRENT_PRESSURE_INVALID); } - return parsed; } /** @@ -693,18 +660,4 @@ export class DevicesService { } return actor.hospitalId; } - - /** - * 确保设备 SN 唯一;更新时允许命中自身。 - */ - private async assertSnCodeUnique(snCode: string, selfId?: number) { - const existing = await this.prisma.device.findUnique({ - where: { snCode }, - select: { id: true }, - }); - - if (existing && existing.id !== selfId) { - throw new ConflictException(MESSAGES.DEVICE.SN_CODE_DUPLICATE); - } - } } diff --git a/src/devices/dto/create-device.dto.ts b/src/devices/dto/create-device.dto.ts index 9c1605a..9db7ff9 100644 --- a/src/devices/dto/create-device.dto.ts +++ b/src/devices/dto/create-device.dto.ts @@ -1,16 +1,12 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { DeviceStatus } from '../../generated/prisma/enums.js'; import { Type } from 'class-transformer'; -import { IsEnum, IsInt, IsOptional, IsString, Min } from 'class-validator'; +import { IsEnum, IsInt, IsOptional, Min } from 'class-validator'; /** * 创建设备 DTO。 */ export class CreateDeviceDto { - @ApiProperty({ description: '设备 SN', example: 'TYT-SN-10001' }) - @IsString({ message: 'snCode 必须是字符串' }) - snCode!: string; - @ApiPropertyOptional({ description: '设备状态,默认 ACTIVE', enum: DeviceStatus, diff --git a/src/devices/dto/create-implant-catalog.dto.ts b/src/devices/dto/create-implant-catalog.dto.ts index c15578e..14b99f9 100644 --- a/src/devices/dto/create-implant-catalog.dto.ts +++ b/src/devices/dto/create-implant-catalog.dto.ts @@ -1,14 +1,11 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; import { ArrayMaxSize, IsArray, IsBoolean, - IsInt, IsOptional, IsString, MaxLength, - Min, } from 'class-validator'; import { ToBoolean } from '../../common/transforms/to-boolean.transform.js'; @@ -38,17 +35,15 @@ export class CreateImplantCatalogDto { name!: string; @ApiPropertyOptional({ - description: '可调压器械的挡位列表,按整数录入', - type: [Number], - example: [80, 100, 120, 140], + description: '可调压器械的挡位列表,按字符串挡位标签录入', + type: [String], + example: ['0.5', '1', '1.5'], }) @IsOptional() @IsArray({ message: 'pressureLevels 必须是数组' }) @ArrayMaxSize(30, { message: 'pressureLevels 最多 30 项' }) - @Type(() => Number) - @IsInt({ each: true, message: 'pressureLevels 必须为整数数组' }) - @Min(0, { each: true, message: 'pressureLevels 必须大于等于 0' }) - pressureLevels?: number[]; + @IsString({ each: true, message: 'pressureLevels 必须为字符串数组' }) + pressureLevels?: string[]; @ApiPropertyOptional({ description: '是否支持调压,默认 true', diff --git a/src/devices/dto/device-query.dto.ts b/src/devices/dto/device-query.dto.ts index b1f7550..7f0874b 100644 --- a/src/devices/dto/device-query.dto.ts +++ b/src/devices/dto/device-query.dto.ts @@ -9,8 +9,9 @@ import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; */ export class DeviceQueryDto { @ApiPropertyOptional({ - description: '关键词(支持设备 SN / 患者姓名 / 患者手机号)', - example: 'SN-A', + description: + '关键词(支持植入物型号 / 植入物名称 / 患者姓名 / 患者手机号)', + example: '脑室', }) @IsOptional() @IsString({ message: 'keyword 必须是字符串' }) diff --git a/src/groups/groups.controller.ts b/src/groups/groups.controller.ts index 4c77163..e63d7eb 100644 --- a/src/groups/groups.controller.ts +++ b/src/groups/groups.controller.ts @@ -41,7 +41,7 @@ export class GroupsController { * 创建小组。 */ @Post() - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) @ApiOperation({ summary: '创建小组' }) create(@CurrentActor() actor: ActorContext, @Body() dto: CreateGroupDto) { return this.groupsService.create(actor, dto); @@ -51,7 +51,13 @@ export class GroupsController { * 查询小组列表。 */ @Get() - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER) + @Roles( + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DIRECTOR, + Role.LEADER, + Role.DOCTOR, + ) @ApiOperation({ summary: '查询小组列表' }) findAll( @CurrentActor() actor: ActorContext, @@ -64,7 +70,13 @@ export class GroupsController { * 查询小组详情。 */ @Get(':id') - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER) + @Roles( + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DIRECTOR, + Role.LEADER, + Role.DOCTOR, + ) @ApiOperation({ summary: '查询小组详情' }) @ApiParam({ name: 'id', description: '小组 ID' }) findOne( @@ -78,7 +90,7 @@ export class GroupsController { * 更新小组。 */ @Patch(':id') - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) @ApiOperation({ summary: '更新小组' }) update( @CurrentActor() actor: ActorContext, @@ -92,7 +104,7 @@ export class GroupsController { * 删除小组。 */ @Delete(':id') - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) @ApiOperation({ summary: '删除小组' }) remove( @CurrentActor() actor: ActorContext, diff --git a/src/groups/groups.service.ts b/src/groups/groups.service.ts index b6f307a..124a7cb 100644 --- a/src/groups/groups.service.ts +++ b/src/groups/groups.service.ts @@ -26,14 +26,10 @@ export class GroupsService { ) {} /** - * 创建小组:系统管理员可跨院;院管仅可在本院;主任仅可在本科室创建。 + * 创建小组:系统管理员可跨院;院管仅可在本院。 */ async create(actor: ActorContext, dto: CreateGroupDto) { - this.access.assertRole(actor, [ - Role.SYSTEM_ADMIN, - Role.HOSPITAL_ADMIN, - Role.DIRECTOR, - ]); + this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]); const departmentId = this.access.toInt( dto.departmentId, MESSAGES.ORG.DEPARTMENT_ID_REQUIRED, @@ -42,12 +38,6 @@ export class GroupsService { if (actor.role === Role.HOSPITAL_ADMIN) { this.access.assertHospitalScope(actor, department.hospitalId); } - if (actor.role === Role.DIRECTOR) { - const actorDepartmentId = this.access.requireActorDepartmentId(actor); - if (actorDepartmentId !== department.id) { - throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN); - } - } return this.prisma.group.create({ data: { @@ -61,7 +51,7 @@ export class GroupsService { } /** - * 查询小组列表:院管限定本院;主任限定本科室;组长限定本组。 + * 查询小组列表:院管限定本院。 */ async findAll(actor: ActorContext, query: OrganizationQueryDto) { this.access.assertRole(actor, [ @@ -69,6 +59,7 @@ export class GroupsService { Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER, + Role.DOCTOR, ]); const paging = this.access.resolvePaging(query); const where: Prisma.GroupWhereInput = {}; @@ -87,10 +78,12 @@ export class GroupsService { where.department = { hospitalId: this.access.requireActorHospitalId(actor), }; - } else if (actor.role === Role.DIRECTOR) { + } else if ( + actor.role === Role.DIRECTOR || + actor.role === Role.LEADER || + actor.role === Role.DOCTOR + ) { where.departmentId = this.access.requireActorDepartmentId(actor); - } else if (actor.role === Role.LEADER) { - where.id = this.access.requireActorGroupId(actor); } else if (query.hospitalId != null) { where.department = { hospitalId: this.access.toInt( @@ -118,7 +111,7 @@ export class GroupsService { } /** - * 查询小组详情:院管仅可查看本院;主任仅可查看本科室;组长仅可查看本组。 + * 查询小组详情:院管仅可查看本院。 */ async findOne(actor: ActorContext, id: number) { this.access.assertRole(actor, [ @@ -126,6 +119,7 @@ export class GroupsService { Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER, + Role.DOCTOR, ]); const groupId = this.access.toInt(id, MESSAGES.ORG.GROUP_ID_REQUIRED); const group = await this.prisma.group.findUnique({ @@ -140,24 +134,21 @@ export class GroupsService { } if (actor.role === Role.HOSPITAL_ADMIN) { this.access.assertHospitalScope(actor, group.department.hospital.id); - } - if (actor.role === Role.DIRECTOR) { + } else if ( + actor.role === Role.DIRECTOR || + actor.role === Role.LEADER || + actor.role === Role.DOCTOR + ) { const actorDepartmentId = this.access.requireActorDepartmentId(actor); if (group.department.id !== actorDepartmentId) { - throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN); - } - } - if (actor.role === Role.LEADER) { - const actorGroupId = this.access.requireActorGroupId(actor); - if (group.id !== actorGroupId) { - throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN); + throw new ForbiddenException(MESSAGES.ORG.HOSPITAL_ADMIN_SCOPE_INVALID); } } return group; } /** - * 更新小组:院管仅可修改本院;主任仅可修改本科室;组长仅可修改本组。 + * 更新小组:院管仅可修改本院。 */ async update(actor: ActorContext, id: number, dto: UpdateGroupDto) { const current = await this.findOne(actor, id); @@ -181,14 +172,10 @@ export class GroupsService { } /** - * 删除小组:院管仅可删除本院;主任仅可删除本科室小组。 + * 删除小组:院管仅可删除本院。 */ async remove(actor: ActorContext, id: number) { - this.access.assertRole(actor, [ - Role.SYSTEM_ADMIN, - Role.HOSPITAL_ADMIN, - Role.DIRECTOR, - ]); + this.access.assertRole(actor, [Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN]); const current = await this.findOne(actor, id); // 业务层先拦截,给前端稳定中文提示;数据库层仍保留 RESTRICT 兜底。 diff --git a/src/main.ts b/src/main.ts index fdb7efe..33be212 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,16 +1,23 @@ import 'dotenv/config'; +import { mkdirSync } from 'node:fs'; import { BadRequestException, ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { AppModule } from './app.module.js'; import { HttpExceptionFilter } from './common/http-exception.filter.js'; import { MESSAGES } from './common/messages.js'; import { ResponseEnvelopeInterceptor } from './common/response-envelope.interceptor.js'; +import { resolveUploadRootDir } from './uploads/upload-path.util.js'; async function bootstrap() { // 创建应用实例并加载核心模块。 - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule); app.enableCors(); + mkdirSync(resolveUploadRootDir(), { recursive: true }); + app.useStaticAssets(resolveUploadRootDir(), { + prefix: '/uploads/', + }); // 全局校验:清理未知字段、自动类型转换,并将校验错误统一为中文信息。 app.useGlobalPipes( new ValidationPipe({ diff --git a/src/patients/b-patients/b-patients.service.ts b/src/patients/b-patients/b-patients.service.ts index 6997788..d86a025 100644 --- a/src/patients/b-patients/b-patients.service.ts +++ b/src/patients/b-patients/b-patients.service.ts @@ -1,4 +1,3 @@ -import { randomUUID } from 'node:crypto'; import { BadRequestException, ConflictException, @@ -11,6 +10,7 @@ import { DeviceStatus, Role } from '../../generated/prisma/enums.js'; import { PrismaService } from '../../prisma.service.js'; import type { ActorContext } from '../../common/actor-context.js'; import { MESSAGES } from '../../common/messages.js'; +import { normalizePressureLabel } from '../../common/pressure-level.util.js'; import { CreatePatientDto } from '../dto/create-patient.dto.js'; import { CreatePatientSurgeryDto } from '../dto/create-patient-surgery.dto.js'; import { UpdatePatientDto } from '../dto/update-patient.dto.js'; @@ -33,10 +33,10 @@ const IMPLANT_CATALOG_SELECT = { const PATIENT_LIST_INCLUDE = { hospital: { select: { id: true, name: true } }, doctor: { select: { id: true, name: true, role: true } }, + creator: { select: { id: true, name: true, role: true } }, devices: { select: { id: true, - snCode: true, status: true, currentPressure: true, isAbandoned: true, @@ -52,6 +52,7 @@ const PATIENT_LIST_INCLUDE = { id: true, surgeryDate: true, surgeryName: true, + surgeonId: true, surgeonName: true, }, orderBy: { surgeryDate: 'desc' }, @@ -76,6 +77,13 @@ const PATIENT_DETAIL_INCLUDE = { groupId: true, }, }, + creator: { + select: { + id: true, + name: true, + role: true, + }, + }, devices: { include: { implantCatalog: { @@ -101,6 +109,13 @@ const PATIENT_DETAIL_INCLUDE = { }, orderBy: { id: 'desc' }, }, + surgeon: { + select: { + id: true, + name: true, + role: true, + }, + }, }, orderBy: { surgeryDate: 'desc' }, }, @@ -212,6 +227,7 @@ export class BPatientsService { const patient = await tx.patient.create({ data: { name: this.normalizeRequiredString(dto.name, 'name'), + creatorId: actor.id, inpatientNo: dto.inpatientNo === undefined ? undefined @@ -231,7 +247,9 @@ export class BPatientsService { if (dto.initialSurgery) { await this.createPatientSurgeryRecord( tx, + actor, patient.id, + patient.doctorId, dto.initialSurgery, ); } @@ -255,7 +273,9 @@ export class BPatientsService { return this.prisma.$transaction(async (tx) => { const createdSurgery = await this.createPatientSurgeryRecord( tx, + actor, patient.id, + patient.doctorId, dto, ); @@ -440,6 +460,7 @@ export class BPatientsService { where: { id: normalizedDoctorId }, select: { id: true, + name: true, role: true, hospitalId: true, departmentId: true, @@ -553,7 +574,9 @@ export class BPatientsService { */ private async createPatientSurgeryRecord( prisma: PrismaExecutor, + actor: ActorContext, patientId: number, + patientDoctorId: number, dto: CreatePatientSurgeryDto, ) { if (!Array.isArray(dto.devices) || dto.devices.length === 0) { @@ -571,13 +594,14 @@ export class BPatientsService { new Set(dto.abandonedDeviceIds ?? []), ); - const [catalogMap, latestSurgery] = await Promise.all([ + const [catalogMap, latestSurgery, surgeon] = await Promise.all([ this.resolveImplantCatalogMap(prisma, catalogIds), prisma.patientSurgery.findFirst({ where: { patientId }, orderBy: { surgeryDate: 'desc' }, select: { surgeryDate: true }, }), + this.resolveWritableDoctor(actor, patientDoctorId), ]); if (abandonedDeviceIds.length > 0) { @@ -596,7 +620,7 @@ export class BPatientsService { } } - const deviceDrafts = dto.devices.map((device, index) => { + const deviceDrafts = dto.devices.map((device) => { const catalog = catalogMap.get(device.implantCatalogId); if (!catalog) { throw new NotFoundException(MESSAGES.PATIENT.IMPLANT_CATALOG_NOT_FOUND); @@ -607,7 +631,7 @@ export class BPatientsService { ? null : this.assertPressureLevelAllowed( catalog, - this.normalizeNonNegativeInteger( + this.normalizePressureLevel( device.initialPressure, 'initialPressure', ), @@ -615,18 +639,17 @@ export class BPatientsService { const fallbackPressureLevel = catalog.isPressureAdjustable && catalog.pressureLevels.length > 0 ? catalog.pressureLevels[0] - : 0; + : '0'; const currentPressure = catalog.isPressureAdjustable ? this.assertPressureLevelAllowed( catalog, initialPressure ?? fallbackPressureLevel, ) - : 0; + : '0'; return { patient: { connect: { id: patientId } }, implantCatalog: { connect: { id: catalog.id } }, - snCode: this.resolveDeviceSnCode(device.snCode, patientId, index), currentPressure, status: DeviceStatus.ACTIVE, implantModel: catalog.modelCode, @@ -662,11 +685,6 @@ export class BPatientsService { }; }); - await this.assertSnCodesUnique( - prisma, - deviceDrafts.map((device) => device.snCode), - ); - const surgery = await prisma.patientSurgery.create({ data: { patientId, @@ -675,10 +693,8 @@ export class BPatientsService { dto.surgeryName, 'surgeryName', ), - surgeonName: this.normalizeRequiredString( - dto.surgeonName, - 'surgeonName', - ), + surgeonId: surgeon.id, + surgeonName: surgeon.name, preOpPressure: dto.preOpPressure == null ? null @@ -765,9 +781,9 @@ export class BPatientsService { private assertPressureLevelAllowed( catalog: { isPressureAdjustable: boolean; - pressureLevels: number[]; + pressureLevels: string[]; }, - pressure: number, + pressure: string, ) { if ( catalog.isPressureAdjustable && @@ -898,6 +914,10 @@ export class BPatientsService { return parsed; } + private normalizePressureLevel(value: unknown, fieldName: string) { + return normalizePressureLabel(value, fieldName); + } + private normalizeStringArray(value: unknown, fieldName: string) { if (!Array.isArray(value) || value.length === 0) { throw new BadRequestException(`${fieldName} 必须为非空数组`); @@ -927,39 +947,6 @@ export class BPatientsService { })) as Prisma.InputJsonArray; } - private resolveDeviceSnCode( - snCode: string | undefined, - patientId: number, - index: number, - ) { - if (snCode) { - return this.normalizeRequiredString(snCode, 'snCode').toUpperCase(); - } - - return `SURG-${patientId}-${Date.now()}-${index + 1}-${randomUUID() - .slice(0, 8) - .toUpperCase()}`; - } - - private async assertSnCodesUnique(prisma: PrismaExecutor, snCodes: string[]) { - const uniqueSnCodes = Array.from(new Set(snCodes)); - if (uniqueSnCodes.length !== snCodes.length) { - throw new ConflictException(MESSAGES.DEVICE.SN_CODE_DUPLICATE); - } - - const existing = await prisma.device.findMany({ - where: { - snCode: { in: uniqueSnCodes }, - }, - select: { id: true }, - take: 1, - }); - - if (existing.length > 0) { - throw new ConflictException(MESSAGES.DEVICE.SN_CODE_DUPLICATE); - } - } - private toInt(value: unknown, fieldName: string) { const parsed = Number(value); if (!Number.isInteger(parsed) || parsed <= 0) { diff --git a/src/patients/c-patients/c-patients.service.ts b/src/patients/c-patients/c-patients.service.ts index 7f98ed7..970f90a 100644 --- a/src/patients/c-patients/c-patients.service.ts +++ b/src/patients/c-patients/c-patients.service.ts @@ -110,11 +110,10 @@ export class CPatientsService { }, devices: surgery.devices.map((device) => ({ id: this.toJsonNumber(device.id), - snCode: device.snCode, status: device.status, isAbandoned: device.isAbandoned, - currentPressure: this.toJsonNumber(device.currentPressure), - initialPressure: this.toJsonNumber(device.initialPressure), + currentPressure: device.currentPressure, + initialPressure: device.initialPressure, implantModel: device.implantModel, implantManufacturer: device.implantManufacturer, implantName: device.implantName, @@ -149,10 +148,9 @@ export class CPatientsService { }, device: { id: this.toJsonNumber(device.id), - snCode: device.snCode, status: device.status, isAbandoned: device.isAbandoned, - currentPressure: this.toJsonNumber(device.currentPressure), + currentPressure: device.currentPressure, implantModel: device.implantModel, implantManufacturer: device.implantManufacturer, implantName: device.implantName, @@ -172,8 +170,8 @@ export class CPatientsService { }, taskItem: { id: this.toJsonNumber(taskItem.id), - oldPressure: this.toJsonNumber(taskItem.oldPressure), - targetPressure: this.toJsonNumber(taskItem.targetPressure), + oldPressure: taskItem.oldPressure, + targetPressure: taskItem.targetPressure, }, }, ]; diff --git a/src/patients/dto/create-patient-surgery.dto.ts b/src/patients/dto/create-patient-surgery.dto.ts index 8983621..741532f 100644 --- a/src/patients/dto/create-patient-surgery.dto.ts +++ b/src/patients/dto/create-patient-surgery.dto.ts @@ -32,13 +32,6 @@ export class CreatePatientSurgeryDto { @IsString({ message: 'surgeryName 必须是字符串' }) surgeryName!: string; - @ApiProperty({ - description: '主刀医生', - example: '张主任', - }) - @IsString({ message: 'surgeonName 必须是字符串' }) - surgeonName!: string; - @ApiPropertyOptional({ description: '术前测压,可为空', example: 22, diff --git a/src/patients/dto/create-surgery-device.dto.ts b/src/patients/dto/create-surgery-device.dto.ts index f1c48ed..d1f3343 100644 --- a/src/patients/dto/create-surgery-device.dto.ts +++ b/src/patients/dto/create-surgery-device.dto.ts @@ -23,14 +23,6 @@ export class CreateSurgeryDeviceDto { @Min(1, { message: 'implantCatalogId 必须大于 0' }) implantCatalogId!: number; - @ApiPropertyOptional({ - description: '设备 SN,可不传;不传时系统自动生成', - example: 'TYT-SHUNT-001', - }) - @IsOptional() - @IsString({ message: 'snCode 必须是字符串' }) - snCode?: string; - @ApiProperty({ description: '分流方式', example: 'VPS', @@ -68,14 +60,12 @@ export class CreateSurgeryDeviceDto { distalShuntDirection!: string; @ApiPropertyOptional({ - description: '初始压力,可为空', - example: 120, + description: '初始压力挡位,可为空', + example: '1.5', }) @IsOptional() - @Type(() => Number) - @IsInt({ message: 'initialPressure 必须是整数' }) - @Min(0, { message: 'initialPressure 必须大于等于 0' }) - initialPressure?: number; + @IsString({ message: 'initialPressure 必须是字符串' }) + initialPressure?: string; @ApiPropertyOptional({ description: '植入物备注', diff --git a/src/tasks/b-tasks/b-tasks.controller.ts b/src/tasks/b-tasks/b-tasks.controller.ts index 0e7f8a8..6ed7ae5 100644 --- a/src/tasks/b-tasks/b-tasks.controller.ts +++ b/src/tasks/b-tasks/b-tasks.controller.ts @@ -1,5 +1,10 @@ -import { Body, Controller, Post, UseGuards } from '@nestjs/common'; -import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common'; +import { + ApiBearerAuth, + ApiOperation, + ApiQuery, + ApiTags, +} from '@nestjs/swagger'; import type { ActorContext } from '../../common/actor-context.js'; import { CurrentActor } from '../../auth/current-actor.decorator.js'; import { AccessTokenGuard } from '../../auth/access-token.guard.js'; @@ -11,6 +16,8 @@ import { PublishTaskDto } from '../dto/publish-task.dto.js'; import { AcceptTaskDto } from '../dto/accept-task.dto.js'; import { CompleteTaskDto } from '../dto/complete-task.dto.js'; import { CancelTaskDto } from '../dto/cancel-task.dto.js'; +import { TaskRecordQueryDto } from '../dto/task-record-query.dto.js'; +import { AssignableEngineerQueryDto } from '../dto/assignable-engineer-query.dto.js'; /** * B 端任务控制器:封装调压任务状态流转接口。 @@ -23,21 +30,78 @@ export class BTasksController { constructor(private readonly taskService: TaskService) {} /** - * 医生/主任/组长发布调压任务。 + * 查询当前角色可指定的接收工程师列表。 + */ + @Get('engineers') + @Roles( + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DOCTOR, + Role.DIRECTOR, + Role.LEADER, + ) + @ApiOperation({ summary: '查询可选接收工程师列表' }) + @ApiQuery({ + name: 'hospitalId', + required: false, + description: '系统管理员可按医院筛选', + }) + findAssignableEngineers( + @CurrentActor() actor: ActorContext, + @Query() query: AssignableEngineerQueryDto, + ) { + return this.taskService.findAssignableEngineers(actor, query.hospitalId); + } + + /** + * 查询当前角色可见的调压记录列表。 + */ + @Get() + @Roles( + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DOCTOR, + Role.DIRECTOR, + Role.LEADER, + Role.ENGINEER, + ) + @ApiOperation({ summary: '查询调压记录列表' }) + @ApiQuery({ + name: 'hospitalId', + required: false, + description: '系统管理员可按医院筛选', + }) + findRecords( + @CurrentActor() actor: ActorContext, + @Query() query: TaskRecordQueryDto, + ) { + return this.taskService.findTaskRecords(actor, query); + } + + /** + * 系统管理员/医院管理员/医生/主任/组长发布调压任务。 */ @Post('publish') - @Roles(Role.DOCTOR, Role.DIRECTOR, Role.LEADER) - @ApiOperation({ summary: '发布任务(DOCTOR/DIRECTOR/LEADER)' }) + @Roles( + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DOCTOR, + Role.DIRECTOR, + Role.LEADER, + ) + @ApiOperation({ + summary: '发布任务(SYSTEM_ADMIN/HOSPITAL_ADMIN/DOCTOR/DIRECTOR/LEADER)', + }) publish(@CurrentActor() actor: ActorContext, @Body() dto: PublishTaskDto) { return this.taskService.publishTask(actor, dto); } /** - * 工程师接收调压任务。 + * 工程师接收调压任务(当前流程已停用)。 */ @Post('accept') @Roles(Role.ENGINEER) - @ApiOperation({ summary: '接收任务(ENGINEER)' }) + @ApiOperation({ summary: '接收任务(已停用)' }) accept(@CurrentActor() actor: ActorContext, @Body() dto: AcceptTaskDto) { return this.taskService.acceptTask(actor, dto); } @@ -53,11 +117,19 @@ export class BTasksController { } /** - * 医生/主任/组长取消调压任务(仅任务创建者)。 + * 系统管理员/医院管理员/医生/主任/组长取消调压任务(仅任务创建者)。 */ @Post('cancel') - @Roles(Role.DOCTOR, Role.DIRECTOR, Role.LEADER) - @ApiOperation({ summary: '取消任务(DOCTOR/DIRECTOR/LEADER)' }) + @Roles( + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DOCTOR, + Role.DIRECTOR, + Role.LEADER, + ) + @ApiOperation({ + summary: '取消任务(SYSTEM_ADMIN/HOSPITAL_ADMIN/DOCTOR/DIRECTOR/LEADER)', + }) cancel(@CurrentActor() actor: ActorContext, @Body() dto: CancelTaskDto) { return this.taskService.cancelTask(actor, dto); } diff --git a/src/tasks/dto/assignable-engineer-query.dto.ts b/src/tasks/dto/assignable-engineer-query.dto.ts new file mode 100644 index 0000000..cf6a1e4 --- /dev/null +++ b/src/tasks/dto/assignable-engineer-query.dto.ts @@ -0,0 +1,20 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsInt, IsOptional, Min } from 'class-validator'; +import { EmptyStringToUndefined } from '../../common/transforms/empty-string-to-undefined.transform.js'; + +/** + * 可指派工程师查询 DTO:系统管理员可显式传医院范围。 + */ +export class AssignableEngineerQueryDto { + @ApiPropertyOptional({ + description: '医院 ID,仅系统管理员可选传', + example: 1, + }) + @IsOptional() + @EmptyStringToUndefined() + @Type(() => Number) + @IsInt({ message: 'hospitalId 必须是整数' }) + @Min(1, { message: 'hospitalId 必须大于 0' }) + hospitalId?: number; +} diff --git a/src/tasks/dto/publish-task.dto.ts b/src/tasks/dto/publish-task.dto.ts index 28a1cd6..4e3c81d 100644 --- a/src/tasks/dto/publish-task.dto.ts +++ b/src/tasks/dto/publish-task.dto.ts @@ -1,11 +1,10 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { EmptyStringToUndefined } from '../../common/transforms/empty-string-to-undefined.transform.js'; import { ArrayMinSize, IsArray, IsInt, - IsOptional, + IsString, Min, ValidateNested, } from 'class-validator'; @@ -20,23 +19,20 @@ export class PublishTaskItemDto { @Min(1, { message: 'deviceId 必须大于 0' }) deviceId!: number; - @ApiProperty({ description: '目标压力值', example: 120 }) - @Type(() => Number) - @IsInt({ message: 'targetPressure 必须是整数' }) - targetPressure!: number; + @ApiProperty({ description: '目标挡位标签', example: '1.5' }) + @IsString({ message: 'targetPressure 必须是字符串' }) + targetPressure!: string; } /** * 发布任务 DTO。 */ export class PublishTaskDto { - @ApiPropertyOptional({ description: '指定工程师 ID(可选)', example: 2 }) - @IsOptional() - @EmptyStringToUndefined() + @ApiProperty({ description: '接收工程师 ID', example: 2 }) @Type(() => Number) @IsInt({ message: 'engineerId 必须是整数' }) @Min(1, { message: 'engineerId 必须大于 0' }) - engineerId?: number; + engineerId!: number; @ApiProperty({ type: [PublishTaskItemDto], description: '任务明细列表' }) @IsArray({ message: 'items 必须是数组' }) diff --git a/src/tasks/dto/task-record-query.dto.ts b/src/tasks/dto/task-record-query.dto.ts new file mode 100644 index 0000000..756cd67 --- /dev/null +++ b/src/tasks/dto/task-record-query.dto.ts @@ -0,0 +1,63 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsEnum, IsInt, IsOptional, IsString, Max, Min } from 'class-validator'; +import { EmptyStringToUndefined } from '../../common/transforms/empty-string-to-undefined.transform.js'; +import { TaskStatus } from '../../generated/prisma/enums.js'; + +/** + * 调压记录查询 DTO:用于后台任务记录页筛选。 + */ +export class TaskRecordQueryDto { + @ApiPropertyOptional({ + description: '关键词(支持患者姓名/住院号/手机号/植入物名称/植入物型号)', + example: '张三', + }) + @IsOptional() + @IsString({ message: 'keyword 必须是字符串' }) + keyword?: string; + + @ApiPropertyOptional({ + description: '任务状态', + enum: TaskStatus, + example: TaskStatus.PENDING, + }) + @IsOptional() + @IsEnum(TaskStatus, { message: 'status 枚举值不合法' }) + status?: TaskStatus; + + @ApiPropertyOptional({ + description: '医院 ID,仅系统管理员可选传', + example: 1, + }) + @IsOptional() + @EmptyStringToUndefined() + @Type(() => Number) + @IsInt({ message: 'hospitalId 必须是整数' }) + @Min(1, { message: 'hospitalId 必须大于 0' }) + hospitalId?: number; + + @ApiPropertyOptional({ + description: '页码(默认 1)', + example: 1, + default: 1, + }) + @IsOptional() + @EmptyStringToUndefined() + @Type(() => Number) + @IsInt({ message: 'page 必须是整数' }) + @Min(1, { message: 'page 最小为 1' }) + page?: number = 1; + + @ApiPropertyOptional({ + description: '每页数量(默认 20,最大 100)', + example: 20, + default: 20, + }) + @IsOptional() + @EmptyStringToUndefined() + @Type(() => Number) + @IsInt({ message: 'pageSize 必须是整数' }) + @Min(1, { message: 'pageSize 最小为 1' }) + @Max(100, { message: 'pageSize 最大为 100' }) + pageSize?: number = 20; +} diff --git a/src/tasks/task.service.ts b/src/tasks/task.service.ts index 70cc53b..b404b8f 100644 --- a/src/tasks/task.service.ts +++ b/src/tasks/task.service.ts @@ -6,6 +6,7 @@ import { NotFoundException, } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; +import { Prisma } from '../generated/prisma/client.js'; import { DeviceStatus, Role, TaskStatus } from '../generated/prisma/enums.js'; import { PrismaService } from '../prisma.service.js'; import type { ActorContext } from '../common/actor-context.js'; @@ -13,7 +14,9 @@ import { PublishTaskDto } from './dto/publish-task.dto.js'; import { AcceptTaskDto } from './dto/accept-task.dto.js'; import { CompleteTaskDto } from './dto/complete-task.dto.js'; import { CancelTaskDto } from './dto/cancel-task.dto.js'; +import { TaskRecordQueryDto } from './dto/task-record-query.dto.js'; import { MESSAGES } from '../common/messages.js'; +import { normalizePressureLabel } from '../common/pressure-level.util.js'; /** * 任务服务:封装调压任务状态机、院内隔离与事件发布逻辑。 @@ -26,11 +29,162 @@ export class TaskService { ) {} /** - * 发布任务:医生/主任/组长创建主任务与明细,状态初始化为 PENDING。 + * 查询当前角色可指定的接收工程师列表。 + */ + async findAssignableEngineers( + actor: ActorContext, + requestedHospitalId?: number, + ) { + this.assertRole(actor, [ + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DOCTOR, + Role.DIRECTOR, + Role.LEADER, + ]); + + const hospitalId = this.resolveAssignableHospitalId( + actor, + requestedHospitalId, + ); + return this.prisma.user.findMany({ + where: { + role: Role.ENGINEER, + hospitalId, + }, + select: { + id: true, + name: true, + phone: true, + hospitalId: true, + }, + orderBy: { id: 'desc' }, + }); + } + + /** + * 查询当前角色可见的调压记录列表。 + */ + async findTaskRecords(actor: ActorContext, query: TaskRecordQueryDto) { + this.assertRole(actor, [ + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DOCTOR, + Role.DIRECTOR, + Role.LEADER, + Role.ENGINEER, + ]); + + const hospitalId = this.resolveListHospitalId(actor, query.hospitalId); + const page = query.page ?? 1; + const pageSize = query.pageSize ?? 20; + const skip = (page - 1) * pageSize; + const where = this.buildTaskRecordWhere(query, hospitalId); + + const [total, items] = await Promise.all([ + this.prisma.taskItem.count({ where }), + this.prisma.taskItem.findMany({ + where, + skip, + take: pageSize, + orderBy: { id: 'desc' }, + select: { + id: true, + oldPressure: true, + targetPressure: true, + task: { + select: { + id: true, + status: true, + createdAt: true, + hospital: { + select: { + id: true, + name: true, + }, + }, + creator: { + select: { + id: true, + name: true, + role: true, + }, + }, + engineer: { + select: { + id: true, + name: true, + role: true, + }, + }, + }, + }, + device: { + select: { + id: true, + currentPressure: true, + implantModel: true, + implantManufacturer: true, + implantName: true, + patient: { + select: { + id: true, + name: true, + inpatientNo: true, + phone: true, + }, + }, + surgery: { + select: { + id: true, + surgeryName: true, + surgeryDate: true, + }, + }, + }, + }, + }, + }), + ]); + + return { + list: items.map((item) => ({ + id: item.id, + oldPressure: item.oldPressure, + targetPressure: item.targetPressure, + currentPressure: item.device.currentPressure, + taskId: item.task.id, + status: item.task.status, + createdAt: item.task.createdAt, + hospital: item.task.hospital, + creator: item.task.creator, + engineer: item.task.engineer, + patient: item.device.patient, + surgery: item.device.surgery, + device: { + id: item.device.id, + implantModel: item.device.implantModel, + implantManufacturer: item.device.implantManufacturer, + implantName: item.device.implantName, + }, + })), + total, + page, + pageSize, + }; + } + + /** + * 发布任务:管理员或临床角色创建主任务与明细,并直接指定接收工程师。 */ async publishTask(actor: ActorContext, dto: PublishTaskDto) { - this.assertRole(actor, [Role.DOCTOR, Role.DIRECTOR, Role.LEADER]); - const hospitalId = this.requireHospitalId(actor); + this.assertRole(actor, [ + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DOCTOR, + Role.DIRECTOR, + Role.LEADER, + ]); if (!Array.isArray(dto.items) || dto.items.length === 0) { throw new BadRequestException(MESSAGES.TASK.ITEMS_REQUIRED); @@ -42,27 +196,32 @@ export class TaskService { if (!Number.isInteger(item.deviceId)) { throw new BadRequestException(`deviceId 非法: ${item.deviceId}`); } - if (!Number.isInteger(item.targetPressure)) { - throw new BadRequestException( - `targetPressure 非法: ${item.targetPressure}`, - ); - } return item.deviceId; }), ), ); + const scopedHospitalId = this.resolveScopedHospitalId(actor); const devices = await this.prisma.device.findMany({ where: { id: { in: deviceIds }, status: DeviceStatus.ACTIVE, isAbandoned: false, isPressureAdjustable: true, - patient: { hospitalId }, + patient: scopedHospitalId + ? { + hospitalId: scopedHospitalId, + } + : undefined, }, select: { id: true, currentPressure: true, + patient: { + select: { + hospitalId: true, + }, + }, implantCatalog: { select: { pressureLevels: true, @@ -75,18 +234,21 @@ export class TaskService { throw new NotFoundException(MESSAGES.TASK.DEVICE_NOT_FOUND); } - if (dto.engineerId != null) { - const engineer = await this.prisma.user.findFirst({ - where: { - id: dto.engineerId, - role: Role.ENGINEER, - hospitalId, - }, - select: { id: true }, - }); - if (!engineer) { - throw new BadRequestException(MESSAGES.TASK.ENGINEER_INVALID); - } + const hospitalId = this.resolveTaskHospitalId( + actor, + devices.map((device) => device.patient.hospitalId), + ); + + const engineer = await this.prisma.user.findFirst({ + where: { + id: dto.engineerId, + role: Role.ENGINEER, + hospitalId, + }, + select: { id: true }, + }); + if (!engineer) { + throw new BadRequestException(MESSAGES.TASK.ENGINEER_INVALID); } const pressureByDeviceId = new Map( @@ -102,25 +264,30 @@ export class TaskService { ); dto.items.forEach((item) => { + const normalizedTargetPressure = this.normalizeTargetPressure( + item.targetPressure, + ); const pressureLevels = pressureLevelsByDeviceId.get(item.deviceId) ?? []; if ( pressureLevels.length > 0 && - !pressureLevels.includes(item.targetPressure) + !pressureLevels.includes(normalizedTargetPressure) ) { throw new BadRequestException(MESSAGES.DEVICE.PRESSURE_LEVEL_INVALID); } + + item.targetPressure = normalizedTargetPressure; }); const task = await this.prisma.task.create({ data: { - status: TaskStatus.PENDING, + status: TaskStatus.ACCEPTED, creatorId: actor.id, - engineerId: dto.engineerId ?? null, + engineerId: engineer.id, hospitalId, items: { create: dto.items.map((item) => ({ deviceId: item.deviceId, - oldPressure: pressureByDeviceId.get(item.deviceId) ?? 0, + oldPressure: pressureByDeviceId.get(item.deviceId) ?? '0', targetPressure: item.targetPressure, })), }, @@ -139,68 +306,10 @@ export class TaskService { } /** - * 接收任务:工程师将任务从 PENDING 流转到 ACCEPTED。 + * 接收任务:当前流程已停用,统一改为发布时直接指定接收工程师。 */ - async acceptTask(actor: ActorContext, dto: AcceptTaskDto) { - this.assertRole(actor, [Role.ENGINEER]); - const hospitalId = this.requireHospitalId(actor); - - const task = await this.prisma.task.findFirst({ - where: { - id: dto.taskId, - hospitalId, - }, - select: { - id: true, - status: true, - hospitalId: true, - engineerId: true, - }, - }); - - if (!task) { - throw new NotFoundException(MESSAGES.TASK.TASK_NOT_FOUND); - } - if (task.status !== TaskStatus.PENDING) { - throw new ConflictException(MESSAGES.TASK.ACCEPT_ONLY_PENDING); - } - if (task.engineerId != null && task.engineerId !== actor.id) { - throw new ForbiddenException(MESSAGES.TASK.ENGINEER_ALREADY_ASSIGNED); - } - - const accepted = await this.prisma.task.updateMany({ - where: { - id: task.id, - hospitalId, - status: TaskStatus.PENDING, - OR: [{ engineerId: null }, { engineerId: actor.id }], - }, - data: { - status: TaskStatus.ACCEPTED, - engineerId: actor.id, - }, - }); - - if (accepted.count !== 1) { - throw new ConflictException(MESSAGES.TASK.ACCEPT_ONLY_PENDING); - } - - const updatedTask = await this.prisma.task.findUnique({ - where: { id: task.id }, - include: { items: true }, - }); - if (!updatedTask) { - throw new NotFoundException(MESSAGES.TASK.TASK_NOT_FOUND); - } - - await this.eventEmitter.emitAsync('task.accepted', { - taskId: updatedTask.id, - hospitalId: updatedTask.hospitalId, - actorId: actor.id, - status: updatedTask.status, - }); - - return updatedTask; + async acceptTask(_actor: ActorContext, _dto: AcceptTaskDto) { + throw new ForbiddenException(MESSAGES.TASK.ACCEPT_DISABLED); } /** @@ -263,13 +372,19 @@ export class TaskService { * 取消任务:任务创建者可将 PENDING/ACCEPTED 任务取消。 */ async cancelTask(actor: ActorContext, dto: CancelTaskDto) { - this.assertRole(actor, [Role.DOCTOR, Role.DIRECTOR, Role.LEADER]); - const hospitalId = this.requireHospitalId(actor); + this.assertRole(actor, [ + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DOCTOR, + Role.DIRECTOR, + Role.LEADER, + ]); + const scopedHospitalId = this.resolveScopedHospitalId(actor); const task = await this.prisma.task.findFirst({ where: { id: dto.taskId, - hospitalId, + hospitalId: scopedHospitalId ?? undefined, }, select: { id: true, @@ -320,7 +435,154 @@ export class TaskService { } /** - * 校验并返回 hospitalId(B 端强依赖租户隔离)。 + * 返回角色可见的医院范围。系统管理员可不绑定医院,按目标设备自动归院。 + */ + private resolveScopedHospitalId(actor: ActorContext): number | null { + if (actor.role === Role.SYSTEM_ADMIN) { + return actor.hospitalId ?? null; + } + if (!actor.hospitalId) { + throw new BadRequestException(MESSAGES.TASK.ACTOR_HOSPITAL_REQUIRED); + } + return actor.hospitalId; + } + + /** + * 解析本次任务归属医院,确保同一批设备不跨院。 + */ + private resolveTaskHospitalId( + actor: ActorContext, + hospitalIds: number[], + ): number { + const uniqueHospitalIds = Array.from( + new Set(hospitalIds.filter((hospitalId) => Number.isInteger(hospitalId))), + ); + + if (uniqueHospitalIds.length !== 1) { + throw new BadRequestException(MESSAGES.TASK.DEVICE_MULTI_HOSPITAL); + } + + const [hospitalId] = uniqueHospitalIds; + if (actor.hospitalId && actor.hospitalId !== hospitalId) { + throw new ForbiddenException(MESSAGES.TASK.DEVICE_NOT_FOUND); + } + + return hospitalId; + } + + /** + * 解析工程师指派范围。系统管理员可显式指定医院,其余角色固定本院。 + */ + private resolveAssignableHospitalId( + actor: ActorContext, + requestedHospitalId?: number, + ) { + if (actor.role === Role.SYSTEM_ADMIN) { + if (requestedHospitalId !== undefined) { + return requestedHospitalId; + } + + return this.requireHospitalId(actor); + } + + return this.requireHospitalId(actor); + } + + /** + * 列表查询的医院范围解析。系统管理员可按查询条件切院,其余角色固定本院。 + */ + private resolveListHospitalId( + actor: ActorContext, + requestedHospitalId?: number, + ) { + if (actor.role === Role.SYSTEM_ADMIN) { + return requestedHospitalId ?? actor.hospitalId ?? undefined; + } + + return this.requireHospitalId(actor); + } + + /** + * 构造调压记录查询条件。 + */ + private buildTaskRecordWhere( + query: TaskRecordQueryDto, + hospitalId?: number, + ): Prisma.TaskItemWhereInput { + const keyword = query.keyword?.trim(); + + const where: Prisma.TaskItemWhereInput = { + task: { + hospitalId, + status: query.status, + }, + }; + + if (!keyword) { + return where; + } + + where.OR = [ + { + device: { + patient: { + name: { + contains: keyword, + mode: 'insensitive', + }, + }, + }, + }, + { + device: { + patient: { + inpatientNo: { + contains: keyword, + mode: 'insensitive', + }, + }, + }, + }, + { + device: { + patient: { + phone: { + contains: keyword, + mode: 'insensitive', + }, + }, + }, + }, + { + device: { + implantName: { + contains: keyword, + mode: 'insensitive', + }, + }, + }, + { + device: { + implantModel: { + contains: keyword, + mode: 'insensitive', + }, + }, + }, + ]; + + return where; + } + + /** + * 调压目标挡位标准化。 + */ + private normalizeTargetPressure(value: unknown) { + return normalizePressureLabel(value, 'targetPressure'); + } + + /** + * 工程师侧任务流转仍要求明确的院内身份。 */ private requireHospitalId(actor: ActorContext): number { if (!actor.hospitalId) { diff --git a/src/uploads/b-uploads/b-uploads.controller.ts b/src/uploads/b-uploads/b-uploads.controller.ts new file mode 100644 index 0000000..d277e20 --- /dev/null +++ b/src/uploads/b-uploads/b-uploads.controller.ts @@ -0,0 +1,133 @@ +import { + BadRequestException, + Body, + Controller, + Get, + ParseIntPipe, + Post, + Query, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { + ApiBearerAuth, + ApiBody, + ApiConsumes, + ApiOperation, + ApiTags, +} from '@nestjs/swagger'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { AccessTokenGuard } from '../../auth/access-token.guard.js'; +import { CurrentActor } from '../../auth/current-actor.decorator.js'; +import { Roles } from '../../auth/roles.decorator.js'; +import { RolesGuard } from '../../auth/roles.guard.js'; +import type { ActorContext } from '../../common/actor-context.js'; +import { Role } from '../../generated/prisma/enums.js'; +import { MESSAGES } from '../../common/messages.js'; +import { UploadAssetQueryDto } from '../dto/upload-asset-query.dto.js'; +import { ensureUploadDirectories, resolveUploadTempDir } from '../upload-path.util.js'; +import { UploadsService } from '../uploads.service.js'; +import { diskStorage } from 'multer'; +import { extname } from 'node:path'; +import { randomUUID } from 'node:crypto'; + +const MAX_UPLOAD_SIZE = 1024 * 1024 * 200; + +function isAllowedMimeType(mimeType: string) { + return ( + mimeType.startsWith('image/') || + mimeType.startsWith('video/') || + [ + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ].includes(mimeType) + ); +} + +@ApiTags('上传资产(B端)') +@ApiBearerAuth('bearer') +@Controller('b/uploads') +@UseGuards(AccessTokenGuard, RolesGuard) +export class BUploadsController { + constructor(private readonly uploadsService: UploadsService) {} + + /** + * 上传图片/视频/文件。 + */ + @Post() + @Roles( + Role.SYSTEM_ADMIN, + Role.HOSPITAL_ADMIN, + Role.DIRECTOR, + Role.LEADER, + Role.DOCTOR, + ) + @UseInterceptors( + FileInterceptor('file', { + storage: diskStorage({ + destination: (_req, _file, cb) => { + ensureUploadDirectories(); + cb(null, resolveUploadTempDir()); + }, + filename: (_req, file, cb) => { + cb(null, `${randomUUID()}${extname(file.originalname)}`); + }, + }), + limits: { fileSize: MAX_UPLOAD_SIZE }, + fileFilter: (_req, file, cb) => { + if (!isAllowedMimeType(file.mimetype)) { + cb( + new BadRequestException(MESSAGES.UPLOAD.UNSUPPORTED_FILE_TYPE), + false, + ); + return; + } + cb(null, true); + }, + }), + ) + @ApiOperation({ summary: '上传图片/视频/文件' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ + schema: { + type: 'object', + properties: { + file: { + type: 'string', + format: 'binary', + }, + hospitalId: { + type: 'integer', + }, + }, + required: ['file'], + }, + }) + create( + @CurrentActor() actor: ActorContext, + @UploadedFile() file?: Express.Multer.File, + @Body('hospitalId') hospitalId?: string, + ) { + const requestedHospitalId = + hospitalId == null || hospitalId === '' ? undefined : Number(hospitalId); + + return this.uploadsService.createUpload(actor, file, requestedHospitalId); + } + + /** + * 查询图库/视频库/文件库。 + */ + @Get() + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) + @ApiOperation({ summary: '查询上传资产列表' }) + findAll( + @CurrentActor() actor: ActorContext, + @Query() query: UploadAssetQueryDto, + ) { + return this.uploadsService.findAll(actor, query); + } +} diff --git a/src/uploads/dto/upload-asset-query.dto.ts b/src/uploads/dto/upload-asset-query.dto.ts new file mode 100644 index 0000000..b294379 --- /dev/null +++ b/src/uploads/dto/upload-asset-query.dto.ts @@ -0,0 +1,63 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { EmptyStringToUndefined } from '../../common/transforms/empty-string-to-undefined.transform.js'; +import { UploadAssetType } from '../../generated/prisma/enums.js'; +import { + IsEnum, + IsInt, + IsOptional, + IsString, + Max, + Min, +} from 'class-validator'; + +/** + * 上传资产查询 DTO:用于图库/视频库分页筛选。 + */ +export class UploadAssetQueryDto { + @ApiPropertyOptional({ + description: '关键词(按原始文件名模糊匹配)', + example: 'ct', + }) + @IsOptional() + @IsString({ message: 'keyword 必须是字符串' }) + keyword?: string; + + @ApiPropertyOptional({ + description: '资产类型', + enum: UploadAssetType, + example: UploadAssetType.IMAGE, + }) + @IsOptional() + @IsEnum(UploadAssetType, { message: 'type 枚举值不合法' }) + type?: UploadAssetType; + + @ApiPropertyOptional({ description: '医院 ID(SYSTEM_ADMIN 可选)', example: 1 }) + @IsOptional() + @EmptyStringToUndefined() + @Type(() => Number) + @IsInt({ message: 'hospitalId 必须是整数' }) + @Min(1, { message: 'hospitalId 必须大于 0' }) + hospitalId?: number; + + @ApiPropertyOptional({ description: '页码', example: 1, default: 1 }) + @IsOptional() + @EmptyStringToUndefined() + @Type(() => Number) + @IsInt({ message: 'page 必须是整数' }) + @Min(1, { message: 'page 最小为 1' }) + page?: number = 1; + + @ApiPropertyOptional({ + description: '每页数量', + example: 20, + default: 20, + }) + @IsOptional() + @EmptyStringToUndefined() + @Type(() => Number) + @IsInt({ message: 'pageSize 必须是整数' }) + @Min(1, { message: 'pageSize 最小为 1' }) + @Max(100, { message: 'pageSize 最大为 100' }) + pageSize?: number = 20; +} diff --git a/src/uploads/upload-path.util.ts b/src/uploads/upload-path.util.ts new file mode 100644 index 0000000..8f3f96f --- /dev/null +++ b/src/uploads/upload-path.util.ts @@ -0,0 +1,18 @@ +import { mkdirSync } from 'node:fs'; +import { join } from 'node:path'; + +/** + * 上传目录工具:统一管理公开文件目录与临时目录。 + */ +export function resolveUploadRootDir() { + return join(process.cwd(), 'storage', 'uploads'); +} + +export function resolveUploadTempDir() { + return join(process.cwd(), 'storage', 'tmp-uploads'); +} + +export function ensureUploadDirectories() { + mkdirSync(resolveUploadRootDir(), { recursive: true }); + mkdirSync(resolveUploadTempDir(), { recursive: true }); +} diff --git a/src/uploads/uploads.module.ts b/src/uploads/uploads.module.ts new file mode 100644 index 0000000..e1863dd --- /dev/null +++ b/src/uploads/uploads.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { PrismaModule } from '../prisma.module.js'; +import { BUploadsController } from './b-uploads/b-uploads.controller.js'; +import { UploadsService } from './uploads.service.js'; + +@Module({ + imports: [PrismaModule], + controllers: [BUploadsController], + providers: [UploadsService], + exports: [UploadsService], +}) +export class UploadsModule {} diff --git a/src/uploads/uploads.service.spec.ts b/src/uploads/uploads.service.spec.ts new file mode 100644 index 0000000..3dbd0b1 --- /dev/null +++ b/src/uploads/uploads.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { UploadsService } from './uploads.service'; + +describe('UploadsService', () => { + let service: UploadsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [UploadsService], + }).compile(); + + service = module.get(UploadsService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/src/uploads/uploads.service.ts b/src/uploads/uploads.service.ts new file mode 100644 index 0000000..11221f6 --- /dev/null +++ b/src/uploads/uploads.service.ts @@ -0,0 +1,383 @@ +import { + BadRequestException, + Injectable, + InternalServerErrorException, + NotFoundException, +} from '@nestjs/common'; +import { spawn } from 'node:child_process'; +import { access, mkdir, rename, stat, unlink } from 'node:fs/promises'; +import { extname, join } from 'node:path'; +import { randomUUID } from 'node:crypto'; +import ffmpegPath from 'ffmpeg-static'; +import sharp from 'sharp'; +import { Prisma } from '../generated/prisma/client.js'; +import { Role, UploadAssetType } from '../generated/prisma/enums.js'; +import type { ActorContext } from '../common/actor-context.js'; +import { MESSAGES } from '../common/messages.js'; +import { PrismaService } from '../prisma.service.js'; +import { UploadAssetQueryDto } from './dto/upload-asset-query.dto.js'; +import { + ensureUploadDirectories, + resolveUploadRootDir, + resolveUploadTempDir, +} from './upload-path.util.js'; + +@Injectable() +export class UploadsService { + constructor(private readonly prisma: PrismaService) {} + + /** + * 创建上传资产记录并落盘到公开目录。 + */ + async createUpload( + actor: ActorContext, + file: Express.Multer.File | undefined, + requestedHospitalId?: number, + ) { + if (!file) { + throw new BadRequestException(MESSAGES.UPLOAD.FILE_REQUIRED); + } + + const hospitalId = await this.resolveHospitalScope(actor, requestedHospitalId); + const type = this.detectAssetType(file.mimetype); + ensureUploadDirectories(); + const prepared = await this.prepareStoredFile(file, type); + const date = new Date(); + const relativeDir = join( + `${date.getFullYear()}`, + `${date.getMonth() + 1}`.padStart(2, '0'), + `${date.getDate()}`.padStart(2, '0'), + ); + const absoluteDir = join(resolveUploadRootDir(), relativeDir); + await mkdir(absoluteDir, { recursive: true }); + + const finalFileName = await this.buildFinalFileName( + absoluteDir, + file.originalname, + prepared.outputExtension, + date, + ); + const relativePath = join(relativeDir, finalFileName); + const absolutePath = join(resolveUploadRootDir(), relativePath); + + try { + await rename(prepared.tempPath, absolutePath); + if (prepared.tempPath !== file.path) { + await this.safeUnlink(file.path); + } + } catch (error) { + await this.safeUnlink(file.path); + await this.safeUnlink(prepared.tempPath); + throw error; + } + + const url = `/${relativePath.replace(/\\/g, '/')}`.replace(/^\/+/, '/uploads/'); + + try { + return await this.prisma.uploadAsset.create({ + data: { + hospitalId, + creatorId: actor.id, + type, + originalName: file.originalname, + fileName: finalFileName, + storagePath: relativePath.replace(/\\/g, '/'), + url, + mimeType: prepared.mimeType, + fileSize: prepared.fileSize, + }, + include: { + hospital: { select: { id: true, name: true } }, + creator: { select: { id: true, name: true, role: true } }, + }, + }); + } catch (error) { + await this.safeUnlink(absolutePath); + throw error; + } + } + + /** + * 查询上传资产列表:按医院作用域隔离。 + */ + async findAll(actor: ActorContext, query: UploadAssetQueryDto) { + const page = query.page && query.page > 0 ? query.page : 1; + const pageSize = + query.pageSize && query.pageSize > 0 && query.pageSize <= 100 + ? query.pageSize + : 20; + + const where: Prisma.UploadAssetWhereInput = {}; + const scopedHospitalId = this.resolveReadableHospitalScope(actor, query.hospitalId); + if (scopedHospitalId != null) { + where.hospitalId = scopedHospitalId; + } + if (query.type) { + where.type = query.type; + } + if (query.keyword?.trim()) { + where.originalName = { + contains: query.keyword.trim(), + mode: 'insensitive', + }; + } + + const [total, list] = await this.prisma.$transaction([ + this.prisma.uploadAsset.count({ where }), + this.prisma.uploadAsset.findMany({ + where, + include: { + hospital: { select: { id: true, name: true } }, + creator: { select: { id: true, name: true, role: true } }, + }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + ]); + + return { total, page, pageSize, list }; + } + + private detectAssetType(mimeType: string) { + if (mimeType.startsWith('image/')) { + return UploadAssetType.IMAGE; + } + if (mimeType.startsWith('video/')) { + return UploadAssetType.VIDEO; + } + return UploadAssetType.FILE; + } + + private async resolveHospitalScope( + actor: ActorContext, + requestedHospitalId?: number, + ) { + if (actor.role === Role.SYSTEM_ADMIN) { + if ( + requestedHospitalId == null || + !Number.isInteger(requestedHospitalId) || + requestedHospitalId <= 0 + ) { + throw new BadRequestException( + MESSAGES.UPLOAD.SYSTEM_ADMIN_HOSPITAL_REQUIRED, + ); + } + + const hospital = await this.prisma.hospital.findUnique({ + where: { id: requestedHospitalId }, + select: { id: true }, + }); + if (!hospital) { + throw new NotFoundException(MESSAGES.ORG.HOSPITAL_NOT_FOUND); + } + return hospital.id; + } + + if (!actor.hospitalId) { + throw new BadRequestException(MESSAGES.UPLOAD.ACTOR_HOSPITAL_REQUIRED); + } + return actor.hospitalId; + } + + private async prepareStoredFile( + file: Express.Multer.File, + type: UploadAssetType, + ) { + const baseName = randomUUID(); + + if (type === UploadAssetType.IMAGE) { + const tempPath = join(resolveUploadTempDir(), `${baseName}.webp`); + try { + await sharp(file.path) + .rotate() + .resize({ + width: 2560, + height: 2560, + fit: 'inside', + withoutEnlargement: true, + }) + .webp({ + quality: 82, + effort: 4, + }) + .toFile(tempPath); + } catch { + await this.safeUnlink(tempPath); + throw new BadRequestException(MESSAGES.UPLOAD.INVALID_IMAGE_FILE); + } + + const fileInfo = await stat(tempPath); + return { + tempPath, + outputExtension: '.webp', + mimeType: 'image/webp', + fileSize: fileInfo.size, + }; + } + + if (type === UploadAssetType.VIDEO) { + const tempPath = join(resolveUploadTempDir(), `${baseName}.mp4`); + try { + await this.compressVideo(file.path, tempPath); + } catch (error) { + await this.safeUnlink(tempPath); + if (error instanceof BadRequestException) { + throw error; + } + throw new BadRequestException(MESSAGES.UPLOAD.INVALID_VIDEO_FILE); + } + + const fileInfo = await stat(tempPath); + return { + tempPath, + outputExtension: '.mp4', + mimeType: 'video/mp4', + fileSize: fileInfo.size, + }; + } + + return { + tempPath: file.path, + outputExtension: extname(file.originalname), + mimeType: file.mimetype, + fileSize: file.size, + }; + } + + private async buildFinalFileName( + absoluteDir: string, + originalName: string, + outputExtension: string, + date: Date, + ) { + const timestamp = this.formatDateToSecond(date); + const originalBaseName = + this.normalizeOriginalBaseName(originalName) || 'upload-file'; + const extension = outputExtension || extname(originalName) || ''; + const baseFileName = `${timestamp}-${originalBaseName}`; + let candidate = `${baseFileName}${extension}`; + let duplicateIndex = 1; + + while (await this.fileExists(join(absoluteDir, candidate))) { + candidate = `${baseFileName}-${duplicateIndex}${extension}`; + duplicateIndex += 1; + } + + return candidate; + } + + private normalizeOriginalBaseName(originalName: string) { + const rawBaseName = originalName.replace(/\.[^.]+$/, ''); + const sanitized = rawBaseName + .normalize('NFKC') + .replace(/[<>:"/\\|?*\u0000-\u001f]/g, '-') + .replace(/\s+/g, ' ') + .trim() + .replace(/-+/g, '-'); + + return sanitized || 'upload-file'; + } + + private formatDateToSecond(date: Date) { + const year = String(date.getFullYear()); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + + return `${year}${month}${day}${hours}${minutes}${seconds}`; + } + + private async fileExists(path: string) { + try { + await access(path); + return true; + } catch { + return false; + } + } + + private async compressVideo(inputPath: string, outputPath: string) { + const command = ffmpegPath as unknown as string | null; + if (!command) { + throw new InternalServerErrorException(MESSAGES.UPLOAD.FFMPEG_NOT_AVAILABLE); + } + + const args = [ + '-y', + '-i', + inputPath, + '-map', + '0:v:0', + '-map', + '0:a?', + '-vf', + "scale='if(gt(iw,ih),min(1280,iw),-2)':'if(gt(iw,ih),-2,min(1280,ih))'", + '-c:v', + 'libx264', + '-preset', + 'veryfast', + '-crf', + '29', + '-pix_fmt', + 'yuv420p', + '-movflags', + '+faststart', + '-c:a', + 'aac', + '-b:a', + '128k', + outputPath, + ]; + + await new Promise((resolve, reject) => { + const child = spawn(command, args); + let stderr = ''; + + child.stderr?.on('data', (chunk) => { + stderr += String(chunk); + }); + child.on('error', reject); + child.on('close', (code) => { + if (code === 0) { + resolve(); + return; + } + reject( + new BadRequestException( + stderr.trim() || MESSAGES.UPLOAD.INVALID_VIDEO_FILE, + ), + ); + }); + }); + } + + private resolveReadableHospitalScope( + actor: ActorContext, + requestedHospitalId?: number, + ) { + if (actor.role === Role.SYSTEM_ADMIN) { + return requestedHospitalId && requestedHospitalId > 0 + ? requestedHospitalId + : null; + } + + if (!actor.hospitalId) { + throw new BadRequestException(MESSAGES.UPLOAD.ACTOR_HOSPITAL_REQUIRED); + } + return actor.hospitalId; + } + + private async safeUnlink(path: string | undefined) { + if (!path) { + return; + } + try { + await unlink(path); + } catch { + // 临时文件清理失败不影响主流程,避免吞掉原始错误。 + } + } +} diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts index faf38ef..1d2328f 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -38,7 +38,7 @@ export class UsersController { * 创建用户。 */ @Post() - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) @ApiOperation({ summary: '创建用户' }) create( @CurrentActor() actor: ActorContext, @@ -61,7 +61,7 @@ export class UsersController { * 查询用户详情。 */ @Get(':id') - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR, Role.LEADER) @ApiOperation({ summary: '查询用户详情' }) @ApiParam({ name: 'id', description: '用户 ID' }) findOne(@CurrentActor() actor: ActorContext, @Param('id') id: string) { @@ -72,7 +72,7 @@ export class UsersController { * 更新用户。 */ @Patch(':id') - @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN, Role.DIRECTOR) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) @ApiOperation({ summary: '更新用户' }) @ApiParam({ name: 'id', description: '用户 ID' }) update( @@ -87,7 +87,7 @@ export class UsersController { * 删除用户。 */ @Delete(':id') - @Roles(Role.SYSTEM_ADMIN, Role.DIRECTOR) + @Roles(Role.SYSTEM_ADMIN, Role.HOSPITAL_ADMIN) @ApiOperation({ summary: '删除用户' }) @ApiParam({ name: 'id', description: '用户 ID' }) remove(@CurrentActor() actor: ActorContext, @Param('id') id: string) { diff --git a/src/users/users.service.ts b/src/users/users.service.ts index 594de0c..94c0a28 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -30,6 +30,9 @@ const SAFE_USER_SELECT = { groupId: true, } as const; +const DIRECTOR_VISIBLE_ROLES = [Role.LEADER, Role.DOCTOR] as const; +const LEADER_VISIBLE_ROLES = [Role.DOCTOR] as const; + @Injectable() export class UsersService { constructor(private readonly prisma: PrismaService) {} @@ -202,15 +205,18 @@ export class UsersService { actor.hospitalId, MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED, ); - } else if (actor.role === Role.DIRECTOR || actor.role === Role.LEADER) { - where.hospitalId = this.requireActorScopeInt( - actor.hospitalId, - MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED, - ); + } else if (actor.role === Role.DIRECTOR) { where.departmentId = this.requireActorScopeInt( actor.departmentId, MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED, ); + where.role = { in: [...DIRECTOR_VISIBLE_ROLES] }; + } else if (actor.role === Role.LEADER) { + where.groupId = this.requireActorScopeInt( + actor.groupId, + MESSAGES.ORG.ACTOR_GROUP_REQUIRED, + ); + where.role = { in: [...LEADER_VISIBLE_ROLES] }; } else if (actor.role !== Role.SYSTEM_ADMIN) { throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN); } @@ -682,36 +688,7 @@ export class UsersService { } if (actor.role !== Role.HOSPITAL_ADMIN) { - if (actor.role !== Role.DIRECTOR) { - throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN); - } - - // 科室主任仅允许创建本科室医生。 - if (targetRole !== Role.DOCTOR) { - throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN); - } - - const actorHospitalId = this.requireActorScopeInt( - actor.hospitalId, - MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED, - ); - const actorDepartmentId = this.requireActorScopeInt( - actor.departmentId, - MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED, - ); - - if (hospitalId != null && hospitalId !== actorHospitalId) { - throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN); - } - if (departmentId != null && departmentId !== actorDepartmentId) { - throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN); - } - - return { - hospitalId: actorHospitalId, - departmentId: actorDepartmentId, - groupId, - }; + throw new ForbiddenException(MESSAGES.USER.CREATE_FORBIDDEN); } if ( @@ -740,7 +717,7 @@ export class UsersService { } /** - * 读取权限:系统管理员可读全量;院管可读本院;主任可读本科室医生。 + * 读取权限:系统管理员可读全量;院管可读本院。 */ private assertUserReadable( actor: ActorContext, @@ -749,6 +726,7 @@ export class UsersService { role: Role; hospitalId: number | null; departmentId: number | null; + groupId: number | null; }, ) { if (actor.role === Role.SYSTEM_ADMIN) { @@ -769,30 +747,36 @@ export class UsersService { } if (actor.role === Role.DIRECTOR) { - const actorHospitalId = this.requireActorScopeInt( - actor.hospitalId, - MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED, - ); const actorDepartmentId = this.requireActorScopeInt( actor.departmentId, MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED, ); if ( - target.role === Role.DOCTOR && - target.hospitalId === actorHospitalId && - target.departmentId === actorDepartmentId + target.departmentId === actorDepartmentId && + (DIRECTOR_VISIBLE_ROLES as readonly Role[]).includes(target.role) ) { return; } + } - throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN); + if (actor.role === Role.LEADER) { + const actorGroupId = this.requireActorScopeInt( + actor.groupId, + MESSAGES.ORG.ACTOR_GROUP_REQUIRED, + ); + if ( + target.groupId === actorGroupId && + (LEADER_VISIBLE_ROLES as readonly Role[]).includes(target.role) + ) { + return; + } } throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN); } /** - * 写权限:院管可写本院非管理员账号;主任仅可写本科室医生。 + * 写权限:院管可写本院非管理员账号。 */ private assertUserWritable( actor: ActorContext, @@ -807,25 +791,6 @@ export class UsersService { return; } - if (actor.role === Role.DIRECTOR) { - const actorHospitalId = this.requireActorScopeInt( - actor.hospitalId, - MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED, - ); - const actorDepartmentId = this.requireActorScopeInt( - actor.departmentId, - MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED, - ); - if ( - target.role !== Role.DOCTOR || - target.hospitalId !== actorHospitalId || - target.departmentId !== actorDepartmentId - ) { - throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN); - } - return; - } - if (actor.role !== Role.HOSPITAL_ADMIN) { throw new ForbiddenException(MESSAGES.DEFAULT_FORBIDDEN); } @@ -857,10 +822,6 @@ export class UsersService { return; } - if (actor.role === Role.DIRECTOR && nextRole !== Role.DOCTOR) { - throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN); - } - if ( actor.role === Role.HOSPITAL_ADMIN && (nextRole === Role.SYSTEM_ADMIN || nextRole === Role.HOSPITAL_ADMIN) @@ -878,17 +839,6 @@ export class UsersService { actor: ActorContext, hospitalId: number | null, ) { - if (actor.role === Role.DIRECTOR) { - const actorHospitalId = this.requireActorScopeInt( - actor.hospitalId, - MESSAGES.ORG.ACTOR_HOSPITAL_REQUIRED, - ); - if (hospitalId !== actorHospitalId) { - throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN); - } - return; - } - if (actor.role !== Role.HOSPITAL_ADMIN) { return; } @@ -911,17 +861,9 @@ export class UsersService { actor: ActorContext, departmentId: number | null, ) { - if (actor.role !== Role.DIRECTOR) { + if (actor.role !== Role.HOSPITAL_ADMIN) { return; } - - const actorDepartmentId = this.requireActorScopeInt( - actor.departmentId, - MESSAGES.ORG.ACTOR_DEPARTMENT_REQUIRED, - ); - if (departmentId !== actorDepartmentId) { - throw new ForbiddenException(MESSAGES.USER.DIRECTOR_SCOPE_FORBIDDEN); - } } /** diff --git a/storage/tmp-uploads/0bdc181b-5a5f-4911-89ca-d5c56bee0626.png b/storage/tmp-uploads/0bdc181b-5a5f-4911-89ca-d5c56bee0626.png new file mode 100644 index 0000000000000000000000000000000000000000..7567f6d4c156e2dfe2d580b94a7a90ab0fda2503 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={WI14-?iy0W0Uw|;<*6N^aprEy< zi(^Q|oa6}?xEE;onhO+dU?fK$1 gyU($CS1B*UAMOPC-djKR0F7brboFyt=akR{0DyfeOaK4? literal 0 HcmV?d00001 diff --git a/storage/tmp-uploads/1f5bd907-8f7f-44e8-b1ed-499f6c5fd604.png b/storage/tmp-uploads/1f5bd907-8f7f-44e8-b1ed-499f6c5fd604.png new file mode 100644 index 0000000000000000000000000000000000000000..a27279fa493ec7406e8e1ff7059b15e36a23160f GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$fBwreFui8{)*O?S Q4iskaboFyt=akR{0GksJO#lD@ literal 0 HcmV?d00001 diff --git a/storage/tmp-uploads/23530285-2dce-47e6-bb9a-9b0a5a5a9e4d.png b/storage/tmp-uploads/23530285-2dce-47e6-bb9a-9b0a5a5a9e4d.png new file mode 100644 index 0000000000000000000000000000000000000000..7567f6d4c156e2dfe2d580b94a7a90ab0fda2503 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={WI14-?iy0W0Uw|;<*6N^aprEy< zi(^Q|oa6}?xEE;onhO+dU?fK$1 gyU($CS1B*UAMOPC-djKR0F7brboFyt=akR{0DyfeOaK4? literal 0 HcmV?d00001 diff --git a/storage/tmp-uploads/39d2aeb7-9e1a-4576-8e52-d278aeabe6e0.png b/storage/tmp-uploads/39d2aeb7-9e1a-4576-8e52-d278aeabe6e0.png new file mode 100644 index 0000000..eb0e729 --- /dev/null +++ b/storage/tmp-uploads/39d2aeb7-9e1a-4576-8e52-d278aeabe6e0.png @@ -0,0 +1 @@ +upload-SYSTEM_ADMIN \ No newline at end of file diff --git a/storage/tmp-uploads/59692c02-df26-4da0-bffe-0142feb634e0.png b/storage/tmp-uploads/59692c02-df26-4da0-bffe-0142feb634e0.png new file mode 100644 index 0000000000000000000000000000000000000000..a27279fa493ec7406e8e1ff7059b15e36a23160f GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$fBwreFui8{)*O?S Q4iskaboFyt=akR{0GksJO#lD@ literal 0 HcmV?d00001 diff --git a/storage/tmp-uploads/6bb38cd8-3ccf-4d43-8114-32759924c246.png b/storage/tmp-uploads/6bb38cd8-3ccf-4d43-8114-32759924c246.png new file mode 100644 index 0000000000000000000000000000000000000000..a27279fa493ec7406e8e1ff7059b15e36a23160f GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$fBwreFui8{)*O?S Q4iskaboFyt=akR{0GksJO#lD@ literal 0 HcmV?d00001 diff --git a/storage/tmp-uploads/a789898a-215a-4ab6-b8dc-5bab6d040b90.png b/storage/tmp-uploads/a789898a-215a-4ab6-b8dc-5bab6d040b90.png new file mode 100644 index 0000000000000000000000000000000000000000..a27279fa493ec7406e8e1ff7059b15e36a23160f GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$fBwreFui8{)*O?S Q4iskaboFyt=akR{0GksJO#lD@ literal 0 HcmV?d00001 diff --git a/storage/tmp-uploads/b5c90100-1466-47f7-b765-063e05b0b32c.png b/storage/tmp-uploads/b5c90100-1466-47f7-b765-063e05b0b32c.png new file mode 100644 index 0000000000000000000000000000000000000000..7567f6d4c156e2dfe2d580b94a7a90ab0fda2503 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={WI14-?iy0W0Uw|;<*6N^aprEy< zi(^Q|oa6}?xEE;onhO+dU?fK$1 gyU($CS1B*UAMOPC-djKR0F7brboFyt=akR{0DyfeOaK4? literal 0 HcmV?d00001 diff --git a/storage/tmp-uploads/be5cd629-4d00-4542-82b0-b1b43ce693fe.png b/storage/tmp-uploads/be5cd629-4d00-4542-82b0-b1b43ce693fe.png new file mode 100644 index 0000000000000000000000000000000000000000..a27279fa493ec7406e8e1ff7059b15e36a23160f GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcwN$fBwreFui8{)*O?S Q4iskaboFyt=akR{0GksJO#lD@ literal 0 HcmV?d00001 diff --git a/storage/tmp-uploads/ce5177bc-3dfd-4874-b672-4bd90865841a.png b/storage/tmp-uploads/ce5177bc-3dfd-4874-b672-4bd90865841a.png new file mode 100644 index 0000000000000000000000000000000000000000..7567f6d4c156e2dfe2d580b94a7a90ab0fda2503 GIT binary patch literal 137 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1SJ1Ryj={WI14-?iy0W0Uw|;<*6N^aprEy< zi(^Q|oa6}?xEE;onhO+dU?fK$1 gyU($CS1B*UAMOPC-djKR0F7brboFyt=akR{0DyfeOaK4? literal 0 HcmV?d00001 diff --git a/storage/tmp-uploads/d1551d21-3fb1-4478-9b86-9f33cd5ad724.png b/storage/tmp-uploads/d1551d21-3fb1-4478-9b86-9f33cd5ad724.png new file mode 100644 index 0000000..eb0e729 --- /dev/null +++ b/storage/tmp-uploads/d1551d21-3fb1-4478-9b86-9f33cd5ad724.png @@ -0,0 +1 @@ +upload-SYSTEM_ADMIN \ No newline at end of file diff --git a/storage/tmp-uploads/e3b5edd7-6087-4f8a-a059-af9f720c26ef.png b/storage/tmp-uploads/e3b5edd7-6087-4f8a-a059-af9f720c26ef.png new file mode 100644 index 0000000..c9d83be --- /dev/null +++ b/storage/tmp-uploads/e3b5edd7-6087-4f8a-a059-af9f720c26ef.png @@ -0,0 +1 @@ +fake-image-content \ No newline at end of file diff --git a/storage/tmp-uploads/ff32fdc5-a0d4-4652-a853-e3bc6115d8d6.png b/storage/tmp-uploads/ff32fdc5-a0d4-4652-a853-e3bc6115d8d6.png new file mode 100644 index 0000000..c9d83be --- /dev/null +++ b/storage/tmp-uploads/ff32fdc5-a0d4-4652-a853-e3bc6115d8d6.png @@ -0,0 +1 @@ +fake-image-content \ No newline at end of file diff --git a/storage/uploads/1/2026/03/20/05761514-b28b-4daf-a1c6-5c07d4bc85a9.webp b/storage/uploads/1/2026/03/20/05761514-b28b-4daf-a1c6-5c07d4bc85a9.webp new file mode 100644 index 0000000000000000000000000000000000000000..893d611429479dc5cec4eb32c6cda7f88177dfd9 GIT binary patch literal 161480 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/Tfc z2M%2GuJ-}mzCET+_)!``A6zU985~VaOV~>q)C=~n&evl!?qN|Uf!=MEx6-mDOId%R z3G)prft@rXcjCFQXbonn_$lw+r>)0IfRS4^oAeb#caA- zA?K6j_%%+8)Uc2`=?)_21U&z?*!k+dwm4qGpZa51p)b1LCjYnEz9>srE+Lm<1+lBP zFM>$#sjgBSGUo=lTPfT7O`#<6;qxu5JcD-13Pgs4;aOvP%Sj?DS0!a!!s`MdBsxAk zJAv;b2jBBLVco`+cY!=#G(;;_pPb{E3q`2QN;~-KrbE6~O<#+25q8t%&}-E-K@BpK z!Km1qX1A#HFB=?K!}3J!>j?n>^Ew8jlSw?0wKxSN>a_H7c+iu}*WCsZVw*dACQG>EuWbSy5V}eX7!jX3}@jy)neh&4M)Y?NOYVj8Rf=y~M zquD#{h4ihAut;?*E@o%C>ex^bRXgIot3W=4kj&V#^XjcMuNDFAA5y-wH1L4X6Fa6n zf2zQ_&hK%?Qu`N3&v!|eOTUeU+01hUDmMqtCS|ds*_;QN4!wEijX$!^P<(c9nUbJ> zzPWrB?)IXsE^Gc|q*%I?^5WbeUboK=1-FzYAK4uX$S99YaBE`X)*N5dtFJ!El40%V z@?qsMuASgg6z2%=fFtK@wEzHhEwV98o8aj?l@R?UL1k;}(k&TEpdY`ULV4INtJmM3 zW-Ba*PC>a&}fI2E4!AAhCnp%!(o_0Wf-Jb&iflx)yvu-oAlhV*?jJ zM@&5dSrxz2~Z zBy$#ay-#OYrW7tFY<0Y|;QqzBAF|X)!iLs~$Yp{{W$HtjjNrscnVPTg(aQaV(#MQY=Uj5vVv$eLADbj(^8S?f? zyZ|z;nf+Lk?Qi#3k280X3gPoD89-4JeyD`yW*WOp|5BjWU~ncR(zY4ghQ`P0!K|`=^-# zsko|(n4z0EmC_V~=n`4S>I$*=Cg#4c5QRFMcgO~uLit%bwuWuClGW33U%pfv4(U8} zcX4yoFdDAtfztfEQELPrU3hc)TIyK9dt5e!wXP=QU~&OBvK$B?-;>mK1INrOXOkx` zL>fClVxc%`ypkOccCTGqk8J+RslKj2*iu=EDC(LOny&xt>X)OIU!IG84;e3~uH&|O zR_rU4#aoFUAMDt($i-$kT-e?c4@2 z0#@^VN%#dE$mkk6PyAbI9t08iy-NU(CPUc%jxm6Ym}wRGgX%o({)DczsLkps|4kv< zo)qzPq4f!%hGl1Vx(K3uyFTYo47d?=e*AK+yz0-L4=98NNI$({o0mp>Y$+3`m`!9O z7bn`1X|IOb6;bsApemZkhn$7+1bEjEUfqg&2N-r7H@;mokNYgeMupx@n{>QEGi*La*qHSiE8yJts#>yIS^OS2? z^Rmcayetyu5J)rEzK5pBqyiibc6K5aP>lN`Yo-5 zo^}tq=IIDCT?N|20(ht}o~V~-9mG@T9+UCg6A7yx`XH~g`k5p$V31n%FcI@N=$*3U z{jAARicr z9r?re?qx~vSRA~Lo2*KIb;QC=b2t-Se@R5`BBWu72T|VRX!xZCqMn)9__@dD56n%q zz<+y#UIz4jNqY6U5&yj^xu}$k48zST8j|oY-*n}AmF@u=^ubWJ;fd4JXjM~j^AthR z-Ah-O646<^wYQ1u^qECP5XmQ@liOM!oR>{^u6s_q@;3C>=WT+_YCORD6szLU()G*&G6U7qsg;VC}$m8X86kQPf zaU_etsHArNcy6$UuK7w7DERWhA?(}6cPIdq*!*exU>2ww_%A9?jK3|5da-nB^fZhX zJ~^q2mmTMC?wD_vqFkCWS$+1O@lCLnkxWW7M91gkysK@3jX-OK3yu)t#EXj+;eixH z^1QvTuctl*EwD9aKcPB=UgZK;|IQ8)EgK(sljkM{-|sIM4_!phm)<#Hr6HcDu;fi) zKxxV9-?|5rX|}(p&sXxh&e#Y6*(p&)KSX5&pW0g_ zgGU~-NC<(pUkLNv=h$uS)rc-Q%dv@0p+uFyfg~d;2lDOF>wUt#-Tsd$3Tuyf(H57! zuvTjuQ=ExFhNEP|p3DM}-~p%eOt&HtBsJM}c8D-32`ZP-13%g59P~QX;l5%PmhK|# zyVr)FRmDK?Luz7qZFssG?(okmC%Lo6votwmSXD016>Z2-cCRHd?B;07R&paUE(7PS z9});MY?vz&3K|PctTCSZ9k;lbw;LHdB%s5Aj};(w8#oprX8bUlRkA1Q=_;MkcEgBK z;0VF>uwA2lqiSrA+ns=!1i0zu85i1?wx3VPQcfV~a2)wk1~}3wXc4AYt{TpWvi^j$ zvf#gD1k;PuKpa^=$($Lrt1H{rtSH+l`)E8Q%`TK6-S%+daAzoO1M?a;E?j zc_+=f#~jgMnjwPbOZ|D;X1=kO(*lygVk89TMNm3t3GS)}+Awhb=xy|U2B{oI6TG36YIa)(l9Q$}k6tn0y)z5c88iwgPv zMYtL=>O>Z`xyB6~)?;YzFMZ(9e|D|eZ}L6{A&4HK9h>KsR|gY;ox@Qkam&x%_X+g= zOXkY6s%G~Istoa6@I~V%kbvC7|PCg^g9KKbL zI#k$FOLO>Rb8?eEms{iy!P z+V_R*8NVR$f!gD6|zRd?!oQyd{M&#l#ibUVN<%$e!zMO`A8d zAJhWAB#Ua}(FkL!R$ll+tM5Rhk$w0GOlNNTJt8<{D{&DEvNd&V{qX87Cx&VK#&+Uy zL&k7f<)*DNvYX5|jo=tW5iwHW=WN6NRc`gwH+Oqa9;)S6EiX^1rGMZ~jLE z02fz|R*Eu#f-9~g9iS;-SQ(L+LD4JAmN+~L4Dag^PBg6-Ukv9Fe@QP)n>HCfleC`G z!gIB9mWt8yoTGcvF{N9@pgaomCF0^}@1~ySeVzCt2?5D8a+T@dJ#7^>m_I%;1jd}` z!22%u5L&g0w=3z6m0G@|S&M+JV{{Y=8P>_sT`(TMV{1@)PP^h|o|)PkM{aI+m8f8C z<^beADcHm;)iM99uKlId;K5Z}78alx80q*i{gXNd56PK9dEyE4ULC@exp^>UZ~^$o zxW)9cOXsI0twL_c9jt>xc_uLO$i-+pfkFV^`^sCHE}5~APwvLZ5)wueBBp++tOU5` z27jH9Y3;dF@Fi9Jo9l)3V>lAIa>HhBi>`R322?jugCBNwbT<7SJB1^WLa5POSJpB& zB{Ak^)XATPY_Y!!NPr43K`2$V%07L>fCFg^CDKJcFBB9tCt73&J=DF1dcQ!C~`4#AY zVT~ZqA@oOi`MQN-Kjlg+l?iOXPUu(CsS{xV^Dv%Se;F)6@ZOfL7~!LVZ<3lcc|>Sy zvn(%g(nt?^zy)D5q(INrKbp!K6nu{!${jrx$Aehwp(-yP_Ph_48E^0$x{}?F5%Lp4 zq~gTk+?_%;FrNrX(e?<**f_K-s7ngrV$e@4(8E1#ieEXmuSi0jNUx5-j1 z)Ue1y>s+rsbYk33Qqs;0yRUc7)xXS0DG>2Z^3^*0t-^ToRWH=70 z0pICT_`6sTgFj8b*#$I-)rRsW>7tkU?-<{^B>zz#BhJ?{U~zlVV;t5CzQ|Ta8)mTO zGd5BN+aKfpg@TBm2zIU$gXunKuA8_+w>cjdPF1^CGe(dpGt>hvg<98a6AJ9s z%9Tmqs>6cQsW(nk?aaJtiQ5FTS;bcx<)X|m9_m+>3HIQ5x|sHRY{VTC?Mq&-t4bYP zmA-GHWkx02d6?8FDc0Yog_j?BGrV24+Lx=ZOI?hfHZ1Ue`|5a}2K43R$-`}rk(6t= z$x94W2+G4b|Eo82S}E}0IU#BJ%R zaV<_(hDm)KQcocvl&qWX6r=*xdax)Y$laS-=Off?EHm;|05fqlP@im|A@ecKEP{O7<(PylKI{JQStyThY?UGGVn#{6E=1)IsU|~JN`O_sLm2?R;TDHj z)-3`Wd4G0r1ix8K#qs6qJ+l*aOZRwn#}tEJ&DBV>)7CU_9Gs@vA-M3ifNh)R-*lPDA`Hz`3DIwYta6@kDXe8!h_ZU> z2E=7;&hW_w{y?b&*BUkZv-J*&`fk~-gmpDMo4|*-Qn$2Qd4Ws1nVL)TY*H}=goS^8 zciQq~GAC-RpICcaQ=r2ScLNX5s)lg>w5N(inikw<{C}2b|6=#=aMP!mA|hYq-h!1DeSlhGr+Y7lA1^ohV0CQ(?Ky@K(C*7 z8VlvKUzZl1O+-ho`I;z3lM{UQ5u?R&c8fy;rejL(lTITTW6I;CYeHRCcHM`X$Bkg6 z_*26moWaOBr0QMvI0#N0-cgf+l-Jg%E7F{U1+N8%XIAL3$$`c|(;4t2?mPD668^ZS zHOEtn0$*Zx-*?#-3?+dR%F;$^Z2Ai`BE3)zvp$4>lRl|5`QXX zCFp=3$p&zo6|R^ytta+v12TP%rcTycgi*?~QS_#p8mgInV4tI%4bR?3}V=CPlkNkSafiO43dz=;IU4eTxoMRa4 zxg5yb0&6(*GCeQxxH>RJcxFzq8(8_lAglxhqudY5wnDS}Jl~u@JX9n6q!}VsRX}#8o zZx2J7y|Y|xib;Z3cj(?&P8K(mJKRw_eh{2xFXVR~`xAsm9a^1hzd9hUbMFc zdZ9mdl`0?52$VCmHX3owf%+v@bs$}!{iiZ6zE=1yvORdo0@$wB4{pbrO>#Mv$Mz_j7-N!9)ngFEzPLsRf1sJJ^l?DYBuKvtR^H7Icl z2#AdG{yL(6(fVD1Y>sg#8CFdm>1T5TQ_yo9veZSi#$es4s{IrC8VXtCMV0VdIt?zA znBW0yv}CE{@u7z|c;qv{j}OtbY$nEJIs}r)_!}sUa!$kBqLbHsx-7YimoEfxU3Sz@ z_j-O$YzDg`iwj=qhI6Io7^g?Z#N52H!$~tH>4C(GOe_>e9&OE;mZ19rK!EB|gCjgg zZjw5!`f~-)?xthza+=R22sRx%8ITeEG;|+tpA*2LtV^u&otsra^m!anLszy8hd>$T zBpz4xj-}WvBEh=#imDoSm}rK%<0}I%6D3wZ5fLP!wRu+_N65FmLE9nuYtA+8#rsN5 zri)pE(jT@`c9k=OL5LRYkFq_*JNyElgVAY*tmt@VZl1MPMK*M-(eGKfN$X$t2A_|! z&S!JoCKXz}7NKXq1mM9I|4M^*7ZCXZJO@}ONm&0b_6C08=aG%#O-y!xif0%LbUGjc z870^?7O8*}w4r0O;I6uxx?mh5kLXih5D$Ev{OYPVYHbUT?j3m;$D2$&WCC)se(m~k zvP~xr4YOTSzF0=xMrBfg;h}8@iRS_pk`nJi>EtpQXcm?WX0}54%Xq{B@2f%+{Z8@L4RuWNpGM1gDm|G}gwq#=9SrTU(Mv?UXXD<%Kc!VH)VnmSuJ`*LLp>dDfdDyW zWTU&q)OJE2Mq7lmLBXaHYwJ5d_q*-(4Rel%SRR_46E4iu`$GPgj$I zGRt^kd8?YbI_HO* ze2bGMebk!D2&+#PFoFe71)D}x-KGM11kKNTh^k4a&3T7Ci}N_kmFY7^5WX{m^rlfk z6cSjgwRPV~NRY@Fg6|h&HXe%EnnS&?g{4J8xDZ$g_9Nuv(#h7d;zHkt_45&V6O!^W zz6CM0=Jq)ECvZQmBPYbhs~6SHol@0kk+#%wdxgDgtM>x`M{o zSzX$plLN73U362|^M|$R0GlLV&H|F05W1sDUtrY+D&XAVr?Cc`N3}nV z>KP{xQH=W#_%#PvQ;P9WZ=DOdxa(0$^^I9XkmnE~q3Bo1CwA>hfmDp>fzg+KRBs%e zWcueJ;=F6CODuPR=poCZ^t;?m3N#Vi)b%~Vn0E`qhiWX?1LuVJGN#F) zE#GHF>QUius7xHZu;%K4s@q0fueZS^p*$JN!VHJ>OHkUd@CIsI_xM;(SAC=x=65$b zcJmsW1*l)mK})KS3%ZyzHsxfrSl8D(d6k0wxd!b(?K)V&owZ0=BR1x3yXLv{w=b9b zwZW2~LBR}<7Hw|Eg99V#2vySTcqG+eER(DNDR!nyVHrTl8 z29k2NVvP1#BzuJKcF`UTZ^iLAP4%PJTWErk*D}Gs zGp2@KhE}pcPL53LI1-im{r~LMW-#_s@{6xr#)OLI0{g$r8Y;hE05$#Q0+_WNQnGHL zyVh6CS#h{5@D2MIoZse~=olPQ$-k#-+s0ihWHN%MeI>^bJBb?VdwWuje)ZYeN16Ar zB9QMW8@`r`#jTEOYdWk>i^_aagT6kU?o6Z&gYoL27)Pu$#2t%@dPOO% zMq8&C;qfiO6X$U;!^jCB#e2{S!4D)PuZ%HaKCHtIsKe--lh~T!m1+vp#3d^%z`L~k zgSmKyJ&n`xG6fD3rBUt^DpEb3do#&3Al%Mx@;cY|koG2JBP$5>0|txIPe11UUDe6S zecv1~yo1xX^FPV77+!s5m3vs}~(OSm#n(K)J3`f<7UF@o{I+&K4y(^^t1Z{oVXxfrN;$i zh{E^CuNCw+QfoH0LFE6H{V?N4*P`oGCNZmq64e_M1tK9oN+*hQZyOPJNW3V~EU6MT zu<7=Iss~WQuLS(nKdKZKWkH7Rg#x9gLS@`Dy8Xnv{6m!F)023{?`OS)O~g?dTx?On z_M@G6S~4s}7ZbGmrjf}kMoy+SaL?%0(Fnd_&(Q&_hWuFXaG)onJ^1)TJS7q);73-6Qe<}uAl8Nlfp*M_cdI=-&og-_4>qo0~?z7ql-j^T)8{K5z zW}<1?9FeYX%ks6h18x%FcH{duYpjNh^>&JBxnG z&$dDw)jXbR0S7^ma3c+Hn@wqn4I<|>QTtq5O^YyKHT6z_K1iqWc9`~B&7@LqO_ikQ zF$F)7Bhg%!Oeno$dBZGAY-iezzCRFZ#_N<}a!+Twqc$irGIDl}VPLu#*Y)FYlwUJH ztrk7YF;fZy{V0E+1B|3so?#6fMmee-94vxR3QdpzGZ}z+?l;u_k>vn-N;oZp@--Dk z)m^BcDy*au!pt$3+1TvZgml3pzv8OeaW!k{#BtTEt`x);j}bKs|7a0VwIFm66XFn@ znR3X?@Mx?l)jq1)5MYz`E9qpGUoi#X+m8;b1zFKSv~J}Y&~bn$=;&0?czNtd<+%PX zPFhSWa3^bRv*|a#FxO+OtyARCNM!3=QA+;lpy(I3hpU1*~Fo~_Tf82#kmu* z<_pVF!yhE%xOm9UuL(RU*bxL**2p9S7!L>n$_pCAkyV!(2g0vc4AP>Ocf(>qkd$#TmOH68@_|o4q%jmP7aWv82TG!NUdsNrjtq1Go zg;yVyY5M7e$%#$u#f0Hd5hfZJLw&D>R?aIwx7~^i#>t4O&evnv_P#)dvdVJl0@T)A>?8NhKDphX8H+Yxh@E??AGl_c1%y7&U?(OU(?3h>#n@4S+4`cF`48KAwF`ml%gzC)6zaKcl^}KBd{56DJduZd>ar9wYEIN8Q96^mV(PuMVLvF~S`M(vp;VzWTTAoWh)2RC+E{>o_BWYAX zg`b5q9H*&VAAY{&@4W+}D#ZJ>|D5Zf8ch(#aa>BJCuEF5VZ~&~g$Cs8?B-mf)@Sqr z#6ExQiQ^GJ?OoG_DG?1d@Ka_F&UbG`l*7>pGr-+-B%$x$t;>*@n6b(AR^FrM@yQH4 zzR5zxu~7?XS)srJg$$pQmyuE77uZm$%vv4SY5YiCExkHxHig{VMYF5cNz;{jh{h#y z;+Hn6|@?+)T zwt<~54eSSD^`Hy2x(WxLEiiK55wX8c&*jhP!&1|4he@%jMB0-QOV?wt;> za8#un2phVSLWem01fO&HaS7%TjT3jatZYbU_{aG-6@DAl>W7uqm3&o^I&N+XFyA`|%P+*FHlk zFO&$*KHnMIWc#zVT)f}P{pT_t;Y^%#aW%?gxY4_krfR(JRqE)Mw)Xc^8*BJDM=QJR za}M7ZMH#ouS>@{PrhRh=De-Q}L_0`9lc*LI{)1#W#1J5qrmv31{Ietk4&w-FezRGJ z9CHCGBN#1EV~mR3A@+gedXBD7itbGZgDrd!Ahw`h8V=GiaD8nG@r@9yujYI$1y51I zFIYFH#}JV}-{5S&s$1~CrxdDFJXjfxHs7Y05mnP#eTw&}$Muy7ev_((1q#^e3%1e( zfey24!Ze^+(cpv!iANm9T#_6RT7Kg4=n9B!+LPw)ZB+r2D(SpA+`2S%#`w6i=I|okWG31hy^lIfqaE;DcodtUgoCl|$@y7a~Y{iUUIY9uA0!l(bEshcK z=QzAf#>5^ETjR6+@yL9Y>laJ%=DaZy6NB7Nu{UVDgBte z`b+*FxB&~DeD2xE=*8z3RK@s(q$s8Er+0^n@il0%hB8BSC9ku^z|BWS_2dKHkoq~g z?_IV&&Z?V^@iQfQe{W%NaEEHi_i<9T9AQgSv?*tXRv6|fLH zbRl3-H-qfQWnu>zexkL)XYDXp)?p7Y~8j0^Z+l)hE@BorDnYbAge9#~a`;Z^} znNrrcNiq0QrQS#&wqwVkid2d zrSKTCmoSBx;o%@&>O$U}zI`!A0O9dbc^z!n+1*|UP#S04^gwi4b8G~6Hozb5aCaK46E}D=*#sz=C=!Tr^ zaj6(c|A(E}7Kl9xVg*UD2JZ(8E*_KkQE_Wj{u^#;nX$G$5Eba~9P4~j-TKZI-4<0x z%=MmHD4FL>rG7O{VgVNbwXHLs@i4hjX9=p_#KB znKR;;oYM{2aXt#FYO&26H}bO%2cNHh8qmw=yyw@UU$V;WizN0QT>B)AvRyka{y)U+ z&^qd2`8UR#X6-YWHv1?SxLgsY)K*^(xpIrSt!bU$vS3v|QUI~D6#*VLn1Q}Z;j*I+ zdq1p|n%b_!ysJm?MHKWm7~wW@$Qo#v9Glfu?CD@$w`hH_#}7l#M{i;?jS6iQkw zH@M)2f{Z$-U72LoBaJsZ;+cOfnmYnSF1cuBPCa3gyhI&|Bf(BGMA)Aa|KrycCqgcv zcy|@xNhi8X$R{WVntD&E35d%vsOrI_nLkUqFxGqO*DYnd+{CLSCn&e2UUMNRhzHh! zKtYd_67KeMc^}@wRzOhx6{zu=2Vd(-{LRekyl4W-_PC^t2_840p)Z1dD2u3{<3uGu znNR#AnbNLNiuNNH@gR%3asU?Xc;0%4jm2+*AM=CL~*%y&KcrMqShZ3MBjA|Qp{J)f0!&{4o3jQB{ zi=-JPTb6yU9XJv6O<#mN<%aYgt=|syVba^^Loh}@q#V;u+JQkEX;~bz8zEn{%MMoa zBXDhSZ}}KSOZ!Kbc-No|xaCBWv-Y$&ywHC{^IIaM)Z)sG`&!|ySb~K=I<7Y$ZWwXU zPN;Cf!jz{J@<8LF56wX)XdU8u1Y+i4>3ZQ=V*tAsN+6sCQN_Z{t%bzTwF_ST-CvlV z44GcZQ!1K$u8klscC5a=+Z}~&>>I;W0Mfch5U)_bZruj#6R|AtaZhmXTuV*_rCtmF zl=Os>{*M$?)80|aKjOei;F``hvdr|F9fMu6@Z_vJks3!9g+y16uoQ<>3Cq+&$sb?5 z&x&wKH9rc-Y+X`!DYvTgfn$13D0T~}e9}W;rpMmqnME3uFHAWt))FG90!v)P%_M&{ za1&7Q{Z4fUGec~!M;A<_gD+~nl@srmd*tdDBZ4H^*MJRdxW}L-fsQvc5xbkFVZcIg z`Ooop8=e-jV@1*XTvlEl1g$+xj6%B;R>pmvz%swc$|bjmfb7mi9F2R}y^Ao-s3+Fo z9K&b35f|8PkMxgCH83;Z@e1oN@^@`4-HGNTCQ;U!ai^l5-F>5l8QV?TL2Qk6*)yQ; zzs1m5w(4xcTy3SOP~v68LM;zd8l=er)w3WN18uw}Vw&3RAOI2@9qgo?{lDjgrmob) zw9{D`UqvjDzZ|fMqKU$5&jQ&a0q+piH$Hw5l4Jj@*Cqm3x)upo|_u;-q1(hBKN&Lb4i8GYM&iQk9-RI59+4bsm$(K^*Sg<}+`y*0E zI7_>81~%(?+*CQ63G8|ZC5IYkWextTjBSi5ndcD@EEH$Yc6;LgT%HcuAfURtY&Wmk zHR3-J3Kj(WEA87$zn>Y4G1$&^oq3-18HmvLnJ!{TG;cl1=m zDSWl@3u=DQQ`7!rZWV5l$x2duH9_qLZdOskqifu&MyvhR8!}ytboS7ehSt)hb?^$_ z>*Bw--silbN4gn07|Smj$>)7>1iBhh=r~S+3Y*&fkKJ5gD3Iq+f8yN}=l8xJO2_}= z0E?98a3O?XlWk<9Cp0Ka*baT9MM77byg#QpP%3F#0V@|Q|)^U$u!{N&* zLlx1tnvM+RqX4x=9^<8JSEi&MqL-x_^G{(;U;(ND?0_8m;9go?WTmr8FbjlKW|dPe zPY~@yV{l@IWOqd62jZ8e1lH3pVy0$ym5dl}B9HrkPQhBRY(bpFD&K@U#_ zxt>B)0y1|XA&!beOZ+le-mq-?g-;k7p9yWCCpMVqmD1@%ZNGn)}{Y zP}!-l1j}PW$Zry7H6086W#%E3p%@+rDRzUu&+Lg4(~Sr(tXN*Te@}}NNZ7%;ItDis zdkl^C*(|C+J~4Y0vFPE=B{Xa((1)EpAKfU}392SbavU_DqUp%n=$ZDQ%s5aon2Sm z*PP-eppnW#C}Fm&7CLKoi@^FZDwbJ;P}7>}Wz^*7u|j#R01^or{0-3W8=j_oMA&!i z85S#r_M!(jwto6J3=PsmJKCjZ?xlm2b42-fUeg+pQBRQ0js3P5Y*zS7$w>1D4lY^F z)R}<+gH67Ioa(K&;jsEon2-&8i#tO;PJi2Jvn-*n9JvZ-)lp9f(bm3gJrFDb6+#jl zZ8WLVi^;2iwnlDtU)UDkTB^aSlcC#);=E?^SDhxI@Vq!A4A>)T+i4t~1zT_+ZzNTq zvujy6z-yWY>osjY_!T#-=83xo3z6UY4Zf~JA!c0j=VeTZe)xFefYeuk?OPnw+*pe0 zDMr6dDqiHOW>>)pg^N_xq6|RpoVq(Rz{BDpUB|7g9B>0KcY*8d8-*<{)Z5$8u#K*h zR#!nI;*j~D5c>r;NWK*lOfqhAaHmU3{x<(AJB(={3^p_JGa2d+2WC1a$(gmRM#@Nt zp~y_f0iZ~avnA$IAt4O~rfULQfcglf>^>^(u^>pf0*^PPvzA{T@T&d9%Z;9M)zQ!Cq3L%SQ2dT34STiT|)Q^9H z@3Qy!_wxzJDP%T0Xp+_oFpC!rG9 zL*9!yMepCwR_A9RZkZ5G|AaXKJhIaGvc`yaD#VJoB~1|$y~GBeJ}tSNgU~ z!Tafy%z8jLO5u2^`51f1245`J2J{4gPC~xq8#eV-1Ykeg!_~#F=jM%+KJ#dMXC66T z=Xt#vD>Xs9X%^syDRncyK}=i)zxo#6!749D)z|sLo?cmYDpr5AlMAT{eKHKUJc+1; z&&hBDmk0D-cYs+2`s8SD`TI>)@HK&O=d~>ZRdnY!A=VFo#r#N-8FMlTA6tX^n>$(p zI;N=DCo`Hb(%munS5&-f1vOV{OTJ^nNXM2T^s!yz6pK28$@bh<2`<2j!AO{V$$3T5 z_cj)dyA0&kNU%FTJ-AW#)}?};W;fibF!)2_oFfR6JZ>EH!hf!%HqaXt;bE@zM@g^T!dfzvH{xLa_~vKP+qgbAk@LXdqcgs54gMU+shT-(8V%j zd#TiE0bo~B>ZAb5@k~z={eq%IsP+(l#caIY@nGpasL*eU2$Tx|#kusdJIOT6JsMyz z32F#;NBAg7D!A5#G%N%Lq70$8#S@MTG3s?%yI217|G)F{!IPOG8oJ)g5#Y{NhX=!+d1bO%@CrE7F;%CcDb1+wQs`UX%KB); zS3JA*I8vJ6Tc`kwz&TpJ2SNf;${tNOSwi0uU~)LeynOulZIlv?iv4#-rzkGIA1EnL zFRSKn-;IKQwVm$Qrl?flOqr>jZ?BhZ7}^ODYT1(gw<-Zv#+>Z5K)6SO+QVFMvspr# z!kRwhtJ1adv;uAM#V3zjeIO!kDwaH?xDUgVQ*@#Ay_IS!5lT&}I!IaPdR-Jwoc?A< zssyrEtP3p>MWmAX?%Pns=fN7Wfy8pcn1eG`)I8`!BGLnr>NYa##%o(GqpCPzks6xk zU?)KR*leqNdthoDtwoL@8O}@a`rhomJE5jpUH!j;A+TuO<e~m6joz_cjF5qD{^c+X z*=RR4;^}_R(7nNQF^k2mqxS-YKqGmxvt7xofHf`r-JW!t6lSkugav=`0HG-z!~24c zrmR-pd=^6QtRCJFu|oOr3fkc7f=#R%Y+FL$6z_5o3yR%+EZ(0XExxYv}6&tFK0 z0Etyn1zuTVpvLs)GmU6ZH0WKMUlnMhf~>6N!MKK{z3@ey)YFo_O9&KSbgt-f@LTqU zGcm1S$n9rM>E(wMlC0f0W{1O*bprkR486h^B8Nf#L6s=ZlH&PT;43&h#*~94t1x=c zPfy=8h*Xx9)q%t-6Q;lCc4V!kr0z`!!`T5HfnRz%C*dkN!TJfRT-6{`Aa!!K08&zq zzOqNBo^Ed5F8}ggGM&)x<0=czg<^o(9<}Y@Eb#yjPLW37V&DcyX7j)wnCqpkFD-r3 z{$HyY7_eOGENi$m;4xOZxFCV`jCT8XNd589; z$b1=US8sr*C0xTsy4XsLjh(d>#z1@3Iu17p3BaUM0Do(-!F4kczeq`dlbV9_r!}=Qdy!BkrrkJ;)($#GNHXKX&*+N7@+Wh&{{E(GWiRDOa_@Bs zXkW~)(0b`xALws?k~JG&DQaA#yo@cucR|M%)L|HGizCmru1T~$=nb|JlAM~+i_#F64u1(F_o@RGA9L)ie z^hB6EN%EmfLZX1Lb(ZpIEeGnBdLnW{1$WwbNrE8Ll=Z{`r4g0RM;qr=Pw3moq#RVe zR@-F_m7uNph^{+(8;7*J>%{AR*<#Kp0kJO1u9q zI`oe9i1l0QbB_-^ZoQqvJ9)Xg6~F7fTj6!%BC zuq<3yRqZ-gx@5-&Zh_FLkZ$%JG8!zN>W7JFxAT&2zngc8dw99Z+P~BP86Hd2uN5hB$M*dh;|#g)k2nK_ z-DW>eSI%F>mBYV|5M6bK@T8gKC3S7ejEL#0P04VrT)PZV1OXCF_TO#G!}!6ef!bdeg6w;l zV0A;r$=5pA!@DsP_X?oo*IH}qU8whF>t6s_c4BZoW9j#-s&8UM_G#((jpPla5!rl% z-V_tj_FL-PNgC@PrqVvlx`KA3;N89jaLU%yz<2VZC?eTc{lC5Xv-d{l(y08wk%Vm| zWF5i_08yeYM{l&|as{#Sh&0P$DxMk$c95h74VzHR5l0E5nZasKZw)GKLt|)A^Q`e+ z!J>dRbxgSfdgbSRhG+pkn|Ol})MksoN|Ug+i?h^NC(%p5b4becFaK~hTa=e*^Y^r zWrPRuyUSYRdU5|oL{eCo>Jky$uim7|13mVPqZ<&=8q6s)g}t0Hg(FS5mX{!b<&*dT zFyvi}&3v7OXqI7?l`V+3bs1lzykmmWXa#i&wo~Rw-~QHkrG!E2fjBO{+F1Lmhxx0a zv#k?Ng@Ej(u*Rn4KMApUj zfXZK1a$b^W-nwZt})jismRMZo1bN;3W*sJY45NU12jERcqeENW1WRCa?zJ+-Zj zTA7Y`BXI0dkol1VnCiIxd{lMK{KPsjW05Wyfy7*7`Twk2Ejmm9GeFG0owD!x>If2D z^yTD~hmKOa>E}!pH8H$d-dGX3$0{nebU-zfzFe@FYgjW?*c#ox z?Osf<2P=?mRSXY4MI=@AW?5302%$?ed9Skk9Ss~ymi%l1h0z>wO^h9s@4?E`y$iN- zqXCf=jyEUQErAV|{i*^-=Jkw=fBIBV^9Xm?BFvtD$n}blSk)KPYH{nly$|pwin}oW z?PaksJKOLqD{w(SEO=4dk=`wl3VDs@(^EJam=J*G8{y!+Sdh2Bc=uk7nQZ$|yV z>UwP?CwZFPE_}vS-eanY)La!~hyP{pXyDVxXMe!DLO}h55UN;&qiM3CX;6Mymqv*+ zlMsQ}|M^g5D4YYF4C=j#Q%pfS>}<+%f$XaAL0?|o`I{!j|6^>+6}$gkC3miY7H2pA z@>!sQZ^5>SJa=gjPS~^&pu$tfG@TKLTn)k>ea-gL%HPyqe51YP12WRZxy?b51vWiX zW47rF=+Nxb9`YlQ^!PjgD6@*At&KX9Y1F)qt+6gyPtfZNWL0)b@NXQhiwH|`q`6ke z!HP8!lMSV&OApz#FE)aa`rAgR9;jU|64m)II`^aRt1$_)8qXk$|fI z>{ja&yu1<(6cZz*gW|MT6E*QtPX9M z{8Zs&+t!U3lf*ZC*W_^Idx(RpMPcI@5{d?j0XSC>^@lS)U?`vR7sv<5`iSey0T+U9 zly4gDu%&=vF?5BeG{f~+Nxv&3kG5gvnexfA^y9n*lAjke=;ci-zM3%F&!7I$YlL&oaKuf zWALaat>>Z8`sQ4kc-WY8q#fAI&#<0VV@RIZ?Rbsd+jN8^Ssa&fMHsi|x#0=F?^!e% zaH1(>5$&oDk(3{Z1ur;y2i^;xhiKSP%&0-Z zqt4{lCWeDi=z-ozZz17HM>cyXw0UwhC(>^u?jds^j+-)WHJ;BDZGk>XtqSJ7*h&v&t)k(1@%yb3BSt>Pev1W4p&`^yGd;l~H{3-AM z56LpTR`b?bg929cf2;&*Vf+_(#hvBK#BX|qsI-q!D^BM0`$^%yJhB^fA;)t_118%@ zya7miM=&v0sPPOtVjXJK>pSE^&xUqgCds(04v%Q}8xG+-Ee(CKmM54^rIBLWC&OYxJ^iS1}^6(0(QEafH%%L)2@E_HnFo8^mJa| z`C+di6cto{U`H6(HuGMsUYvYf@)h8I&c&aI*$#lMlrxQ*X*O57+ywo$QUYe>4p%P4^Gkiy+c@-9!(><~^== z3j4(h#g7H_Gw6kE>@7DQ@6h_4Gl9rM?7@2EV zYO)%JV)(Vsh>`-^Ub!g?{L@gKPFoA!%Si{Gl=zuK7Xs{QxJ2w=&_}!%N~M`2^U`}- zt5Q?#ALuTZ-JHp;A>o*RIE6jxiI|A7hqK2xkAcGsf#C7xZn9LexN@|r(sQS*BJM3F z6cAwkH74-M_07U6cI&tNOYmNWV?1JWH=dB79ayN^Y6}B-nmb3qr%Ul&2o4~huIh#L z8uht;oSxlP`0YDUldV}r0|U1^iIO7(mV%9nymE4}HVKB>SF_xQ_mHS1LOdu0k@MBQ zlbJXXK;=A|{MIFD)cGeII@K&w*ebZe)A6}w^(RkIU@#A^$*@BD+Oz$l#$}r#D6BZIGj6DhsPw+U;iFmeyBMl)k&Td-#rL+c- zxen7rZJfe^l}M^9fiYMfmsRZ@E^$E;)i!}QkgiCTw|=!29XX#=hGJYHJFc%bMW~=& z4W^6J@YDil|&6XL6B|RsL&3 zvf+Ul(v@`TWxHi+G~cE9c${3*{2Py0C^P&z3dWC*JQ+LK!G_h1q&iHQjH#8< zyP1N}_kqylN53d_p?&?x`s``Sg_+j54wOR+~Fu7&xnE1bdVl_zoOSw)`#~YBG z?eWrJrC7SvANmKi4`T}q_83n2Mhf5(it0NnyY~3Et;u}Gm|E1yGUUfvP z9+JhkC`8BGbWW>=$R%#LXafV7Qo>7kb8iYEyW)B19h#YdlcQCE?e=thMG4Eo&u-5u zJbmIOQXjocw3wmjNq_%h8=|Yl=|eFImOhB;M7=rNa^7C2ej!b?6L8)NK8yS|lR%Ni zf=wPX(JI()xIoU>!RlhU=V+$PJ{w#zWNeq9PW*1rgl@{&6u})ODlTj}e_#`Gf(Rw) z9Q>Kzy}9<57#bJo)oV;3SAGhZ;wBMaH5Ow&oqw9k2p<9Y^WDumOBfmOV1XhU~|cj&#H-o0y~AsLTy{+{U#P)`ic0-5>tGX{*|(( z3~!e_p9!Q$y5}g))J`X=L~~o- z#7j~l9ZITY4YqqzVzcQ+T6L^hcQ7}rh}oSQNja~{bH=wXe;nm@`H-?~` zNxG{h7?7ILeyN`T0sfItuuoByIE;!b2BGrff62Gxr8V(`X_e6Q5pi0PJyA$N@~=CD zh+C$HEb_|e37*8mk+S=zBZl)E#|SvEoDb6@IY*4CMH)9G`YIMP=piEwPv*0(@h*^w zN`f_vm+_r0Z7hvrO3UUnaqJO@yVGb{2r;boufTeS$$gf21$vh0Wpra-L0Bw2_@8rE zi&VtE98^u00kXnAg-|2fW#41RxJ$98d^x+GR^o6VRmm4Sy8M+2Y{uNTj5pXI{#o@C zQ%(~-ngbJtA@yFA%ZMXHTp|h%SZDI%?A%koBDNOanv9}KJmkxdeZaG@V(VwnO`7`v z^yar5a*{nPXwjL;Z4C+V!(^RLC)tuzq#-ea={v#3`}-kb;7cH3Nh=i5Xnd~6r0a~< zBa~%G(ue%i&jPAG7ym;;x7LAw$7cZQC6L^F6eN+qRbIoUUB83T8;D_fL!d7Tyl#`} zd6WZ#v}p-e%1JGgpELOaTB)*QoOlZU(%TD9sZHZ;y)gUIA-GOiAVr|ZHdqUG;s z9|<*4YWH_;sDwO!ANgWZvQo}BbQfH~G|GOrs65RM&~ufIKG5CK%QMIXU?bmoSP`}x zq!A2)j;o~6L^-B-{78|wQ}Gy?|OiFl1X?X8M$_ql*8}iHE`*4^;!AR;}@vk zwp0%hsXFzz7GB4j^Uf{^o_tDwmt@$Cr^CiZ))&K=zIvQH(zUQPqGUalFtMv0;;8Q3 z?qfWjjkiG%vt9+Xx%vUOOXQAK{J-$0Wp-=#XA}&gCq#w&D7FVGy)^l2Gyt2A@Tb90 z(h>uRsraiAXGu_1I&ev7z+ixg+5%O4-lKDIq?lY@biipJUeeOn+nAXHe!|4bm$-VW zG_tjU6}%m%61@)efvD+V{uTpL*~b7_j`i2!oNZP`RDJ{lEA4g&OMZpTN9n$ zyHX4~i*qADN&|!w64V;h|064ASNE1k(70EusO=Ddk!o3-fAdm}&Inx|Lo9o#dnaiZ zBu&{B-TbY6nTR7YPmBh=nT|a8EodF`LUfEjkL*?1?rHBB1qEkPzOhdh#S4}}dAx0X z1JHwm7`{I%7{ZZMGL7CoOw+tcJ;u8d1q}D$z`|10#8f2%@(bHn^k%SK;T#Kp9s?K5 zCMzfA+d=x@1w<#AygBG5Jfoz-lU_y-u{Sy4QB}Kvl$1d4e9{Yy_)I8A`TNok<{iRz z;gQgoZExPgn(1O!jL}eJ!-hYVNjvza zxEp?`sMZp>HA-X93?Ez_;~~iQGQN9lX~u=cxkojVdHs{_$908JdZePoleAfg0ynN! z>}D0#KtKnGBY#!bOJET306()W?v$R>UoJ8$X*t7=C|=tgyTSSyqeRM+xxW9k(R-VJ zQ?q8|%f#9XOc$8$f%q({o|M3k7(-7q{!Cv>OqI}fV6;dlqdY8|ieo)mOXnX$EF`Ux zCpQ2|qgnSw_XFv^Yv%D3R^ZxxzVZ%IJ~f^oQ7HBdmE|<{vfLBp;u9#*g<(C^lVRej zwWoLGa}aNL1a#gF#(l5Wx&)n@rk<5!&>B@ul^4ZcYu`n=R z|Dx&?xVn-pK!fK?XQQIeMgtJ5=xGX7#N_ug*=$e8u{m^2a~C;a+^YCG=qy z)Qg$L_lwl(9UzjeZ9@5;ocxxq(JaSPP-2-|uh`yD<2bOCbhv-@hvlHs=(dMR^OL+< zv3B5M51$0I$O6<+RP=%!Ht zBSHLJA?~cuXkf;nKkwSAWXP7xY9Gk=as~!X?R)PD++_V!Bg3S7TqF}qo1<2uNPJPZ zkYI*>^%JC8Y~*tZ2PxtKP+}So zZUpl{96bEPNpH8VlgN_*3fVn`N}sF&G!DNPJRYdzgS|B)$>BNoy0-qYn}k5|!Sk}S zhpfB~e=WwomYw=??c2NvOD(wa7##*OxT;TUDC|GCZ)pGbU8A7ke4B77IQi(qZ*RUi z4_5?*$nw@kxMSz!ogL>4pg|Z+^J1Veyu8wEaZ`DfZXO0)Av%4Qn#0+4z~h*I84rR* z%TrtYgxSi>T{bK>id1-^ee-|bCo8W*=#i=m^u0H--7_@tzaMj1ff1K>-;`isE(!A) z?93uDul(Dsz~ng(hhsDc8?y&f?)dhy-*W`FUzHl1)ptuGT3Cbub+wy)8#=a9TD~Ws zU~(BZ{GPF}0k$tED+i}BzGb~uSX;JQ@%#wLig_^;w@7QsU!o=WW6EpkXBNCtKM3-a zIC9MQasC6OZK1@fga84=p?UGfBi7pd&vHY=sP9Kil)Dzqe6iCvS#|P87~v$cgCj{RHQ ze~38so1{d^<;91KqA!amV$y4j62n)mu@KHG0JgdpT0IlQ{~ezdQyvs+;*(lvqJ{Ik zY*>5qdlWb7DnfscLtTD%+pT|Whfs=Pyw!C(UI1JrP%Q@=^T7S2+X>`TpkAh7;}ul)(Fe&3?8c?H2c}xX#yHVyQf<)^b@h;$SvPT9lQRZjw14 zr7E4lnyOhectP4N{S*1Vqd^eL(fq+SI{Y**Q4`Q^pF7X)YNb|#<#CY`wZPm8IrLa2 zagPijm1hgr(RT=iU=(W&1>vR&iKzFPMTKf7PTMi-BiQWw*H_v`>p>(5*-RW*hWj@5 znq}Nc>(bKL*sG7v!)r!n=JpYK{Hq<03&7A3(k7hLxAHOoF+eh>BpW)8T3Ls#z zwyC;8hR>si{mX?pbCOtAFrA)UY_K~^U}Mlw%7DZ+a+B+^0Fhz>gTpb_7k6|q@KVMN zk;5%iGPT)v=d^)%DnuPaMuqWZNA!8ui~AdRsY|P?)>OPFNta#0UuPR|2{}!SSs@*H z(xHr1a)={zTh(`n?s7c;5yWZAA%!J^x0d#d*&;r9dTKkVEt0goTzoAWA z4D>v~UEjX)h=?RSO_>}k4Ki{{@$Zr%Nw&WhRpgTkp9V!)ZEdBIwCI9t%@)^so$6gb z`%L^#%w>7+yLeAo{^3#%-MWumpE&%Iddz_NPT3)2+PQ-C^$7H}?~GE*+vX{59v} z=aX)yu|CP>urlTmCN~Fe5SVu+e>i?N)7God6JUp~YuoxL1Ohs!4W|V@{Px|)a3kl( z1gpFp3cG*j!_!Jd{sFgRiK2L@!maJ>5Gpd$d}=xmW~MZna06%1nmHV}nM3t;1l85- zp~KvbLVkPRd7daEmX?d8r4gsOX|QHKGnNlmcRd<Ou|LQMItX<9|1%YMl3 zvkw!N=v`Y~d*lM#Q#6_2UX575vik7-?wZXny4N&*6#GMKMWnG)noo6tZ&WGkX$Qhh zDaSdG!T3|L0g>{SJz2832mTdRd-)DK>9TH2&^?kQ0syIMli)0SWN*j`iJH{mQsCZ_ zEIV9^8k7B>e?at7FcI*v?nuz%TLnm{22A_S8CcxYH3m>(pdw(aAS-&BOB?j`mbwfB z`u+LL(|@q-R$Y>qH3hAjL;>#$u)e-v9n%|y!R?KopH;mFJ=&kCe?NW;)1;5?WQ5B_ zD1f%18s8JLI;qrTbdJ7^12w-mrGJ5pGWD zq4^mGG2{nfT>X0o4N|B8&Ido}N$vKZQ_6zYss_?DkL9Ywyn6#xp}zH(2(x8T2L_<=o#L#==gGC)qJ412((Q}- zY+A8(97mNx{$!v^mVQNwj~fxtO)&L4N8)R-sm{=An?q3WsK6IpJU4h9J%@0{KKK8s z9NmMdc&wF-eXk+fGVKTu_bNt@0#5Ci9!V@o)gZXMWM2weaXz+D4o0mpwcUKP<@jxW z!olXt^JQ*t7-5DgaSz~HBD8^n%!&<9E)yH&XdHG)Nqxy5EJ3eTWICL4W&;O#;q2}c zKO3zF`}tEj)5L<1!Xb~Sf#|MwA$7(;3n4I;>Eo;E2Dll}MkyyOF->E_JcWQ=(y9U8 zbmYvE&8Iv=vZyJ-qD$q}J} zoXG-F?L)IRRGDxcHnF;>g}Zgmfw#c-i|p z$H2WYW@5d!CE!_We2&S^s-iNXP&xEbs@C8M4nLO3G|Hal{djMS7=1>i<%L1i2S7mf zaOA=c$9QyPn`W}PbQVm)ia;CvZq-Qb4v2v%4o{hlwux4L19zXU%?egqTUYZPen!=N6o&QY&~uv|&&QAq%(}H01_Jfx&3n>r z54~v>gbiACU!aj~sWMjsM?R_;eqeaL{CaoLZpje23#PBxLGMbW+29D+ukc5XX;)V&-OB-qjZDji`lV{`vH2+(N<))+nrw}H6 zZP-s5z7dL52S!$AGM;*CK6zH+2+qMFG{}-^k-&^Y)G6Va^RR+V(r(bXF4wBa9P4UA zjSptN%Ka1kPPFV4YV38_%J+uN#qV=Io(`w0Am8f>PUW zg~<<}p#gI3C^J%cgf`!Zv9+b+6!1&+IOq=a6v+V#z`YGpFhO7GlHSW_=LhRLZ4JVT z=5x82uRh3cCO$vg`@+~QFn!-i)9drtBP=6M^dhmVZ)xemV{wN;P@$YW`-3gVbKD457Gli+q&&`=zhiAy)1`Cm++|*IPl+cQWH5qG7;>5od!kFSLIYPQiYqOv|RdQ z@AAgAll2w}#fBFA#->V{MX{@Z$Ac^T&@@VO1Wu=J5BQHUeYv3DGVcej!1B)EM+ zT(}QKNIK!;-pd2iZ!pFP0wD!%?+$QxB(3zI=B+TcPS%U7OWzQNkuO#vi;rQUu(Y{X zSvbJg&V-ql*O_E>-M&-%F?d3arN<22d&X3tffS*OmOG3rBg)YgY>k!TBk`0XM6uyapMFSyKy&fNhgX%$Y5XWD{kV_l zqbcQPr|pHf6Rd$6E6rz@f@>7HalW<2f^xq6P%{vhuekHfvS$m@>rGfapctkmAH#7_ zsdxYXySKQ_#mk^!_rKoK&F09Xi;$Q{gWh z@1?A1q7{aQ)kw0?+~O-<|1}?$d}bqx+M`S*j3na5-bLf;c1Ine6D#?eN@)S&p$X_d01(T!$exuaIN6FwECFjh+EJI?^pFX2{!HPVEj$ zX|88C0?|>4QsL-pzyMuAuh?EeN*wf!kJlt)7bA67egDaoC8u3Uvt0|A{$Wpx#{J86 zX<@vCvsZPcfA1zNT4pjD_HL;WfdT^7Brh`3s55%6>kRjM$R9HQDj%2YW)G^pSz%Vm zusTuauL;#_SgW})Po>g^H)vOLBwF@~Vr%Mf65^{}#Q*DXX^zd$QLsv^=a@VZmzJ0K8eX)^Wf*IvXJ~MkSVJYy1i7(BvQ)%Kzvn^uNhDbIt zqM_3a&S2i#cC)5!PI5Gy(s;Tm;xu(QHw=l#_QYcme_4o8W@XvU6}Ip7j^FG&4x*51 z+$g4LZ)fbCjsgbf$tC-cI~`9(;Yg>q3x{Z4&ypEp_>6hk<4g>98H={>mEEjl;EWvm zncYx^Tu+U0AeTqn!J>o*|7@A>t^=@<#5|$H#a%@E*1|-no!;`Qv#Kva?dx3c@A|uK z5E0(iFDCJ&k@Gv1V%@ye|IJo!F+FuYyi&NC95Ji;A_MZvG{W!;J zI{TX)NaB^BS!zhvn`1p*q|u9*339+CiBb4WRjW;3=U8uXQd{~`Eo83Kz@@tOn=IvD z8rNQ?VF2|Vg~n#_Y$Axj2RfALh6gpiJvP34msJB^@LQs#@X7jvPq^9XR?A&t}WUaNLt%~V*41Uz|skJ6qz_u zFQe%)pxUXR6^U5oD1>{lqR9udP?G!7h1RSK*|!|6WsAwtxqN^a?c~!q!K5whRf2&S z2FTvAw%Z>rk!6|?FdS~8Q*Ve%izhd(^q^3Iy zQJ=u%7~Gyt5^!8n%)u(B?CQN=5twieXg(AU!+x_kyXe*UUEnkud>}w1tlr!yiA5s~yyR-&Yix&7BCWUl;JRzhnGiJn z(BurcGwTLI#`=WrfSQW8e5QTBp($sX9IlP%A#ns;Ac#}_I+=`ss=cqs4*OsiYU3<9 z|CT0eiv9Y*l2;EwD{L+>RGL-!d!^M7BPXys=o_@nbkDEh*uV`1uo+rB)&EBQY~N|F zW*7&HyrDSQWKK93Fy*X$T(4tQ7jNHl|o4xska z7;`OIJ66p$WCBw>(i`d@vGMh2^}AztW*MrUaCHG}8^X-qk-Va7j%YYZ5=OQiaFV(| zXRT|K#s#LAcXC)$S{EVErSdSCqRnYK@b%oM{K-iTWu+o1sWd=wME~MVs1_y?aknEB zRiZ6EZ)*F3?2f0Yt%K9C>QIYtyqZPkRF&%@3VE))!%O4YGJSN0-cBlj&$)Lj(nUOF zB%yoMPI#_&zkP`9)f*|YOT;eTPI_2Lc6CfWm2M9)e36rAn`tJ;85ZJYa8QRVegq%* zO}atzVT|>7fGF;ABS>Tzn^K5uSI0|nzKBYK9R|03)3j!8oaiCEr^Pw~(CNsKXGP-c zDm!2pjK_%%>8I-S!~D}zS>J858IFfo)tVpTD#YQhLpff`6n3_Zt;?SKlRB5}k(|=0 zI9HL05sYRnTf`wV?)CUAq_{)`Fr$-=Ys#S-=71lZ-={QPfdGo(=9oHdA;HC$MJkO| zIh6Y0xw7hZN{sHSCgiA;tA1wUdfLD2#!R6kz^zIjRc#C4Cr+ZGqTu+jbrWPEZ}4Hf z2E)p(b&TU1VFvX6P#rw5MP1S#dufjZNi471IPZcU1dMDc1nly>ZMLj?^m-J(dSjzB zp@erpV{-8TtM<|xZi^>w1bCX8FC2SYYLfY}C-#%$^gZR#aH9p9?w}F7tpILeXDYFR z^ItE`v6x7jX#3jCDRb^qH^R0Qss+N(F3xOTIjkZ*IC3=DTLS}868(qZZD4DA7OYUR z3|1uug4u)G(XtU=na9%YuHeqT>qMei<<2R~C$doL^I9i@or;-DqdY>`VFT;Ubdp>-^}V<_K0|%B(p|+k@Op! zL-hKbOdc0$x^%i2etLE}pcMBHdU*6~X0J15pU-1}fx+uP z3Lf9?H(wMu_=a4BQY#eUq8%jkO&%2|X7@CK@4QK8(GEW0${c^cb$1`da2{)^R9^uP z8yT+HAN3skBn=C7qO`m ziFUJAs@s__=WxAX>2VQ7?%H%K*Em3_w9=rrGgdto6AcB$l+8((-w>?VspD!|?WVSf zy{qOd09cfb^s-E7Vz&mD1u`u{?edU|QsaxR+(6U=Pp)#BqIFf!gk@(lXLG=k5qA7Y zSPd05P-Zdf9<2Y|Ir(8=(e#Pdl|Z;_Ku&fy)}=9gJbe zxWpW12dpb7K~~}@{mh{4v|~IhY({%Y^tfG%=L&~Fc#M`^q#JZ)>b~Cxqvya`P=vXe zo1mJJq4CVwHSq|zrC(m1$6#p^Dg$5UK53zyZ?DSi{jt44tlei=3?z>F07e~j_k6yF z^#+e@lvu}w_|rNgj<$Do>mXS5`G;@^^WmpZD!Y-nVhxW-{gGqWJ6B4{Og7Y{HLKTIcn5`Jz{t zw&)&XSuPud{+>N@46W+vO1!D9m^qwqKZ_ozV@s1+G^pT}z~-UFe^P;%M2hILvex;W zcG|YR(yevS#f7N8K?9tXvVxTb?}aw|{NW4L8NKHD%{JkjQ5*6y25SSNuPv#aGQ47w zaV5_lKI%Wq+iwgg4Dm^bY6F!QrAWDy0J#AN=~P#6JMX0zFK}zwv99nm9WRwZeq`@J zwICKJI~>pkn9gwcN;zRc{o!;K$wlU*aw%&(3l(v;sxTwa4H+M8O2wo>Lo&a zB7VTZ{^Ey~J`G>j==bI&fPsdy!mRLFh0PWS2KMJLA@luw8wOR_p$h9rWAq;Xng{H} ztuX38EllrbK}cppK6-JdlMrH2=zHH;D<#x@e8#F0@%(ESOvcrw0(P|MvoZNZ-i>KH za`Qy~D2b6lSnWECI7S-)Tz$qJABBlmH%TLkJjTAWN8TB0?eNM7jUnY-)*;C^VQceB zNsPhfpD+#uwM?%z9CUe6`m6fPh7}BY+K@q^@Dj}v)DRrHs)aC-_-2D3kCZpeIriib zmxLfJvNd=ULh0mqzoOf^bvrHzYBXg{k))wYSU$eN^e_?}ib( z9c_P{ZvFep{E^~b5AHAeP0kC3LqWZA2eL0l|4tS_!=L-baFKH%v$&NaUY)ASc*4f4 zzbxl~=QeF(o+x#e{S0-ll=A)P602EXH79$xcR<5z)GgZ@>tF-CBbTj-G==x?4EVEE zxo)i%18k)5wQLGk#pX|veBa>4(2x{lw@by!P~3!=>R}uon_^6y3O?PW5}MgRYxFl> zH#NnAD|s^3mSMGn8mEu$9d&uoo9UQGR*C!1;%FkhaLBPZ%DVz@K!uS3J)7BnzL zC}Tk5Pr}-^3Hpkw^dmZ;4ZdxiF2I?2cl9qtM3*>)qzEVHT&)$>fJybIBHt@d{F9a; zWG|zyztqsPd>BHFXR_P@6l%T3hA7-f;a)?}&Ni_pdc>p*gD^gsii>Q+ClMZCHPtn( zKcN=ZzQfopUniMW;rbv?E}>yc+LqbO->VM4B_>tGMbFq?62LW|bHKKEQ)cs%>^1?FS5dG=jG^mul^wnF{vl!l zR9eLVJ}Fz!jTU$*4YD4xVTsLol(R_A4gPyrmPFA6wTn<@=Xaes@CtOpnv!KGnF5T7 z-7zw0Txt`k@kBnyENV}VqZQzb-$>oU%kRI{Ehlb^A5TtVw66T#LEbI&ADjM$WJC<+B&cuhDKxY)vu_J zz_l;ZqGT0J#{V{gooq6V=*o#Vd}&vHL)R8&a%Ao0WZ4QR5B}wOGw^ESVZBkKkwG?G zhNEj3=F0!AK-`mxSSRX#^oq{e#9U5jTS$dF&$nsCODK@;+)*;k=B#y&eg^>%2eciU zE@_Ku_B6qrmE*k!mAzr|_F$-4TyldnZM^tQONinL`yKbN+Ev@ z$!2msbLOlsY|p>LJNJKU@3xAnO3@xho~rX}B1Q{6z1_y2_sogt8b`Zo!>teKFA z~+5MwLjKu|(F^CHYfKoRqN+lJl zc5*=h`dcfIF<~3icaUN?sBmLQN$q&Ys=WOcTj+nPt?6(~jcoc=Qnoi=@N`xAFBifc zHWPqdEngXcoI7oB_pCjijJ?gqw(kY7bB+;s9C@`}vMfUm#~C%>p4Zh|N3IgbqW4O1 z|3cLbpWx0Qp9f$4of4`>`T~h9V2ZDr4^?wz$SMfpG6^NBSVYN)Oy72|wfB}(*as&J zoH^S;N@qXBND?N$;9K|Cy*y3EPYIE;$n&iPG`JWp*Bcj@-JyU-BpXtn=H_~en?|n4 z5P{5avAy$v?43!PM7ka+=~+Q6LHM(&Fb<85*ABcXTRo|D12@JcQuC^P5aU@V7tvz7`h81R42+E1BQU$7+i!c&DD7 z_}Qa_Y9O!oF?F{JNPcf{c+%AnD}9v-ljynff|5R!D^)7oFQQkY#N^}kJmL92c9Sp z3;b53lS>X8g2>ZR)xmK%r15P5C*G?9*!(OtZ%|PBNMPcqGgS7j_wt8YN4yd~96R|Y zCog@kC6hnpfzqZ9amUuj9NwWO2Yo&+#%n2AJ8K@MNZav}r(7VBAp{vfpZ5aK$-!a3 z$#V9zVX#_fwV%Jy4C-~L4OBhZ$rERA8z_20O8({qwC(Z?oIEtT)Vdr-uMsU^l;0WK zkWJGiYBpxz8AXx9p&U^>vRm($`ceeUxuR1~C`49P6vQJUHw-(wke4h}GnNp|LH|rb ztn>r$=xF*<>Tad9*pVS@k)322m4IV{0_jFoO%5m6Y+cmnzT}XUsPo}Oj`iIMh!qN| zfu%Tw9uyP_GT(u{8ezd2M+(N-at`#AVGC4|RP{j48@N3EzJ8J<8Hx}6M`aRDLW?dmt> zw|;>&>XGb147|p8khV{Nsjy0aJb~q<-G(bWtixN_+~d--iarVp>1-^>JRLHPtR^=_ z;4M;oH}LYQSe@9v#dX z2XXNlbJbjNq2k zm;_{!(f!HfV%KEsozc@$$fCTxD7AKR%{$-}h>uC=HMSJ_f%2y(`3~Ky4wYh_WB%Qw zdTS}_E`Y1MzPV(`Y<6BpLb0L9-#{>U%;1t84YCxe*8iQF)c z51`W~k2}wc8X`&k{~}cv5*_qA!{MUz)?nehHzE@%A*1}P2>p%MOxrQrNZ6umU)zgw zP&w82jf;`!8@Yw*c?wE7iD8w zu2-%JcDOa2Bi6mqV{EUJ-tm%tg??{Of13~sG+)~Z3j0|V$ zQ@=c3#JEK1%|g5$VgzWf(5N;r;jyQ0R`1GY54H5>>FVIQ3V;?vjmJ_6zpoCQbVT$@ z+^q-VG@9r_K$qPB#m}tY#m6yg=>(5sq8KjYI-S+)I*(YELY(iafqym_$Nw?ep9BO` z+1ZxpdWs}Qx!y`|kqrAlYF2xL;}r;VB&x3JPiLLvlLH+y5X=HJnG~JiqawT`gMZ!2z+y%PpG}4Ek+jCDy}Wu7 zkxal);$kzgSgEPO#Wh%S(iZOs*aw<@uI4ZRpB)gv1FkD_4IOP(d+1}IFFla_u)7_E zE~)oM@U~`6VD5e|0OgVfpJgQ4LzZXp(OcQl@vqBNIXc+80Q8D$N70uF7itsF`j@2x{|Eh*{slGoz){%tJt;!aGDeS46iS+q%|0Pfx5KKA^aI)f z0;%`VK^!WV;I50X`8jg``w>Sa2MZNNbJ_kBiZbGk@PKPG^kmcPJ0AiJe)A%s9#=7^ zp+zTTj6er@u}p1Al4^V=U6yN0>iVB*m9y5YrT^E