From c8dfb4db25432519b057cb8fc40f8fa8df336ed6 Mon Sep 17 00:00:00 2001
From: longdayi <13477510+longdayilongdayi@user.noreply.gitee.com>
Date: Mon, 30 Dec 2024 08:26:40 +0800
Subject: [PATCH] 12300826
---
.continue/prompts/comment.prompt | 45 ++
.continue/prompts/error-handler.prompt | 45 ++
.continue/prompts/explain.prompt | 30 +
.continue/prompts/react-refact.prompt | 39 +
.continue/prompts/refact.prompt | 52 ++
apps/server/package.json | 27 +-
apps/server/src/app.module.ts | 52 +-
apps/server/src/auth/auth.controller.ts | 67 +-
apps/server/src/auth/auth.module.ts | 14 +-
apps/server/src/auth/auth.router.ts | 20 +
apps/server/src/auth/auth.service.ts | 259 +++---
apps/server/src/auth/config.ts | 9 +
apps/server/src/auth/session.service.ts | 61 ++
apps/server/src/auth/types.ts | 9 +
apps/server/src/auth/utils.ts | 187 +++++
apps/server/src/env.ts | 5 +-
apps/server/src/filters/exceptions.filter.ts | 25 +
apps/server/src/init/init.module.ts | 11 -
apps/server/src/main.ts | 15 +-
.../models/app-config/app-config.module.ts | 12 +
.../models/app-config/app-config.router.ts | 47 ++
.../models/app-config/app-config.service.ts | 21 +
apps/server/src/models/base/base.service.ts | 562 +++++++++++++
.../src/models/base/base.tree.service.ts | 389 +++++++++
apps/server/src/models/base/base.type.ts | 44 +
.../server/src/models/base/errorMap.prisma.ts | 198 +++++
.../src/models/base/row-cache.service.ts | 183 +++++
.../src/models/base/row-model.service.ts | 240 ++++++
apps/server/src/models/base/sql-builder.ts | 138 ++++
apps/server/src/models/base/test.sql | 30 +
.../department/department.controller.ts | 87 ++
.../models/department/department.module.ts | 7 +-
.../models/department/department.router.ts | 83 +-
.../department/department.row.service.ts | 91 +++
.../models/department/department.service.ts | 590 +++++++-------
apps/server/src/models/department/utils.ts | 68 ++
.../src/models/message/message.controller.ts | 125 +++
.../src/models/message/message.module.ts | 14 +
.../src/models/message/message.router.ts | 39 +
.../src/models/message/message.service.ts | 57 ++
apps/server/src/models/message/utils.ts | 20 +
.../server/src/models/post/post.controller.ts | 89 ++
apps/server/src/models/post/post.module.ts | 18 +
apps/server/src/models/post/post.router.ts | 77 ++
apps/server/src/models/post/post.service.ts | 146 ++++
apps/server/src/models/post/utils.ts | 44 +
.../src/{ => models}/rbac/rbac.module.ts | 10 +-
apps/server/src/models/rbac/role.router.ts | 44 +
apps/server/src/models/rbac/role.service.ts | 180 +++++
apps/server/src/models/rbac/rolemap.router.ts | 72 ++
.../server/src/models/rbac/rolemap.service.ts | 317 ++++++++
.../src/models/staff/staff.controller.ts | 48 ++
apps/server/src/models/staff/staff.module.ts | 12 +-
apps/server/src/models/staff/staff.router.ts | 65 +-
.../src/models/staff/staff.row.service.ts | 135 ++++
.../src/models/staff/staff.service.spec.ts | 18 -
apps/server/src/models/staff/staff.service.ts | 336 ++++----
.../models/taxonomy/taxonomy.controller.ts | 28 +
.../src/models/taxonomy/taxonomy.module.ts | 10 +-
.../src/models/taxonomy/taxonomy.router.ts | 81 +-
.../src/models/taxonomy/taxonomy.service.ts | 85 +-
.../server/src/models/term/term.controller.ts | 28 +
apps/server/src/models/term/term.module.ts | 13 +-
apps/server/src/models/term/term.router.ts | 124 +--
.../src/models/term/term.row.service.ts | 91 +++
apps/server/src/models/term/term.service.ts | 760 ++++++++++--------
apps/server/src/models/term/utils.ts | 24 +
.../transform/transform.module.ts | 24 +-
.../transform/transform.router.ts | 17 +-
.../src/models/transform/transform.service.ts | 551 +++++++++++++
apps/server/src/models/visit/visit.module.ts | 10 +
apps/server/src/models/visit/visit.router.ts | 37 +
apps/server/src/models/visit/visit.service.ts | 89 ++
.../queue/general/general-queue.listener.ts | 32 -
.../queue/general/general-queue.service.ts | 23 -
.../src/queue/general/general.service.ts | 17 -
apps/server/src/queue/job.interface.ts | 4 +
.../src/queue/push/push.queue.service.ts | 52 ++
apps/server/src/queue/push/push.service.ts | 124 +++
apps/server/src/queue/queue.module.ts | 39 +-
apps/server/src/queue/worker/processor.ts | 15 +-
apps/server/src/rbac/role.router.ts | 37 -
apps/server/src/rbac/role.service.ts | 134 ---
apps/server/src/rbac/rolemap.router.ts | 53 --
apps/server/src/rbac/rolemap.service.ts | 215 -----
apps/server/src/rbac/roleperms.service.ts | 189 -----
apps/server/src/redis/redis.module.ts | 11 -
apps/server/src/redis/redis.service.ts | 73 --
apps/server/src/relation/relation.service.ts | 85 --
.../src/socket/base/base-websocket-server.ts | 205 +++++
.../src/socket/collaboration/callback.ts | 150 ++++
.../collaboration/collaboration.module.ts | 8 +
.../src/socket/collaboration/persistence.ts | 34 +
apps/server/src/socket/collaboration/types.ts | 5 +
.../src/socket/collaboration/ws-shared-doc.ts | 158 ++++
.../src/socket/collaboration/yjs.server.ts | 85 ++
.../src/socket/realtime/realtime.module.ts | 9 +
.../src/socket/realtime/realtime.server.ts | 31 +
apps/server/src/socket/socket.gateway.ts | 28 -
apps/server/src/socket/types.ts | 29 +
apps/server/src/socket/websocket.module.ts | 11 +
apps/server/src/socket/websocket.service.ts | 61 ++
apps/server/src/tasks/init/gendev.service.ts | 234 ++++++
apps/server/src/tasks/init/init.module.ts | 16 +
.../src/{ => tasks}/init/init.service.ts | 93 ++-
apps/server/src/tasks/init/utils.ts | 102 +++
.../src/tasks/reminder/reminder.module.ts | 10 +
.../src/tasks/reminder/reminder.service.ts | 80 ++
apps/server/src/tasks/tasks.module.ts | 5 +-
.../tasks.service.spec.ts} | 10 +-
apps/server/src/tasks/tasks.service.ts | 41 +-
.../server/src/transform/transform.service.ts | 496 ------------
apps/server/src/trpc/trpc.module.ts | 38 +-
apps/server/src/trpc/trpc.router.ts | 100 ++-
apps/server/src/trpc/trpc.service.ts | 60 +-
apps/server/src/utils/event-bus.ts | 16 +
.../src/{ => utils}/minio/minio.module.ts | 0
.../src/{ => utils}/minio/minio.service.ts | 4 +-
apps/server/src/utils/redis/redis.service.ts | 192 +++++
apps/server/src/utils/redis/utils.ts | 13 +
apps/server/src/utils/tool.ts | 148 ++++
apps/server/src/utils/tusd.ts | 76 --
apps/server/tsconfig.json | 17 +-
apps/web/.env.example | 0
apps/web/.eslintrc.cjs | 18 -
apps/web/README.md | 42 +-
apps/web/entrypoint.sh | 14 +
apps/web/eslint.config.js | 28 +
apps/web/index.html | 25 +-
apps/web/nginx.conf | 0
apps/web/package.json | 79 +-
apps/web/postcss.config.js | 0
apps/web/public/params.json | 2 +
apps/web/public/vite.svg | 0
apps/web/src/App.css | 42 +
apps/web/src/App.tsx | 45 +-
apps/web/src/app/admin/base-setting/page.tsx | 189 +++++
apps/web/src/app/admin/department/page.tsx | 6 +-
apps/web/src/app/admin/role/page.tsx | 33 +-
apps/web/src/app/admin/staff/page.tsx | 14 +-
apps/web/src/app/admin/term/page.tsx | 18 +-
apps/web/src/app/denied.tsx | 7 +
apps/web/src/app/error.tsx | 10 +-
apps/web/src/app/layout.tsx | 118 ++-
apps/web/src/app/login.tsx | 408 ++++++----
apps/web/src/app/main/page.tsx | 7 +-
apps/web/src/assets/react.svg | 0
.../src/components/animation/sine-wave.tsx | 122 +++
.../src/components/button/excel-importer.tsx | 145 ++++
apps/web/src/components/layout/breadcrumb.tsx | 38 +
apps/web/src/components/layout/fix-header.tsx | 176 ++++
.../components/layout/resizable-sidebar.tsx | 95 +++
.../src/components/layout/sidebar-content.tsx | 90 +++
.../web/src/components/layout/user-header.tsx | 50 ++
.../models/department/department-drawer.tsx | 40 -
.../models/department/department-form.tsx | 192 +++--
.../department/department-import-drawer.tsx | 2 +-
.../models/department/department-list.tsx | 299 +++----
.../models/department/department-select.tsx | 248 +++---
.../models/department/dept-editor.tsx | 90 +++
.../models/department/dept-import-form.tsx | 80 ++
.../models/department/dept-import-modal.tsx | 25 +
.../models/department/dept-modal.tsx | 26 +
.../models/domain/domain-select.tsx | 53 --
.../components/models/role/role-drawer.tsx | 39 -
.../models/role/role-editor/assign-list.tsx | 118 +++
.../models/role/role-editor/role-editor.tsx | 82 ++
.../models/role/role-editor/role-form.tsx | 64 ++
.../models/role/role-editor/role-list.tsx | 91 +++
.../models/role/role-editor/role-modal.tsx | 31 +
.../role/role-editor/role-staff-modal.tsx | 64 ++
.../src/components/models/role/role-form.tsx | 64 --
.../src/components/models/role/role-list.tsx | 101 ---
.../components/models/role/role-map-table.tsx | 256 ------
.../components/models/role/role-select.tsx | 3 +-
.../components/models/role/rolemap-drawer.tsx | 41 -
.../components/models/role/rolemap-form.tsx | 104 ---
.../components/models/staff/staff-drawer.tsx | 41 -
.../components/models/staff/staff-editor.tsx | 71 ++
.../components/models/staff/staff-form.tsx | 267 +++---
.../models/staff/staff-import-drawer.tsx | 73 --
.../components/models/staff/staff-list.tsx | 195 +++++
.../components/models/staff/staff-modal.tsx | 27 +
.../components/models/staff/staff-select.tsx | 113 ++-
.../components/models/staff/staff-table.tsx | 242 ------
.../models/staff/staff-transfer.tsx | 79 ++
.../models/taxonomy/taxonomy-drawer.tsx | 41 -
.../models/taxonomy/taxonomy-form.tsx | 32 -
.../models/taxonomy/taxonomy-select.tsx | 102 +--
.../models/taxonomy/taxonomy-table.tsx | 175 ----
.../components/models/term/taxonomy-form.tsx | 60 ++
.../components/models/term/taxonomy-list.tsx | 45 ++
.../components/models/term/taxonomy-modal.tsx | 23 +
.../components/models/term/term-drawer.tsx | 43 -
.../components/models/term/term-editor.tsx | 94 +++
.../src/components/models/term/term-form.tsx | 179 ++---
.../models/term/term-import-drawer.tsx | 109 ---
.../models/term/term-import-form.tsx | 87 ++
.../models/term/term-import-modal.tsx | 25 +
.../src/components/models/term/term-list.tsx | 531 ++++++------
.../src/components/models/term/term-modal.tsx | 28 +
.../components/models/term/term-select.tsx | 223 +++--
.../models/term/term-select_BACKUP.tsx | 92 +++
apps/web/src/components/models/term/util.ts | 15 +
.../presentation/ag-server-table.tsx | 494 ++++++++++++
.../presentation/animate-progress.tsx | 28 +
.../presentation/animation/sine-wave.tsx | 110 ---
.../presentation/collapse-section.tsx | 134 +++
.../presentation/dashboard-card.tsx | 62 ++
.../src/components/presentation/dialog.tsx | 229 ++++++
.../components/presentation/dropdown-menu.tsx | 293 +++++++
.../presentation/excel-to-base64-uploader.tsx | 85 ++
.../presentation/general-dialog.tsx | 64 ++
.../src/components/presentation/id-card.tsx | 25 +
.../src/components/presentation/nice-img.tsx | 80 ++
.../components/presentation/phone-book.tsx | 23 +
.../src/components/presentation/round-tag.tsx | 20 +
.../presentation/rounded-rectangle-tag.tsx | 19 +
apps/web/src/components/svg/rounded-clip.tsx | 28 +
.../src/components/utilities/with-auth.tsx | 30 -
.../{utilities => utils}/excel-importer.tsx | 10 +-
.../src/components/utils/image-uploader.tsx | 111 +++
apps/web/src/components/utils/with-auth.tsx | 62 ++
apps/web/src/env.ts | 22 +-
apps/web/src/hooks/useDepartment.ts | 119 ---
apps/web/src/hooks/useLocalSetting.ts | 17 +
apps/web/src/hooks/useRoleMap.ts | 37 -
apps/web/src/hooks/useStaff.ts | 31 -
apps/web/src/hooks/useTerm.ts | 101 ---
apps/web/src/hooks/useTransform.ts | 29 -
apps/web/src/index.css | 109 ++-
apps/web/src/io/index.ts | 1 +
apps/web/src/io/tusd.ts | 96 +++
apps/web/src/locale/ag-grid-locale.ts | 563 +++++++++++++
apps/web/src/main.tsx | 31 +-
apps/web/src/providers/auth-provider.tsx | 404 ++++++----
apps/web/src/providers/params-provider.tsx | 45 ++
apps/web/src/providers/query-provider.tsx | 123 ++-
apps/web/src/providers/theme-provider.tsx | 160 +++-
apps/web/src/routes/index.tsx | 230 ++++--
apps/web/src/utils/axios-client.ts | 4 +-
apps/web/src/utils/idb.ts | 0
apps/web/src/utils/tusd.ts | 95 ---
apps/web/src/vite-env.d.ts | 0
apps/web/tailwind.config.js | 32 +-
apps/web/tsconfig.app.json | 60 +-
apps/web/tsconfig.json | 10 +-
apps/web/tsconfig.node.json | 2 +
apps/web/vite.config.ts | 7 +-
config/backup.sh | 26 +
config/redis.conf | 94 +++
packages/client/package.json | 39 +
packages/client/src/api/hooks/index.ts | 12 +
packages/client/src/api/hooks/useAppConfig.ts | 49 ++
.../client/src/api/hooks/useDepartment.ts | 92 +++
packages/client/src/api/hooks/useMessage.ts | 18 +
packages/client/src/api/hooks/usePost.ts | 37 +
packages/client/src/api/hooks/useQueryApi.ts | 27 +
.../client/src/api}/hooks/useRole.ts | 6 +-
packages/client/src/api/hooks/useRoleMap.ts | 47 ++
packages/client/src/api/hooks/useStaff.ts | 43 +
.../client/src/api}/hooks/useTaxonomy.ts | 6 +-
packages/client/src/api/hooks/useTerm.ts | 71 ++
packages/client/src/api/hooks/useVisitor.ts | 131 +++
packages/client/src/api/index.ts | 3 +
.../utils => packages/client/src/api}/trpc.ts | 2 +-
.../client/src/api/utils.ts | 14 +-
packages/client/src/event/index.ts | 149 ++++
packages/client/src/hooks/index.ts | 4 +
.../client}/src/hooks/useAwaitState.ts | 0
packages/client/src/hooks/useCheckBox.ts | 156 ++++
packages/client/src/hooks/useStack.ts | 128 +++
packages/client/src/hooks/useTimeout.ts | 78 ++
packages/client/src/index.ts | 8 +
packages/client/src/io/download.ts | 42 +
packages/client/src/io/index.ts | 1 +
packages/client/src/presentation/color.ts | 13 +
packages/client/src/presentation/index.ts | 1 +
packages/client/src/providers/index.ts | 1 +
packages/client/src/singleton/DataHolder.ts | 36 +
packages/client/src/tools/index.ts | 3 +
packages/client/src/tools/level.ts | 136 ++++
packages/client/src/tools/number.ts | 76 ++
packages/client/src/tools/objects.ts | 21 +
packages/client/src/websocket/client.ts | 232 ++++++
packages/client/src/websocket/index.ts | 2 +
packages/client/src/websocket/types.ts | 46 ++
packages/client/tsconfig.json | 28 +
packages/client/tsup.config.ts | 10 +
packages/common/.env.example | 2 +-
packages/common/package.json | 38 +-
packages/common/prisma/schema.prisma | 356 +++++---
packages/common/src/collaboration/index.ts | 4 +
packages/common/src/collaboration/types.ts | 7 +
packages/common/src/collaboration/utils.ts | 3 +
packages/common/src/collaboration/y-auth.ts | 29 +
.../common/src/collaboration/y-awareness.ts | 544 +++++++++++++
.../common/src/collaboration/y-handler.ts | 87 ++
packages/common/src/collaboration/y-socket.ts | 388 +++++++++
packages/common/src/collaboration/y-sync.ts | 189 +++++
packages/common/src/constants.ts | 134 +--
packages/common/src/db.ts | 14 +-
packages/common/src/enum.ts | 226 +++++-
packages/common/src/index.ts | 12 +-
packages/common/src/schema.ts | 658 +++++++++------
packages/common/src/select.ts | 69 ++
packages/common/src/type.ts | 52 --
packages/common/src/types.ts | 180 +++++
packages/common/src/utils.ts | 395 +++++++--
packages/common/tsconfig.cjs.json | 21 -
packages/common/tsconfig.esm.json | 22 -
packages/common/tsconfig.json | 39 +
packages/common/tsup.config.ts | 10 +
tsconfig.json => tsconfig.base.json | 8 +-
314 files changed, 20634 insertions(+), 7190 deletions(-)
create mode 100644 .continue/prompts/comment.prompt
create mode 100644 .continue/prompts/error-handler.prompt
create mode 100644 .continue/prompts/explain.prompt
create mode 100644 .continue/prompts/react-refact.prompt
create mode 100644 .continue/prompts/refact.prompt
mode change 100644 => 100755 apps/server/src/app.module.ts
mode change 100644 => 100755 apps/server/src/auth/auth.controller.ts
mode change 100644 => 100755 apps/server/src/auth/auth.module.ts
create mode 100755 apps/server/src/auth/auth.router.ts
mode change 100644 => 100755 apps/server/src/auth/auth.service.ts
create mode 100644 apps/server/src/auth/config.ts
create mode 100644 apps/server/src/auth/session.service.ts
create mode 100644 apps/server/src/auth/types.ts
create mode 100644 apps/server/src/auth/utils.ts
create mode 100644 apps/server/src/filters/exceptions.filter.ts
delete mode 100644 apps/server/src/init/init.module.ts
mode change 100644 => 100755 apps/server/src/main.ts
create mode 100644 apps/server/src/models/app-config/app-config.module.ts
create mode 100644 apps/server/src/models/app-config/app-config.router.ts
create mode 100644 apps/server/src/models/app-config/app-config.service.ts
create mode 100644 apps/server/src/models/base/base.service.ts
create mode 100644 apps/server/src/models/base/base.tree.service.ts
create mode 100644 apps/server/src/models/base/base.type.ts
create mode 100644 apps/server/src/models/base/errorMap.prisma.ts
create mode 100644 apps/server/src/models/base/row-cache.service.ts
create mode 100644 apps/server/src/models/base/row-model.service.ts
create mode 100644 apps/server/src/models/base/sql-builder.ts
create mode 100644 apps/server/src/models/base/test.sql
create mode 100755 apps/server/src/models/department/department.controller.ts
create mode 100644 apps/server/src/models/department/department.row.service.ts
create mode 100644 apps/server/src/models/department/utils.ts
create mode 100755 apps/server/src/models/message/message.controller.ts
create mode 100644 apps/server/src/models/message/message.module.ts
create mode 100755 apps/server/src/models/message/message.router.ts
create mode 100644 apps/server/src/models/message/message.service.ts
create mode 100644 apps/server/src/models/message/utils.ts
create mode 100755 apps/server/src/models/post/post.controller.ts
create mode 100755 apps/server/src/models/post/post.module.ts
create mode 100755 apps/server/src/models/post/post.router.ts
create mode 100755 apps/server/src/models/post/post.service.ts
create mode 100644 apps/server/src/models/post/utils.ts
rename apps/server/src/{ => models}/rbac/rbac.module.ts (59%)
mode change 100644 => 100755
create mode 100755 apps/server/src/models/rbac/role.router.ts
create mode 100755 apps/server/src/models/rbac/role.service.ts
create mode 100755 apps/server/src/models/rbac/rolemap.router.ts
create mode 100755 apps/server/src/models/rbac/rolemap.service.ts
create mode 100755 apps/server/src/models/staff/staff.controller.ts
mode change 100644 => 100755 apps/server/src/models/staff/staff.module.ts
create mode 100644 apps/server/src/models/staff/staff.row.service.ts
delete mode 100644 apps/server/src/models/staff/staff.service.spec.ts
mode change 100644 => 100755 apps/server/src/models/staff/staff.service.ts
create mode 100755 apps/server/src/models/taxonomy/taxonomy.controller.ts
mode change 100644 => 100755 apps/server/src/models/taxonomy/taxonomy.module.ts
mode change 100644 => 100755 apps/server/src/models/taxonomy/taxonomy.router.ts
create mode 100755 apps/server/src/models/term/term.controller.ts
create mode 100644 apps/server/src/models/term/term.row.service.ts
create mode 100644 apps/server/src/models/term/utils.ts
rename apps/server/src/{ => models}/transform/transform.module.ts (51%)
mode change 100644 => 100755
rename apps/server/src/{ => models}/transform/transform.router.ts (68%)
mode change 100644 => 100755
create mode 100755 apps/server/src/models/transform/transform.service.ts
create mode 100644 apps/server/src/models/visit/visit.module.ts
create mode 100644 apps/server/src/models/visit/visit.router.ts
create mode 100644 apps/server/src/models/visit/visit.service.ts
delete mode 100644 apps/server/src/queue/general/general-queue.listener.ts
delete mode 100644 apps/server/src/queue/general/general-queue.service.ts
delete mode 100644 apps/server/src/queue/general/general.service.ts
create mode 100755 apps/server/src/queue/job.interface.ts
create mode 100755 apps/server/src/queue/push/push.queue.service.ts
create mode 100755 apps/server/src/queue/push/push.service.ts
mode change 100644 => 100755 apps/server/src/queue/queue.module.ts
mode change 100644 => 100755 apps/server/src/queue/worker/processor.ts
delete mode 100644 apps/server/src/rbac/role.router.ts
delete mode 100644 apps/server/src/rbac/role.service.ts
delete mode 100644 apps/server/src/rbac/rolemap.router.ts
delete mode 100644 apps/server/src/rbac/rolemap.service.ts
delete mode 100755 apps/server/src/rbac/roleperms.service.ts
delete mode 100644 apps/server/src/redis/redis.module.ts
delete mode 100644 apps/server/src/redis/redis.service.ts
delete mode 100644 apps/server/src/relation/relation.service.ts
create mode 100644 apps/server/src/socket/base/base-websocket-server.ts
create mode 100644 apps/server/src/socket/collaboration/callback.ts
create mode 100644 apps/server/src/socket/collaboration/collaboration.module.ts
create mode 100644 apps/server/src/socket/collaboration/persistence.ts
create mode 100644 apps/server/src/socket/collaboration/types.ts
create mode 100644 apps/server/src/socket/collaboration/ws-shared-doc.ts
create mode 100644 apps/server/src/socket/collaboration/yjs.server.ts
create mode 100644 apps/server/src/socket/realtime/realtime.module.ts
create mode 100644 apps/server/src/socket/realtime/realtime.server.ts
delete mode 100644 apps/server/src/socket/socket.gateway.ts
create mode 100644 apps/server/src/socket/types.ts
create mode 100644 apps/server/src/socket/websocket.module.ts
create mode 100644 apps/server/src/socket/websocket.service.ts
create mode 100644 apps/server/src/tasks/init/gendev.service.ts
create mode 100755 apps/server/src/tasks/init/init.module.ts
rename apps/server/src/{ => tasks}/init/init.service.ts (51%)
mode change 100644 => 100755
create mode 100644 apps/server/src/tasks/init/utils.ts
create mode 100755 apps/server/src/tasks/reminder/reminder.module.ts
create mode 100755 apps/server/src/tasks/reminder/reminder.service.ts
mode change 100644 => 100755 apps/server/src/tasks/tasks.module.ts
rename apps/server/src/{minio/minio.service.spec.ts => tasks/tasks.service.spec.ts} (56%)
mode change 100644 => 100755
mode change 100644 => 100755 apps/server/src/tasks/tasks.service.ts
delete mode 100644 apps/server/src/transform/transform.service.ts
mode change 100644 => 100755 apps/server/src/trpc/trpc.module.ts
mode change 100644 => 100755 apps/server/src/trpc/trpc.router.ts
mode change 100644 => 100755 apps/server/src/trpc/trpc.service.ts
create mode 100644 apps/server/src/utils/event-bus.ts
rename apps/server/src/{ => utils}/minio/minio.module.ts (100%)
rename apps/server/src/{ => utils}/minio/minio.service.ts (87%)
create mode 100644 apps/server/src/utils/redis/redis.service.ts
create mode 100644 apps/server/src/utils/redis/utils.ts
create mode 100755 apps/server/src/utils/tool.ts
delete mode 100644 apps/server/src/utils/tusd.ts
mode change 100644 => 100755 apps/server/tsconfig.json
mode change 100644 => 100755 apps/web/.env.example
delete mode 100644 apps/web/.eslintrc.cjs
mode change 100644 => 100755 apps/web/README.md
create mode 100755 apps/web/entrypoint.sh
create mode 100755 apps/web/eslint.config.js
mode change 100644 => 100755 apps/web/index.html
mode change 100644 => 100755 apps/web/nginx.conf
mode change 100644 => 100755 apps/web/package.json
mode change 100644 => 100755 apps/web/postcss.config.js
create mode 100755 apps/web/public/params.json
mode change 100644 => 100755 apps/web/public/vite.svg
mode change 100644 => 100755 apps/web/src/App.css
mode change 100644 => 100755 apps/web/src/App.tsx
create mode 100644 apps/web/src/app/admin/base-setting/page.tsx
mode change 100644 => 100755 apps/web/src/app/admin/department/page.tsx
mode change 100644 => 100755 apps/web/src/app/admin/role/page.tsx
mode change 100644 => 100755 apps/web/src/app/admin/staff/page.tsx
mode change 100644 => 100755 apps/web/src/app/admin/term/page.tsx
create mode 100644 apps/web/src/app/denied.tsx
mode change 100644 => 100755 apps/web/src/app/error.tsx
mode change 100644 => 100755 apps/web/src/app/main/page.tsx
mode change 100644 => 100755 apps/web/src/assets/react.svg
create mode 100644 apps/web/src/components/animation/sine-wave.tsx
create mode 100755 apps/web/src/components/button/excel-importer.tsx
create mode 100644 apps/web/src/components/layout/breadcrumb.tsx
create mode 100644 apps/web/src/components/layout/fix-header.tsx
create mode 100644 apps/web/src/components/layout/resizable-sidebar.tsx
create mode 100644 apps/web/src/components/layout/sidebar-content.tsx
create mode 100644 apps/web/src/components/layout/user-header.tsx
delete mode 100644 apps/web/src/components/models/department/department-drawer.tsx
create mode 100644 apps/web/src/components/models/department/dept-editor.tsx
create mode 100644 apps/web/src/components/models/department/dept-import-form.tsx
create mode 100644 apps/web/src/components/models/department/dept-import-modal.tsx
create mode 100644 apps/web/src/components/models/department/dept-modal.tsx
delete mode 100644 apps/web/src/components/models/domain/domain-select.tsx
delete mode 100644 apps/web/src/components/models/role/role-drawer.tsx
create mode 100644 apps/web/src/components/models/role/role-editor/assign-list.tsx
create mode 100644 apps/web/src/components/models/role/role-editor/role-editor.tsx
create mode 100644 apps/web/src/components/models/role/role-editor/role-form.tsx
create mode 100644 apps/web/src/components/models/role/role-editor/role-list.tsx
create mode 100644 apps/web/src/components/models/role/role-editor/role-modal.tsx
create mode 100644 apps/web/src/components/models/role/role-editor/role-staff-modal.tsx
delete mode 100644 apps/web/src/components/models/role/role-form.tsx
delete mode 100644 apps/web/src/components/models/role/role-list.tsx
delete mode 100644 apps/web/src/components/models/role/role-map-table.tsx
delete mode 100644 apps/web/src/components/models/role/rolemap-drawer.tsx
delete mode 100644 apps/web/src/components/models/role/rolemap-form.tsx
delete mode 100644 apps/web/src/components/models/staff/staff-drawer.tsx
create mode 100644 apps/web/src/components/models/staff/staff-editor.tsx
delete mode 100644 apps/web/src/components/models/staff/staff-import-drawer.tsx
create mode 100644 apps/web/src/components/models/staff/staff-list.tsx
create mode 100644 apps/web/src/components/models/staff/staff-modal.tsx
delete mode 100644 apps/web/src/components/models/staff/staff-table.tsx
create mode 100644 apps/web/src/components/models/staff/staff-transfer.tsx
delete mode 100644 apps/web/src/components/models/taxonomy/taxonomy-drawer.tsx
delete mode 100644 apps/web/src/components/models/taxonomy/taxonomy-form.tsx
delete mode 100644 apps/web/src/components/models/taxonomy/taxonomy-table.tsx
create mode 100644 apps/web/src/components/models/term/taxonomy-form.tsx
create mode 100644 apps/web/src/components/models/term/taxonomy-list.tsx
create mode 100644 apps/web/src/components/models/term/taxonomy-modal.tsx
delete mode 100644 apps/web/src/components/models/term/term-drawer.tsx
create mode 100644 apps/web/src/components/models/term/term-editor.tsx
delete mode 100644 apps/web/src/components/models/term/term-import-drawer.tsx
create mode 100644 apps/web/src/components/models/term/term-import-form.tsx
create mode 100644 apps/web/src/components/models/term/term-import-modal.tsx
create mode 100644 apps/web/src/components/models/term/term-modal.tsx
create mode 100644 apps/web/src/components/models/term/term-select_BACKUP.tsx
create mode 100644 apps/web/src/components/models/term/util.ts
create mode 100644 apps/web/src/components/presentation/ag-server-table.tsx
create mode 100644 apps/web/src/components/presentation/animate-progress.tsx
delete mode 100644 apps/web/src/components/presentation/animation/sine-wave.tsx
create mode 100644 apps/web/src/components/presentation/collapse-section.tsx
create mode 100644 apps/web/src/components/presentation/dashboard-card.tsx
create mode 100644 apps/web/src/components/presentation/dialog.tsx
create mode 100644 apps/web/src/components/presentation/dropdown-menu.tsx
create mode 100644 apps/web/src/components/presentation/excel-to-base64-uploader.tsx
create mode 100644 apps/web/src/components/presentation/general-dialog.tsx
create mode 100644 apps/web/src/components/presentation/id-card.tsx
create mode 100644 apps/web/src/components/presentation/nice-img.tsx
create mode 100644 apps/web/src/components/presentation/phone-book.tsx
create mode 100644 apps/web/src/components/presentation/round-tag.tsx
create mode 100644 apps/web/src/components/presentation/rounded-rectangle-tag.tsx
create mode 100644 apps/web/src/components/svg/rounded-clip.tsx
delete mode 100644 apps/web/src/components/utilities/with-auth.tsx
rename apps/web/src/components/{utilities => utils}/excel-importer.tsx (91%)
create mode 100644 apps/web/src/components/utils/image-uploader.tsx
create mode 100644 apps/web/src/components/utils/with-auth.tsx
mode change 100644 => 100755 apps/web/src/env.ts
delete mode 100644 apps/web/src/hooks/useDepartment.ts
create mode 100644 apps/web/src/hooks/useLocalSetting.ts
delete mode 100644 apps/web/src/hooks/useRoleMap.ts
delete mode 100644 apps/web/src/hooks/useStaff.ts
delete mode 100644 apps/web/src/hooks/useTerm.ts
delete mode 100644 apps/web/src/hooks/useTransform.ts
mode change 100644 => 100755 apps/web/src/index.css
create mode 100644 apps/web/src/io/index.ts
create mode 100644 apps/web/src/io/tusd.ts
create mode 100644 apps/web/src/locale/ag-grid-locale.ts
mode change 100644 => 100755 apps/web/src/main.tsx
create mode 100755 apps/web/src/providers/params-provider.tsx
mode change 100644 => 100755 apps/web/src/providers/query-provider.tsx
mode change 100644 => 100755 apps/web/src/routes/index.tsx
mode change 100644 => 100755 apps/web/src/utils/idb.ts
delete mode 100644 apps/web/src/utils/tusd.ts
mode change 100644 => 100755 apps/web/src/vite-env.d.ts
mode change 100644 => 100755 apps/web/tailwind.config.js
mode change 100644 => 100755 apps/web/tsconfig.app.json
mode change 100644 => 100755 apps/web/tsconfig.json
mode change 100644 => 100755 apps/web/tsconfig.node.json
mode change 100644 => 100755 apps/web/vite.config.ts
create mode 100755 config/backup.sh
create mode 100644 config/redis.conf
create mode 100644 packages/client/package.json
create mode 100644 packages/client/src/api/hooks/index.ts
create mode 100644 packages/client/src/api/hooks/useAppConfig.ts
create mode 100755 packages/client/src/api/hooks/useDepartment.ts
create mode 100644 packages/client/src/api/hooks/useMessage.ts
create mode 100644 packages/client/src/api/hooks/usePost.ts
create mode 100644 packages/client/src/api/hooks/useQueryApi.ts
rename {apps/web/src => packages/client/src/api}/hooks/useRole.ts (85%)
mode change 100644 => 100755
create mode 100755 packages/client/src/api/hooks/useRoleMap.ts
create mode 100755 packages/client/src/api/hooks/useStaff.ts
rename {apps/web/src => packages/client/src/api}/hooks/useTaxonomy.ts (88%)
mode change 100644 => 100755
create mode 100755 packages/client/src/api/hooks/useTerm.ts
create mode 100644 packages/client/src/api/hooks/useVisitor.ts
create mode 100644 packages/client/src/api/index.ts
rename {apps/web/src/utils => packages/client/src/api}/trpc.ts (69%)
mode change 100644 => 100755
rename apps/web/src/utils/general.ts => packages/client/src/api/utils.ts (88%)
mode change 100755 => 100644
create mode 100644 packages/client/src/event/index.ts
create mode 100644 packages/client/src/hooks/index.ts
rename {apps/web => packages/client}/src/hooks/useAwaitState.ts (100%)
create mode 100755 packages/client/src/hooks/useCheckBox.ts
create mode 100755 packages/client/src/hooks/useStack.ts
create mode 100644 packages/client/src/hooks/useTimeout.ts
create mode 100644 packages/client/src/index.ts
create mode 100644 packages/client/src/io/download.ts
create mode 100644 packages/client/src/io/index.ts
create mode 100644 packages/client/src/presentation/color.ts
create mode 100644 packages/client/src/presentation/index.ts
create mode 100644 packages/client/src/providers/index.ts
create mode 100644 packages/client/src/singleton/DataHolder.ts
create mode 100644 packages/client/src/tools/index.ts
create mode 100755 packages/client/src/tools/level.ts
create mode 100644 packages/client/src/tools/number.ts
create mode 100644 packages/client/src/tools/objects.ts
create mode 100644 packages/client/src/websocket/client.ts
create mode 100644 packages/client/src/websocket/index.ts
create mode 100644 packages/client/src/websocket/types.ts
create mode 100644 packages/client/tsconfig.json
create mode 100644 packages/client/tsup.config.ts
mode change 100755 => 100644 packages/common/.env.example
mode change 100755 => 100644 packages/common/package.json
mode change 100755 => 100644 packages/common/prisma/schema.prisma
create mode 100644 packages/common/src/collaboration/index.ts
create mode 100644 packages/common/src/collaboration/types.ts
create mode 100644 packages/common/src/collaboration/utils.ts
create mode 100644 packages/common/src/collaboration/y-auth.ts
create mode 100644 packages/common/src/collaboration/y-awareness.ts
create mode 100644 packages/common/src/collaboration/y-handler.ts
create mode 100644 packages/common/src/collaboration/y-socket.ts
create mode 100644 packages/common/src/collaboration/y-sync.ts
mode change 100644 => 100755 packages/common/src/constants.ts
mode change 100644 => 100755 packages/common/src/enum.ts
create mode 100644 packages/common/src/select.ts
delete mode 100644 packages/common/src/type.ts
create mode 100755 packages/common/src/types.ts
mode change 100644 => 100755 packages/common/src/utils.ts
delete mode 100755 packages/common/tsconfig.cjs.json
delete mode 100755 packages/common/tsconfig.esm.json
create mode 100644 packages/common/tsconfig.json
create mode 100644 packages/common/tsup.config.ts
rename tsconfig.json => tsconfig.base.json (87%)
diff --git a/.continue/prompts/comment.prompt b/.continue/prompts/comment.prompt
new file mode 100644
index 0000000..cd79c77
--- /dev/null
+++ b/.continue/prompts/comment.prompt
@@ -0,0 +1,45 @@
+temperature: 0.5
+maxTokens: 8192
+---
+
+角色定位:
+- 高级软件开发工程师
+- 代码文档化与知识传播专家
+
+注释目标:
+1. 顶部注释
+ - 模块/文件整体功能描述
+ - 版本历史
+ - 使用场景
+
+2. 类注释
+ - 类的职责和设计意图
+ - 核心功能概述
+ - 设计模式解析
+ - 使用示例
+
+3. 方法/函数注释
+ - 功能详细描述
+ - 输入参数解析
+ - 返回值说明
+ - 异常处理机制
+ - 算法复杂度
+ - 时间/空间性能分析
+
+4. 代码块注释
+ - 逐行解释代码意图
+ - 关键语句原理阐述
+ - 高级语言特性解读
+ - 潜在的设计考量
+
+注释风格要求:
+- 全程使用中文
+- 专业、清晰、通俗易懂
+- 面向初学者的知识传递
+- 保持技术严谨性
+
+输出约束:
+- 仅返回添加注释后的代码
+- 注释与代码完美融合
+- 保持原代码结构不变
+
\ No newline at end of file
diff --git a/.continue/prompts/error-handler.prompt b/.continue/prompts/error-handler.prompt
new file mode 100644
index 0000000..2ec3d74
--- /dev/null
+++ b/.continue/prompts/error-handler.prompt
@@ -0,0 +1,45 @@
+
+角色定位:
+- 身份: 高级错误处理与诊断工程师
+- 专业能力: 深入系统异常分析与解决
+- 分析维度: 错误类型、根因追踪、修复策略
+
+错误处理分析要求:
+1. 错误详细诊断
+ - 精确定位错误来源
+ - 追踪完整错误调用链
+ - 分析潜在影响范围
+
+2. 错误分类与解析
+ - 错误类型精确分类
+ - 技术根因深度剖析
+ - 系统架构潜在风险评估
+
+3. 修复方案设计
+ - 提供多层次修复建议
+ - 评估每种方案的优缺点
+ - 给出最优实施路径
+
+4. 预防性建议
+ - 提出系统防御性编程策略
+ - 设计错误拦截与处理机制
+ - 推荐代码健壮性改进方案
+
+输出规范:
+- 错误报告格式化文档
+- 中英文专业技术术语精准使用
+- 层次清晰、逻辑严密
+- 技术性、建设性并重
+
+报告要素:
+1. 错误摘要
+2. 详细诊断报告
+3. 根因分析
+4. 修复方案
+5. 预防建议
+
+禁止:
+- 避免泛泛而谈
+- 不提供无依据的猜测
+- 严格遵循技术分析逻辑
+
diff --git a/.continue/prompts/explain.prompt b/.continue/prompts/explain.prompt
new file mode 100644
index 0000000..e771c02
--- /dev/null
+++ b/.continue/prompts/explain.prompt
@@ -0,0 +1,30 @@
+temperature: 0.5
+maxTokens: 8192
+---
+
+角色定位:
+- 身份: 高级软件开发工程师
+- 专业能力: 深入代码架构分析
+- 分析维度: 技术、设计、性能、最佳实践
+
+分析要求:
+1. 代码逐行详细注释
+2. 注释必须包含:
+ - 代码意图解析
+ - 技术原理阐述
+ - 数据结构解读
+ - 算法复杂度分析
+ - 可能的优化建议
+
+输出规范:
+- 全中文专业技术文档注释
+- 注释风格: 标准文档型
+- 保留原代码结构
+- 注释与代码同步展示
+- 技术性、专业性并重
+
+禁止:
+- 不返回无关说明
+- 不进行无意义的介绍
+- strictly遵循技术分析本身
+
\ No newline at end of file
diff --git a/.continue/prompts/react-refact.prompt b/.continue/prompts/react-refact.prompt
new file mode 100644
index 0000000..4def8e2
--- /dev/null
+++ b/.continue/prompts/react-refact.prompt
@@ -0,0 +1,39 @@
+角色定位:
+- 身份: 资深前端架构师
+- 专业能力: React组件设计与重构
+- 分析维度: 组件性能、可维护性、代码规范
+
+重构分析要求:
+1. 组件代码全面评估
+2. 重构目标:
+ - 提升组件渲染性能
+ - 优化代码结构
+ - 增强组件复用性
+ - 遵循React最佳实践
+
+重构评估维度:
+- 状态管理是否合理
+- 渲染性能分析
+- Hook使用规范
+- 组件拆分颗粒度
+- 依赖管理
+- 类型安全
+
+重构输出要求:
+1. 详细重构方案
+2. 每个重构点需包含:
+ - 当前问题描述
+ - 重构建议
+ - 重构后代码示例
+ - 性能/架构提升说明
+
+重构原则:
+- 保持原有业务逻辑不变
+- 代码简洁、可读性强
+- 遵循函数式编程思想
+- 类型安全优先
+
+禁止:
+- 过度工程化
+- 不切实际的重构
+- 损害可读性的过度抽象
\ No newline at end of file
diff --git a/.continue/prompts/refact.prompt b/.continue/prompts/refact.prompt
new file mode 100644
index 0000000..8d267e0
--- /dev/null
+++ b/.continue/prompts/refact.prompt
@@ -0,0 +1,52 @@
+temperature: 0.5
+maxTokens: 8192
+---
+
+角色定位:
+- 高级软件架构师
+- 代码质量与性能改进专家
+
+重构核心目标:
+1. 代码质量提升
+ - 消除代码坏味道
+ - 提高可读性
+ - 增强可维护性
+ - 优化代码结构
+
+2. 架构设计优化
+ - 应用合适的设计模式
+ - 提升代码解耦程度
+ - 增强系统扩展性
+ - 改进模块间交互
+
+3. 性能与资源优化
+ - 算法复杂度改进
+ - 内存使用效率
+ - 计算资源利用率
+ - 减少不必要的计算开销
+
+4. 健壮性增强
+ - 完善异常处理机制
+ - 增加错误边界保护
+ - 提高代码容错能力
+ - 规范化错误处理流程
+
+重构原则:
+- 保持原始功能不变
+- 遵循SOLID设计原则
+- 代码简洁性
+- 高内聚低耦合
+- 尽量使用语言特性
+- 避免过度设计
+
+注释与文档要求:
+- 保留原有有效注释
+- 补充专业的中文文档型注释
+- 解释重构的关键决策
+- 说明性能与架构改进点
+
+输出约束:
+- 仅返回重构后的代码
+- 保持代码原有风格
+- 注释清晰专业
+
\ No newline at end of file
diff --git a/apps/server/package.json b/apps/server/package.json
index 5fac39f..6ad92f7 100644
--- a/apps/server/package.json
+++ b/apps/server/package.json
@@ -26,38 +26,47 @@
"@nestjs/core": "^10.0.0",
"@nestjs/jwt": "^10.2.0",
"@nestjs/platform-express": "^10.0.0",
- "@nestjs/platform-socket.io": "^10.4.1",
+ "@nestjs/platform-socket.io": "^10.3.10",
"@nestjs/schedule": "^4.1.0",
"@nestjs/websockets": "^10.3.10",
- "@nicestack/common": "workspace:^",
+ "@nicestack/common": "workspace:*",
"@trpc/server": "11.0.0-rc.456",
- "axios": "^1.7.3",
- "bcrypt": "^5.1.1",
+ "argon2": "^0.41.1",
+ "axios": "^1.7.2",
"bullmq": "^5.12.0",
"cron": "^3.1.7",
"dayjs": "^1.11.13",
+ "dotenv": "^16.4.7",
"exceljs": "^4.4.0",
"ioredis": "^5.4.1",
- "mime-types": "^2.1.35",
+ "lib0": "^0.2.97",
+ "lodash": "^4.17.21",
+ "lodash.debounce": "^4.0.8",
"minio": "^8.0.1",
+ "mitt": "^3.0.1",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"socket.io": "^4.7.5",
"superjson-cjs": "^2.2.3",
- "tus-js-client": "^4.1.0"
+ "tus-js-client": "^4.1.0",
+ "uuid": "^10.0.0",
+ "ws": "^8.18.0",
+ "y-leveldb": "^0.1.2",
+ "yjs": "^13.6.20",
+ "zod": "^3.23.8"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
- "@types/bcrypt": "^5.0.2",
"@types/exceljs": "^1.3.0",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.2",
- "@types/mime-types": "^2.1.4",
"@types/multer": "^1.4.12",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
+ "@types/uuid": "^10.0.0",
+ "@types/ws": "^8.5.13",
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.42.0",
@@ -71,7 +80,7 @@
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
- "typescript": "^5.1.3"
+ "typescript": "^5.5.4"
},
"jest": {
"moduleFileExtensions": [
diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts
old mode 100644
new mode 100755
index 0a10d11..e126092
--- a/apps/server/src/app.module.ts
+++ b/apps/server/src/app.module.ts
@@ -1,24 +1,50 @@
import { Module } from '@nestjs/common';
import { TrpcModule } from './trpc/trpc.module';
-import { RedisService } from './redis/redis.service';
-
-import { RedisModule } from './redis/redis.module';
-import { SocketGateway } from './socket/socket.gateway';
import { QueueModule } from './queue/queue.module';
-import { TransformModule } from './transform/transform.module';
import { AuthModule } from './auth/auth.module';
-import { ScheduleModule } from '@nestjs/schedule';
-import { ConfigService } from '@nestjs/config';
+import { TaxonomyModule } from './models/taxonomy/taxonomy.module';
import { TasksModule } from './tasks/tasks.module';
+import { ScheduleModule } from '@nestjs/schedule';
+import { InitModule } from './tasks/init/init.module';
+import { ReminderModule } from './tasks/reminder/reminder.module';
import { JwtModule } from '@nestjs/jwt';
import { env } from './env';
-import { MinioModule } from './minio/minio.module';
+import { ConfigModule } from '@nestjs/config';
+import { APP_FILTER } from '@nestjs/core';
+import { MinioModule } from './utils/minio/minio.module';
+import { WebSocketModule } from './socket/websocket.module';
+import { CollaborationModule } from './socket/collaboration/collaboration.module';
+import { ExceptionsFilter } from './filters/exceptions.filter';
+import { TransformModule } from './models/transform/transform.module';
+import { RealTimeModule } from './socket/realtime/realtime.module';
@Module({
- imports: [ScheduleModule.forRoot(), JwtModule.register({
- global: true,
- secret: env.JWT_SECRET
- }), TrpcModule, RedisModule, QueueModule, TransformModule, AuthModule, TasksModule, MinioModule],
- providers: [RedisService, SocketGateway, ConfigService],
+ imports: [
+ ConfigModule.forRoot({
+ isGlobal: true, // 全局可用
+ envFilePath: '.env'
+ }),
+ ScheduleModule.forRoot(),
+ JwtModule.register({
+ global: true,
+ secret: env.JWT_SECRET
+ }),
+ WebSocketModule,
+ TrpcModule,
+ QueueModule,
+ AuthModule,
+ TaxonomyModule,
+ TasksModule,
+ InitModule,
+ ReminderModule,
+ TransformModule,
+ MinioModule,
+ CollaborationModule,
+ RealTimeModule
+ ],
+ providers: [{
+ provide: APP_FILTER,
+ useClass: ExceptionsFilter,
+ }],
})
export class AppModule { }
diff --git a/apps/server/src/auth/auth.controller.ts b/apps/server/src/auth/auth.controller.ts
old mode 100644
new mode 100755
index c945af8..3f7f4c4
--- a/apps/server/src/auth/auth.controller.ts
+++ b/apps/server/src/auth/auth.controller.ts
@@ -1,38 +1,43 @@
import { Controller, Post, Body, UseGuards, Get, Req } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthSchema, JwtPayload } from '@nicestack/common';
-import { z } from '@nicestack/common';
import { AuthGuard } from './auth.guard';
+import { UserProfileService } from './utils';
+import { z } from 'zod';
@Controller('auth')
export class AuthController {
- constructor(private readonly authService: AuthService) { }
- @UseGuards(AuthGuard)
- @Get("user-profile")
- async getUserProfile(@Req() request: Request) {
- const user: JwtPayload = (request as any).user
- return this.authService.getUserProfile(user)
- }
- @Post('login')
- async login(@Body() body: z.infer) {
- return this.authService.signIn(body);
- }
- @Post('signup')
- async signup(@Body() body: z.infer) {
- return this.authService.signUp(body);
- }
- @UseGuards(AuthGuard) // Protecting the refreshToken endpoint with AuthGuard
- @Post('refresh-token')
- async refreshToken(@Body() body: z.infer) {
- return this.authService.refreshToken(body);
- }
- @UseGuards(AuthGuard) // Protecting the logout endpoint with AuthGuard
- @Post('logout')
- async logout(@Body() body: z.infer) {
- return this.authService.logout(body);
- }
- @UseGuards(AuthGuard) // Protecting the changePassword endpoint with AuthGuard
- @Post('change-password')
- async changePassword(@Body() body: z.infer) {
- return this.authService.changePassword(body);
- }
+ constructor(private readonly authService: AuthService) { }
+ @UseGuards(AuthGuard)
+ @Get('user-profile')
+ async getUserProfile(@Req() request: Request) {
+ const payload: JwtPayload = (request as any).user;
+ const { staff } = await UserProfileService.instance.getUserProfileById(payload.sub);
+ return staff
+ }
+ @Post('login')
+ async login(@Body() body: z.infer) {
+ return this.authService.signIn(body);
+ }
+ @Post('signup')
+ async signup(@Body() body: z.infer) {
+ return this.authService.signUp(body);
+ }
+ @Post('refresh-token')
+ async refreshToken(
+ @Body() body: z.infer,
+ ) {
+ return this.authService.refreshToken(body);
+ }
+ // @UseGuards(AuthGuard)
+ @Post('logout')
+ async logout(@Body() body: z.infer) {
+ return this.authService.logout(body);
+ }
+ @UseGuards(AuthGuard) // Protecting the changePassword endpoint with AuthGuard
+ @Post('change-password')
+ async changePassword(
+ @Body() body: z.infer,
+ ) {
+ return this.authService.changePassword(body);
+ }
}
diff --git a/apps/server/src/auth/auth.module.ts b/apps/server/src/auth/auth.module.ts
old mode 100644
new mode 100755
index 5982550..5abc663
--- a/apps/server/src/auth/auth.module.ts
+++ b/apps/server/src/auth/auth.module.ts
@@ -1,15 +1,17 @@
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
-import { JwtModule } from '@nestjs/jwt';
-import { env } from '@server/env';
import { AuthController } from './auth.controller';
-import { StaffService } from '@server/models/staff/staff.service';
-import { RoleMapService } from '@server/rbac/rolemap.service';
+import { StaffModule } from '@server/models/staff/staff.module';
+import { AuthRouter } from './auth.router';
+import { TrpcService } from '@server/trpc/trpc.service';
import { DepartmentService } from '@server/models/department/department.service';
+import { SessionService } from './session.service';
+import { RoleMapModule } from '@server/models/rbac/rbac.module';
@Module({
- providers: [AuthService, StaffService, RoleMapService, DepartmentService],
+ imports: [StaffModule, RoleMapModule],
+ providers: [AuthService, AuthRouter, TrpcService, DepartmentService, SessionService],
+ exports: [AuthRouter, AuthService],
controllers: [AuthController],
- exports: [AuthService]
})
export class AuthModule { }
diff --git a/apps/server/src/auth/auth.router.ts b/apps/server/src/auth/auth.router.ts
new file mode 100755
index 0000000..1392d41
--- /dev/null
+++ b/apps/server/src/auth/auth.router.ts
@@ -0,0 +1,20 @@
+import { Injectable } from '@nestjs/common';
+import { TrpcService } from '@server/trpc/trpc.service';
+import { AuthSchema, ObjectModelMethodSchema } from '@nicestack/common';
+import { AuthService } from './auth.service';
+
+@Injectable()
+export class AuthRouter {
+ constructor(
+ private readonly trpc: TrpcService,
+ private readonly authService: AuthService,
+ ) { }
+ router = this.trpc.router({
+ signUp: this.trpc.procedure
+ .input(AuthSchema.signUpRequest)
+ .mutation(async ({ input }) => {
+ const result = await this.authService.signUp(input);
+ return result;
+ }),
+ });
+}
diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts
old mode 100644
new mode 100755
index 9009b7d..5b93b06
--- a/apps/server/src/auth/auth.service.ts
+++ b/apps/server/src/auth/auth.service.ts
@@ -2,170 +2,187 @@ import {
Injectable,
UnauthorizedException,
BadRequestException,
- NotFoundException,
+ Logger,
+ InternalServerErrorException,
} from '@nestjs/common';
+import { StaffService } from '../models/staff/staff.service';
+import {
+ db,
+ AuthSchema,
+ JwtPayload,
+} from '@nicestack/common';
+import * as argon2 from 'argon2';
import { JwtService } from '@nestjs/jwt';
-import * as bcrypt from 'bcrypt';
-import { AuthSchema, db, z } from '@nicestack/common';
-import { StaffService } from '@server/models/staff/staff.service';
-import { JwtPayload } from '@nicestack/common';
-import { RoleMapService } from '@server/rbac/rolemap.service';
+import { redis } from '@server/utils/redis/redis.service';
+import { UserProfileService } from './utils';
+import { SessionInfo, SessionService } from './session.service';
+import { tokenConfig } from './config';
+import { z } from 'zod';
@Injectable()
export class AuthService {
+ private logger = new Logger(AuthService.name)
constructor(
- private readonly jwtService: JwtService,
private readonly staffService: StaffService,
- private readonly roleMapService: RoleMapService
+ private readonly jwtService: JwtService,
+ private readonly sessionService: SessionService,
) { }
- async signIn(data: z.infer) {
+ private async generateTokens(payload: JwtPayload): Promise<{
+ accessToken: string;
+ refreshToken: string;
+ }> {
+ const [accessToken, refreshToken] = await Promise.all([
+ this.jwtService.signAsync(payload, {
+ expiresIn: `${tokenConfig.accessToken.expirationMs / 1000}s`,
+ }),
+ this.jwtService.signAsync(
+ { sub: payload.sub },
+ { expiresIn: `${tokenConfig.refreshToken.expirationMs / 1000}s` },
+ ),
+ ]);
+
+ return { accessToken, refreshToken };
+ }
+
+ async signIn(data: z.infer): Promise {
const { username, password, phoneNumber } = data;
- // Find the staff by either username or phoneNumber
- const staff = await db.staff.findFirst({
- where: {
- OR: [
- { username },
- { phoneNumber }
- ]
- }
+
+ let staff = await db.staff.findFirst({
+ where: { OR: [{ username }, { phoneNumber }], deletedAt: null },
});
- if (!staff) {
- throw new UnauthorizedException('Invalid username/phone number or password');
+ if (!staff && phoneNumber) {
+ staff = await this.signUp({
+ showname: '新用户',
+ username: phoneNumber,
+ phoneNumber,
+ password: phoneNumber,
+ });
+ } else if (!staff) {
+ throw new UnauthorizedException('帐号不存在');
}
-
- const isPasswordMatch = await bcrypt.compare(password, staff.password);
+ if (!staff.enabled) {
+ throw new UnauthorizedException('帐号已禁用');
+ }
+ const isPasswordMatch = phoneNumber || await argon2.verify(staff.password, password);
if (!isPasswordMatch) {
- throw new UnauthorizedException('Invalid username/phone number or password');
+ throw new UnauthorizedException('帐号或密码错误');
}
- const payload: JwtPayload = { sub: staff.id, username: staff.username };
- const accessToken = await this.jwtService.signAsync(payload, { expiresIn: '1h' });
- const refreshToken = await this.generateRefreshToken(staff.id);
+ try {
+ const payload = { sub: staff.id, username: staff.username };
+ const { accessToken, refreshToken } = await this.generateTokens(payload);
- // Calculate expiration dates
- const accessTokenExpiresAt = new Date();
- accessTokenExpiresAt.setHours(accessTokenExpiresAt.getHours() + 1);
+ return await this.sessionService.createSession(
+ staff.id,
+ accessToken,
+ refreshToken,
+ {
+ accessTokenExpirationMs: tokenConfig.accessToken.expirationMs,
+ refreshTokenExpirationMs: tokenConfig.refreshToken.expirationMs,
+ sessionTTL: tokenConfig.accessToken.expirationTTL,
+ },
+ );
+ } catch (error) {
+ this.logger.error(error);
+ throw new InternalServerErrorException('创建会话失败');
+ }
+ }
+ async signUp(data: z.infer) {
+ const { username, phoneNumber, officerId } = data;
- const refreshTokenExpiresAt = new Date();
- refreshTokenExpiresAt.setDate(refreshTokenExpiresAt.getDate() + 7);
-
- // Store the refresh token in the database
- await db.refreshToken.create({
- data: {
- staffId: staff.id,
- token: refreshToken,
+ const existingUser = await db.staff.findFirst({
+ where: {
+ OR: [{ username }, { officerId }, { phoneNumber }],
+ deletedAt: null
},
});
- return {
- access_token: accessToken,
- access_token_expires_at: accessTokenExpiresAt,
- refresh_token: refreshToken,
- refresh_token_expires_at: refreshTokenExpiresAt,
- };
- }
+ if (existingUser) {
+ throw new BadRequestException('帐号或证件号已存在');
+ }
- async generateRefreshToken(userId: string): Promise {
- const payload = { sub: userId };
- return this.jwtService.signAsync(payload, { expiresIn: '7d' }); // Set an appropriate expiration
+ return this.staffService.create({
+ data: {
+ ...data,
+ domainId: data.deptId,
+ }
+ });
}
-
async refreshToken(data: z.infer) {
- const { refreshToken } = data;
+ const { refreshToken, sessionId } = data;
let payload: JwtPayload;
try {
payload = this.jwtService.verify(refreshToken);
- } catch (error) {
- throw new UnauthorizedException('Invalid refresh token');
+ } catch {
+ throw new UnauthorizedException('用户会话已过期');
}
- const storedToken = await db.refreshToken.findUnique({ where: { token: refreshToken } });
- if (!storedToken) {
- throw new UnauthorizedException('Invalid refresh token');
+ const session = await this.sessionService.getSession(payload.sub, sessionId);
+ if (!session || session.refresh_token !== refreshToken) {
+ throw new UnauthorizedException('用户会话已过期');
}
- const user = await db.staff.findUnique({ where: { id: payload.sub } });
+ const user = await db.staff.findUnique({ where: { id: payload.sub, deletedAt: null } });
if (!user) {
- throw new UnauthorizedException('Invalid refresh token');
+ throw new UnauthorizedException('用户不存在');
}
- const newAccessToken = await this.jwtService.signAsync(
- { sub: user.id, username: user.username },
- { expiresIn: '1h' },
+ const { accessToken } = await this.generateTokens({
+ sub: user.id,
+ username: user.username,
+ });
+
+ const updatedSession = {
+ ...session,
+ access_token: accessToken,
+ access_token_expires_at: Date.now() + tokenConfig.accessToken.expirationMs,
+ };
+ await this.sessionService.saveSession(
+ payload.sub,
+ updatedSession,
+ tokenConfig.accessToken.expirationTTL,
);
-
- // Calculate new expiration date
- const accessTokenExpiresAt = new Date();
- accessTokenExpiresAt.setHours(accessTokenExpiresAt.getHours() + 1);
-
+ await redis.del(UserProfileService.instance.getProfileCacheKey(payload.sub));
return {
- access_token: newAccessToken,
- access_token_expires_at: accessTokenExpiresAt,
+ access_token: accessToken,
+ access_token_expires_at: updatedSession.access_token_expires_at,
};
}
-
- async signUp(data: z.infer) {
- const { username, password, phoneNumber } = data;
-
- const existingUserByUsername = await db.staff.findUnique({ where: { username } });
- if (existingUserByUsername) {
- throw new BadRequestException('Username is already taken');
- }
- if (phoneNumber) {
- const existingUserByPhoneNumber = await db.staff.findUnique({ where: { phoneNumber } });
- if (existingUserByPhoneNumber) {
- throw new BadRequestException('Phone number is already taken');
- }
- }
- const hashedPassword = await bcrypt.hash(password, 10);
- const staff = await this.staffService.create({
- username,
- phoneNumber,
- password: hashedPassword,
- });
-
- return staff;
- }
- async logout(data: z.infer) {
- const { refreshToken } = data;
- await db.refreshToken.deleteMany({ where: { token: refreshToken } });
- return { message: 'Logout successful' };
- }
-
async changePassword(data: z.infer) {
- const { oldPassword, newPassword, username } = data;
- const user = await db.staff.findUnique({ where: { username } });
+ const { newPassword, phoneNumber, username } = data;
+ const user = await db.staff.findFirst({
+ where: { OR: [{ username }, { phoneNumber }], deletedAt: null },
+ });
if (!user) {
- throw new NotFoundException('User not found');
+ throw new UnauthorizedException('用户不存在');
}
-
- const isPasswordMatch = await bcrypt.compare(oldPassword, user.password);
- if (!isPasswordMatch) {
- throw new UnauthorizedException('Old password is incorrect');
- }
-
- const hashedNewPassword = await bcrypt.hash(newPassword, 10);
- await this.staffService.update({ id: user.id, password: hashedNewPassword });
-
- return { message: 'Password successfully changed' };
- }
- async getUserProfile(data: JwtPayload) {
- const { sub } = data
- const staff = await db.staff.findUnique({
- where: { id: sub }, include: {
- department: true,
- domain: true
+ await this.staffService.update({
+ where: { id: user?.id },
+ data: {
+ password: newPassword,
}
- })
- const staffPerms = await this.roleMapService.getPermsForObject({
- domainId: staff.domainId,
- staffId: staff.id,
- deptId: staff.deptId,
});
- return { ...staff, permissions: staffPerms }
+
+ return { message: '密码已修改' };
}
-}
+ async logout(data: z.infer) {
+ const { refreshToken, sessionId } = data;
+
+ try {
+ const payload = this.jwtService.verify(refreshToken);
+ await Promise.all([
+ this.sessionService.deleteSession(payload.sub, sessionId),
+ redis.del(UserProfileService.instance.getProfileCacheKey(payload.sub)),
+ ]);
+ } catch {
+ throw new UnauthorizedException('无效的会话');
+ }
+
+ return { message: '注销成功' };
+ }
+
+}
\ No newline at end of file
diff --git a/apps/server/src/auth/config.ts b/apps/server/src/auth/config.ts
new file mode 100644
index 0000000..0edaf5d
--- /dev/null
+++ b/apps/server/src/auth/config.ts
@@ -0,0 +1,9 @@
+export const tokenConfig = {
+ accessToken: {
+ expirationMs: 7 * 24 * 3600000, // 7 days
+ expirationTTL: 7 * 24 * 60 * 60, // 7 days in seconds
+ },
+ refreshToken: {
+ expirationMs: 30 * 24 * 3600000, // 30 days
+ },
+};
\ No newline at end of file
diff --git a/apps/server/src/auth/session.service.ts b/apps/server/src/auth/session.service.ts
new file mode 100644
index 0000000..9ee9022
--- /dev/null
+++ b/apps/server/src/auth/session.service.ts
@@ -0,0 +1,61 @@
+// session.service.ts
+import { Injectable } from '@nestjs/common';
+import { redis } from '@server/utils/redis/redis.service';
+import { v4 as uuidv4 } from 'uuid';
+
+export interface SessionInfo {
+ session_id: string;
+ access_token: string;
+ access_token_expires_at: number;
+ refresh_token: string;
+ refresh_token_expires_at: number;
+}
+
+@Injectable()
+export class SessionService {
+ private getSessionKey(userId: string, sessionId: string): string {
+ return `session-${userId}-${sessionId}`;
+ }
+ async createSession(
+ userId: string,
+ accessToken: string,
+ refreshToken: string,
+ expirationConfig: {
+ accessTokenExpirationMs: number;
+ refreshTokenExpirationMs: number;
+ sessionTTL: number;
+ },
+ ): Promise {
+ const sessionInfo: SessionInfo = {
+ session_id: uuidv4(),
+ access_token: accessToken,
+ access_token_expires_at: Date.now() + expirationConfig.accessTokenExpirationMs,
+ refresh_token: refreshToken,
+ refresh_token_expires_at: Date.now() + expirationConfig.refreshTokenExpirationMs,
+ };
+
+ await this.saveSession(userId, sessionInfo, expirationConfig.sessionTTL);
+ return sessionInfo;
+ }
+
+ async getSession(userId: string, sessionId: string): Promise {
+ const sessionData = await redis.get(this.getSessionKey(userId, sessionId));
+ return sessionData ? JSON.parse(sessionData) : null;
+ }
+
+ async saveSession(
+ userId: string,
+ sessionInfo: SessionInfo,
+ ttl: number,
+ ): Promise {
+ await redis.setex(
+ this.getSessionKey(userId, sessionInfo.session_id),
+ ttl,
+ JSON.stringify(sessionInfo),
+ );
+ }
+
+ async deleteSession(userId: string, sessionId: string): Promise {
+ await redis.del(this.getSessionKey(userId, sessionId));
+ }
+}
\ No newline at end of file
diff --git a/apps/server/src/auth/types.ts b/apps/server/src/auth/types.ts
new file mode 100644
index 0000000..3404d3c
--- /dev/null
+++ b/apps/server/src/auth/types.ts
@@ -0,0 +1,9 @@
+export interface TokenConfig {
+ accessToken: {
+ expirationMs: number;
+ expirationTTL: number;
+ };
+ refreshToken: {
+ expirationMs: number;
+ };
+}
diff --git a/apps/server/src/auth/utils.ts b/apps/server/src/auth/utils.ts
new file mode 100644
index 0000000..234a3ae
--- /dev/null
+++ b/apps/server/src/auth/utils.ts
@@ -0,0 +1,187 @@
+import { DepartmentService } from '@server/models/department/department.service';
+import {
+ UserProfile,
+ db,
+ JwtPayload,
+ RolePerms,
+ ObjectType,
+} from '@nicestack/common';
+import { JwtService } from '@nestjs/jwt';
+import { env } from '@server/env';
+import { redis } from '@server/utils/redis/redis.service';
+import EventBus from '@server/utils/event-bus';
+import { RoleMapService } from '@server/models/rbac/rolemap.service';
+
+interface ProfileResult {
+ staff: UserProfile | undefined;
+ error?: string;
+}
+
+interface TokenVerifyResult {
+ id?: string;
+ error?: string;
+}
+
+export class UserProfileService {
+ public static readonly instance = new UserProfileService();
+
+ private readonly CACHE_TTL = 3600; // 缓存时间1小时
+ private readonly jwtService: JwtService;
+ private readonly departmentService: DepartmentService;
+ private readonly roleMapService: RoleMapService;
+
+ private constructor() {
+ this.jwtService = new JwtService();
+ this.departmentService = new DepartmentService();
+ this.roleMapService = new RoleMapService(this.departmentService);
+ EventBus.on("dataChanged", ({ type, data }) => {
+ if (type === ObjectType.STAFF) {
+ // 确保 data 是数组,如果不是则转换为数组
+ const dataArray = Array.isArray(data) ? data : [data];
+ for (const item of dataArray) {
+ if (item.id) {
+ redis.del(this.getProfileCacheKey(item.id));
+ }
+ }
+ }
+ });
+
+ }
+ public getProfileCacheKey(id: string) {
+ return `user-profile-${id}`;
+ }
+ /**
+ * 验证并解析token
+ */
+ public async verifyToken(token?: string): Promise {
+ if (!token) {
+ return {};
+ }
+ try {
+ const { sub: id } = await this.jwtService.verifyAsync(token, {
+ secret: env.JWT_SECRET,
+ });
+ return { id };
+ } catch (error) {
+ return {
+ error:
+ error instanceof Error ? error.message : 'Token verification failed',
+ };
+ }
+ }
+
+ /**
+ * 通过Token获取用户信息
+ */
+ public async getUserProfileByToken(token?: string): Promise {
+ const { id, error } = await this.verifyToken(token);
+ if (error) {
+ return {
+ staff: undefined,
+ error,
+ };
+ }
+ return await this.getUserProfileById(id);
+ }
+
+ /**
+ * 通过用户ID获取用户信息
+ */
+ public async getUserProfileById(id?: string): Promise {
+ if (!id) {
+ return { staff: undefined };
+ }
+ try {
+ const cachedProfile = await this.getCachedProfile(id);
+ if (cachedProfile) {
+ return { staff: cachedProfile };
+ }
+ const staff = await this.getBaseProfile(id);
+ if (!staff) {
+ throw new Error(`User with id ${id} does not exist`);
+ }
+
+ await this.populateStaffExtras(staff);
+ await this.cacheProfile(id, staff);
+
+ return { staff };
+ } catch (error) {
+ return {
+ staff: undefined,
+ error:
+ error instanceof Error ? error.message : 'Failed to get user profile',
+ };
+ }
+ }
+
+ /**
+ * 从缓存获取用户信息
+ */
+ private async getCachedProfile(id: string): Promise {
+ const cachedData = await redis.get(this.getProfileCacheKey(id));
+ if (!cachedData) return null;
+
+ try {
+ const profile = JSON.parse(cachedData) as UserProfile;
+ return profile.id === id ? profile : null;
+ } catch {
+ return null;
+ }
+ }
+
+ /**
+ * 缓存用户信息
+ */
+ private async cacheProfile(id: string, profile: UserProfile): Promise {
+ await redis.set(
+ this.getProfileCacheKey(id),
+ JSON.stringify(profile),
+ 'EX',
+ this.CACHE_TTL,
+ );
+ }
+
+ /**
+ * 获取基础用户信息
+ */
+ private async getBaseProfile(id: string): Promise {
+ return (await db.staff.findUnique({
+ where: { id },
+ select: {
+ id: true,
+ deptId: true,
+ department: true,
+ domainId: true,
+ domain: true,
+ showname: true,
+ username: true,
+ phoneNumber: true,
+ },
+ })) as unknown as UserProfile;
+ }
+
+ /**
+ * 填充用户权限信息
+ */
+ private async populateStaffExtras(staff: UserProfile): Promise {
+ const [deptIds, parentDeptIds, permissions] = await Promise.all([
+ staff.deptId
+ ? this.departmentService.getDescendantIdsInDomain(staff.deptId)
+ : [],
+ staff.deptId
+ ? this.departmentService.getAncestorIds([staff.deptId])
+ : [],
+ this.roleMapService.getPermsForObject({
+ domainId: staff.domainId,
+ staffId: staff.id,
+ deptId: staff.deptId,
+ }) as Promise,
+ ]);
+
+ Object.assign(staff, {
+ deptIds,
+ parentDeptIds,
+ permissions,
+ });
+ }
+}
diff --git a/apps/server/src/env.ts b/apps/server/src/env.ts
index 8c648d7..74aecae 100755
--- a/apps/server/src/env.ts
+++ b/apps/server/src/env.ts
@@ -1,4 +1,3 @@
-export const env: { JWT_SECRET: string, APP_URL: string } = {
- JWT_SECRET: process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA=',
- APP_URL: process.env.APP_URL || 'http://localhost:5173'
+export const env: { JWT_SECRET: string } = {
+ JWT_SECRET: process.env.JWT_SECRET || '/yT9MnLm/r6NY7ee2Fby6ihCHZl+nFx4OQFKupivrhA='
}
\ No newline at end of file
diff --git a/apps/server/src/filters/exceptions.filter.ts b/apps/server/src/filters/exceptions.filter.ts
new file mode 100644
index 0000000..9c82fdd
--- /dev/null
+++ b/apps/server/src/filters/exceptions.filter.ts
@@ -0,0 +1,25 @@
+import { ArgumentsHost, Catch, ExceptionFilter, HttpException, HttpStatus } from '@nestjs/common';
+import { Response } from 'express';
+@Catch()
+export class ExceptionsFilter implements ExceptionFilter {
+ catch(exception: unknown, host: ArgumentsHost) {
+ const ctx = host.switchToHttp();
+ const response = ctx.getResponse();
+ const request = ctx.getRequest();
+
+ const status = exception instanceof HttpException
+ ? exception.getStatus()
+ : HttpStatus.INTERNAL_SERVER_ERROR;
+
+ const message = exception instanceof HttpException
+ ? exception.message
+ : 'Internal server error';
+
+ response.status(status).json({
+ statusCode: status,
+ timestamp: new Date().toISOString(),
+ path: request.url,
+ message,
+ });
+ }
+}
diff --git a/apps/server/src/init/init.module.ts b/apps/server/src/init/init.module.ts
deleted file mode 100644
index e51a559..0000000
--- a/apps/server/src/init/init.module.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { Module } from '@nestjs/common';
-import { InitService } from './init.service';
-import { AuthModule } from '@server/auth/auth.module';
-import { MinioModule } from '@server/minio/minio.module';
-
-@Module({
- imports: [AuthModule, MinioModule],
- providers: [InitService],
- exports: [InitService]
-})
-export class InitModule { }
diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts
old mode 100644
new mode 100755
index 10abb60..ff75e3f
--- a/apps/server/src/main.ts
+++ b/apps/server/src/main.ts
@@ -1,16 +1,23 @@
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { TrpcRouter } from './trpc/trpc.router';
-import { env } from './env';
+import { WebSocketService } from './socket/websocket.service';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
+
+ // 启用 CORS 并允许所有来源
app.enableCors({
- origin: [env.APP_URL],
- credentials: true
+ origin: "*",
});
+ const wsService = app.get(WebSocketService);
+ await wsService.initialize(app.getHttpServer());
const trpc = app.get(TrpcRouter);
trpc.applyMiddleware(app);
- await app.listen(3000);
+
+ const port = process.env.SERVER_PORT || 3000;
+
+ await app.listen(port);
+
}
bootstrap();
diff --git a/apps/server/src/models/app-config/app-config.module.ts b/apps/server/src/models/app-config/app-config.module.ts
new file mode 100644
index 0000000..732313c
--- /dev/null
+++ b/apps/server/src/models/app-config/app-config.module.ts
@@ -0,0 +1,12 @@
+import { Module } from '@nestjs/common';
+import { AppConfigService } from './app-config.service';
+import { TrpcService } from '@server/trpc/trpc.service';
+import { AppConfigRouter } from './app-config.router';
+import { RealTimeModule } from '@server/socket/realtime/realtime.module';
+
+@Module({
+ imports: [RealTimeModule],
+ providers: [AppConfigService, AppConfigRouter, TrpcService],
+ exports: [AppConfigService, AppConfigRouter]
+})
+export class AppConfigModule { }
diff --git a/apps/server/src/models/app-config/app-config.router.ts b/apps/server/src/models/app-config/app-config.router.ts
new file mode 100644
index 0000000..3adf36e
--- /dev/null
+++ b/apps/server/src/models/app-config/app-config.router.ts
@@ -0,0 +1,47 @@
+import { Injectable } from '@nestjs/common';
+import { TrpcService } from '@server/trpc/trpc.service';
+import { AppConfigService } from './app-config.service';
+import { z, ZodType } from 'zod';
+import { Prisma } from '@nicestack/common';
+import { RealtimeServer } from '@server/socket/realtime/realtime.server';
+const AppConfigUncheckedCreateInputSchema: ZodType = z.any()
+const AppConfigUpdateArgsSchema: ZodType = z.any()
+const AppConfigDeleteManyArgsSchema: ZodType = z.any()
+const AppConfigFindFirstArgsSchema: ZodType = z.any()
+@Injectable()
+export class AppConfigRouter {
+ constructor(
+ private readonly trpc: TrpcService,
+ private readonly appConfigService: AppConfigService,
+ private readonly realtimeServer: RealtimeServer
+ ) { }
+ router = this.trpc.router({
+ create: this.trpc.protectProcedure
+ .input(AppConfigUncheckedCreateInputSchema)
+ .mutation(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.appConfigService.create({ data: input });
+ }),
+ update: this.trpc.protectProcedure
+ .input(AppConfigUpdateArgsSchema)
+ .mutation(async ({ ctx, input }) => {
+
+ const { staff } = ctx;
+ return await this.appConfigService.update(input);
+ }),
+ deleteMany: this.trpc.protectProcedure.input(AppConfigDeleteManyArgsSchema).mutation(async ({ input }) => {
+ return await this.appConfigService.deleteMany(input)
+ }),
+ findFirst: this.trpc.protectProcedure.input(AppConfigFindFirstArgsSchema).
+ query(async ({ input }) => {
+
+ return await this.appConfigService.findFirst(input)
+ }),
+ clearRowCache: this.trpc.protectProcedure.mutation(async () => {
+ return await this.appConfigService.clearRowCache()
+ }),
+ getClientCount: this.trpc.protectProcedure.query(() => {
+ return this.realtimeServer.getClientCount()
+ })
+ });
+}
diff --git a/apps/server/src/models/app-config/app-config.service.ts b/apps/server/src/models/app-config/app-config.service.ts
new file mode 100644
index 0000000..7915667
--- /dev/null
+++ b/apps/server/src/models/app-config/app-config.service.ts
@@ -0,0 +1,21 @@
+import { Injectable } from '@nestjs/common';
+import {
+ db,
+ ObjectType,
+ Prisma,
+} from '@nicestack/common';
+
+
+import { BaseService } from '../base/base.service';
+import { deleteByPattern } from '@server/utils/redis/utils';
+
+@Injectable()
+export class AppConfigService extends BaseService {
+ constructor() {
+ super(db, "appConfig");
+ }
+ async clearRowCache() {
+ await deleteByPattern("row-*")
+ return true
+ }
+}
diff --git a/apps/server/src/models/base/base.service.ts b/apps/server/src/models/base/base.service.ts
new file mode 100644
index 0000000..457c91c
--- /dev/null
+++ b/apps/server/src/models/base/base.service.ts
@@ -0,0 +1,562 @@
+import { db, Prisma, PrismaClient } from '@nicestack/common';
+import {
+ Operations,
+ DelegateArgs,
+ DelegateReturnTypes,
+ DataArgs,
+ WhereArgs,
+ DelegateFuncs,
+ UpdateOrderArgs,
+ TransactionType,
+} from './base.type';
+import {
+ NotFoundException,
+ InternalServerErrorException,
+} from '@nestjs/common';
+import { ERROR_MAP, operationT, PrismaErrorCode } from './errorMap.prisma';
+
+/**
+ * BaseService provides a generic CRUD interface for a prisma model.
+ * It enables common data operations such as find, create, update, and delete.
+ *
+ * @template D - Type for the model delegate, defining available operations.
+ * @template A - Arguments for the model delegate's operations.
+ * @template R - Return types for the model delegate's operations.
+ */
+export class BaseService<
+ D extends DelegateFuncs,
+ A extends DelegateArgs = DelegateArgs,
+ R extends DelegateReturnTypes = DelegateReturnTypes,
+> {
+ protected ORDER_INTERVAL = 100;
+ /**
+ * Initializes the BaseService with the specified model.
+ * @param model - The Prisma model delegate for database operations.
+ */
+ constructor(
+ protected prisma: PrismaClient,
+ protected objectType: string,
+ protected enableOrder: boolean = false
+ ) {
+
+ }
+
+ /**
+ * Retrieves the name of the model dynamically.
+ * @returns {string} - The name of the model.
+ */
+ private getModelName(): string {
+ const modelName = this.getModel().constructor.name;
+ return modelName;
+ }
+ private getModel(tx?: TransactionType): D {
+ return tx?.[this.objectType] || this.prisma[this.objectType] as D;
+ }
+ /**
+ * Error handling helper function
+ */
+ private handleError(error: any, operation: operationT): never {
+ if (error instanceof Prisma.PrismaClientKnownRequestError) {
+ const handler = ERROR_MAP[error.code as PrismaErrorCode];
+ if (handler) {
+ throw handler(
+ operation,
+ error?.meta || {
+ target: 'record',
+ model: this.getModelName(),
+ },
+ );
+ }
+ throw new InternalServerErrorException(
+ `Database error: ${error.message}`,
+ );
+ }
+ throw new InternalServerErrorException(
+ `Unexpected error: ${error.message || 'Unknown error occurred.'}`,
+ );
+ }
+ /**
+ * Finds a unique record by given criteria.
+ * @param args - Arguments to find a unique record.
+ * @returns {Promise} - A promise resolving to the found record.
+ * @example
+ * const user = await service.findUnique({ where: { id: 'user_id' } });
+ */
+ async findUnique(args: A['findUnique']): Promise {
+ try {
+ return this.getModel().findUnique(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'read');
+ }
+ }
+ /**
+ * Finds the first record matching the given criteria.
+ * @param args - Arguments to find the first matching record.
+ * @returns {Promise} - A promise resolving to the first matching record.
+ * @example
+ * const firstUser = await service.findFirst({ where: { name: 'John' } });
+ */
+ async findFirst(args: A['findFirst']): Promise {
+ try {
+ return this.getModel().findFirst(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'read');
+ }
+ }
+
+ /**
+ * Finds a record by its ID.
+ * @param id - The ID of the record to find.
+ * @param args - Optional additional arguments for the find operation.
+ * @returns {Promise} - A promise resolving to the found record.
+ * @throws {NotFoundException} - If no record is found with the given ID.
+ * @example
+ * const user = await service.findById('user_id');
+ */
+ async findById(id: string, args?: A['findFirst']): Promise {
+ try {
+ const record = (await this.getModel().findFirst({
+ where: { id },
+ ...(args || {}),
+ })) as R['findFirst'];
+ if (!record) {
+ throw new NotFoundException(`Record with ID ${id} not found.`);
+ }
+ return record;
+ } catch (error) {
+ this.handleError(error, 'read');
+ }
+ }
+
+ /**
+ * Finds multiple records matching the given criteria.
+ * @param args - Arguments to find multiple records.
+ * @returns {Promise} - A promise resolving to the list of found records.
+ * @example
+ * const users = await service.findMany({ where: { isActive: true } });
+ */
+ async findMany(args: A['findMany']): Promise {
+ try {
+ return this.getModel().findMany(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'read');
+ }
+ }
+
+ /**
+ * Creates a new record with the given data.
+ * @param args - Arguments to create a record.
+ * @returns {Promise} - A promise resolving to the created record.
+ * @example
+ * const newUser = await service.create({ data: { name: 'John Doe' } });
+ */
+ async create(args: A['create'], params?: any): Promise {
+
+ try {
+
+ if (this.enableOrder && !(args as any).data.order) {
+ // 查找当前最大的 order 值
+ const maxOrderItem = await this.getModel(params?.tx).findFirst({
+ orderBy: { order: 'desc' }
+ }) as any;
+ // 设置新记录的 order 值
+ const newOrder = maxOrderItem ? maxOrderItem.order + this.ORDER_INTERVAL : 1;
+ // 将 order 添加到创建参数中
+ (args as any).data.order = newOrder;
+ }
+ return this.getModel(params?.tx).create(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'create');
+ }
+ }
+
+
+ /**
+ * Creates multiple new records with the given data.
+ * @param args - Arguments to create multiple records.
+ * @returns {Promise} - A promise resolving to the created records.
+ * @example
+ * const newUsers = await service.createMany({ data: [{ name: 'John' }, { name: 'Jane' }] });
+ */
+ async createMany(args: A['createMany'], params?: any): Promise {
+ try {
+ return this.getModel(params?.tx).createMany(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'create');
+ }
+ }
+
+ /**
+ * Updates a record with the given data.
+ * @param args - Arguments to update a record.
+ * @returns {Promise} - A promise resolving to the updated record.
+ * @example
+ * const updatedUser = await service.update({ where: { id: 'user_id' }, data: { name: 'John' } });
+ */
+ async update(args: A['update'], params?: any): Promise {
+ try {
+
+ return this.getModel(params?.tx).update(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'update');
+ }
+ }
+
+ /**
+ * Updates a record by ID with the given data.
+ * @param id - The ID of the record to update.
+ * @param data - The data to update the record with.
+ * @returns {Promise} - A promise resolving to the updated record.
+ * @example
+ * const updatedUser = await service.updateById('user_id', { name: 'John Doe' });
+ */
+ async updateById(
+ id: string,
+ data: DataArgs,
+ ): Promise {
+ try {
+ return (await this.getModel().update({
+ where: { id },
+ data: data as any,
+ })) as R['update'];
+ } catch (error) {
+ this.handleError(error, 'update');
+ }
+ }
+
+ /**
+ * Deletes a record by ID.
+ * @param id - The ID of the record to delete.
+ * @returns {Promise} - A promise resolving to the deleted record.
+ * @example
+ * const deletedUser = await service.deleteById('user_id');
+ */
+ async deleteById(id: string): Promise {
+ try {
+ return (await this.getModel().delete({
+ where: { id },
+ })) as R['delete'];
+ } catch (error) {
+ this.handleError(error, 'delete');
+ }
+ }
+
+ /**
+ * Deletes a record based on the given criteria.
+ * @param args - Arguments to delete a record.
+ * @returns {Promise} - A promise resolving to the deleted record.
+ * @example
+ * const deletedUser = await service.delete({ where: { name: 'John' } });
+ */
+ async delete(args: A['delete'], params?: any): Promise {
+ try {
+ return this.getModel(params?.tx).delete(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'delete');
+ }
+ }
+
+ /**
+ * Creates or updates a record based on the given criteria.
+ * @param args - Arguments to upsert a record.
+ * @returns {Promise} - A promise resolving to the created or updated record.
+ * @example
+ * const user = await service.upsert({ where: { id: 'user_id' }, create: { name: 'John' }, update: { name: 'Johnny' } });
+ */
+ async upsert(args: A['upsert']): Promise {
+ try {
+ return this.getModel().upsert(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'create');
+ }
+ }
+
+ /**
+ * Counts the number of records matching the given criteria.
+ * @param args - Arguments to count records.
+ * @returns {Promise} - A promise resolving to the count.
+ * @example
+ * const userCount = await service.count({ where: { isActive: true } });
+ */
+ async count(args: A['count']): Promise {
+ try {
+ return this.getModel().count(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'read');
+ }
+ }
+
+ /**
+ * Aggregates records based on the given criteria.
+ * @param args - Arguments to aggregate records.
+ * @returns {Promise} - A promise resolving to the aggregation result.
+ * @example
+ * const userAggregates = await service.aggregate({ _count: true });
+ */
+ async aggregate(args: A['aggregate']): Promise {
+ try {
+ return this.getModel().aggregate(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'read');
+ }
+ }
+
+ /**
+ * Deletes multiple records based on the given criteria.
+ * @param args - Arguments to delete multiple records.
+ * @returns {Promise} - A promise resolving to the result of the deletion.
+ * @example
+ * const deleteResult = await service.deleteMany({ where: { isActive: false } });
+ */
+ async deleteMany(args: A['deleteMany'], params?: any): Promise {
+ try {
+
+ return this.getModel(params?.tx).deleteMany(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'delete');
+ }
+ }
+
+ /**
+ * Updates multiple records based on the given criteria.
+ * @param args - Arguments to update multiple records.
+ * @returns {Promise} - A promise resolving to the result of the update.
+ * @example
+ * const updateResult = await service.updateMany({ where: { isActive: true }, data: { isActive: false } });
+ */
+ async updateMany(args: A['updateMany']): Promise {
+ try {
+ return this.getModel().updateMany(args as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'update');
+ }
+ }
+
+ /**
+ * Finds a record by unique criteria or creates it if not found.
+ * @param args - Arguments to find or create a record.
+ * @returns {Promise} - A promise resolving to the found or created record.
+ * @example
+ * const user = await service.findOrCreate({ where: { email: 'john@example.com' }, create: { email: 'john@example.com', name: 'John' } });
+ */
+ async findOrCreate(args: {
+ where: WhereArgs;
+ create: DataArgs;
+ }): Promise {
+ try {
+ const existing = (await this.getModel().findUnique({
+ where: args.where,
+ } as any)) as R['findUnique'];
+ if (existing) {
+ return existing;
+ }
+ return this.getModel().create({ data: args.create } as any) as Promise<
+ R['create']
+ >;
+ } catch (error) {
+ this.handleError(error, 'create');
+ }
+ }
+
+ /**
+ * Checks if a record exists based on the given criteria.
+ * @param where - The criteria to check for existence.
+ * @returns {Promise} - A promise resolving to true if the record exists, false otherwise.
+ * @example
+ * const exists = await service.exists({ email: 'john@example.com' });
+ */
+ async exists(where: WhereArgs): Promise {
+ try {
+ const count = (await this.getModel().count({ where } as any)) as number;
+ return count > 0;
+ } catch (error) {
+ this.handleError(error, 'read');
+ }
+ }
+
+ /**
+ * Soft deletes records by setting `isDeleted` to true for the given IDs.
+ * @param ids - An array of IDs of the records to soft delete.
+ * @param data - Additional data to update on soft delete. (Optional)
+ * @returns {Promise} - A promise resolving to an array of updated records.
+ * @example
+ * const softDeletedUsers = await service.softDeleteByIds(['user_id1', 'user_id2'], { reason: 'Bulk deletion' });
+ */
+ async softDeleteByIds(
+ ids: string[],
+ data: Partial> = {}, // Default to empty object
+ ): Promise {
+ try {
+ if (!ids || ids.length === 0) {
+ return []; // Return empty array if no IDs are provided
+ }
+ return this.getModel().updateMany({
+ where: { id: { in: ids } },
+ data: { ...data, deletedAt: new Date() } as any,
+ }) as Promise;
+ } catch (error) {
+ this.handleError(error, 'delete');
+ throw error; // Re-throw the error to be handled higher up
+ }
+ }
+ /**
+ * Restores soft-deleted records by setting `isDeleted` to false for the given IDs.
+ * @param ids - An array of IDs of the records to restore.
+ * @param data - Additional data to update on restore. (Optional)
+ * @returns {Promise} - A promise resolving to an array of updated records.
+ * @example
+ * const restoredUsers = await service.restoreByIds(['user_id1', 'user_id2'], { restoredBy: 'admin' });
+ */
+ async restoreByIds(
+ ids: string[],
+ data: Partial> = {}, // Default to empty object
+ ): Promise {
+ try {
+ if (!ids || ids.length === 0) {
+ return []; // Return empty array if no IDs are provided
+ }
+ return this.getModel().updateMany({
+ where: { id: { in: ids }, isDeleted: true }, // Only restore soft-deleted records
+ data: { ...data, deletedAt: null } as any,
+ }) as Promise;
+ } catch (error) {
+ this.handleError(error, "update");
+
+ }
+ }
+
+ /**
+ * Finds multiple records with pagination.
+ * @param args - Arguments including page, pageSize, and optional filters.
+ * @returns {Promise} - A promise resolving to the paginated list of records.
+ * @example
+ * const users = await service.findManyWithPagination({ page: 1, pageSize: 10, where: { isActive: true } });
+ */
+ async findManyWithPagination(args: {
+ page: number;
+ pageSize: number;
+ where?: WhereArgs;
+ }): Promise {
+ const { page, pageSize, where } = args;
+ try {
+ return this.getModel().findMany({
+ where,
+ skip: (page - 1) * pageSize,
+ take: pageSize,
+ } as any) as Promise;
+ } catch (error) {
+ this.handleError(error, 'read');
+ }
+ }
+ /**
+ * 基于游标的分页查询方法
+ * @description 该方法实现了基于游标的分页查询,相比传统的offset/limit分页有更好的性能
+ * @param args 查询参数对象,包含cursor、take、where、orderBy、select等字段
+ * @returns 返回查询结果对象,包含items数组、总数和下一页游标
+ */
+ async findManyWithCursor(
+ args: A['findMany'],
+ ): Promise<{ items: R['findMany']; nextCursor: string }> {
+ // 解构查询参数,设置默认每页取10条记录
+ const { cursor, take = 6, where, orderBy, select } = args as any;
+
+ try {
+
+ /**
+ * 执行查询
+ * @description 查询条件包含:
+ * 1. where - 过滤条件
+ * 2. orderBy - 排序规则,除了传入的排序外,还加入updatedAt和id的降序作为稳定排序
+ * 3. select - 选择返回的字段
+ * 4. take - 实际取n+1条记录,用于判断是否还有下一页
+ * 5. cursor - 游标定位,基于updatedAt和id的组合
+ */
+ const items = (await this.getModel().findMany({
+ where: where,
+ orderBy: [{ ...orderBy }, { updatedAt: 'desc' }, { id: 'desc' }],
+ select,
+ take: take + 1,
+ cursor: cursor
+ ? { updatedAt: cursor.split('_')[0], id: cursor.split('_')[1] }
+ : undefined,
+ } as any)) as any[];
+
+
+ /**
+ * 处理下一页游标
+ * @description
+ * 1. 如果查到的记录数超过take,说明还有下一页
+ * 2. 将最后一条记录弹出,用其updatedAt和id构造下一页游标
+ * 3. 游标格式为: updatedAt_id
+ */
+ let nextCursor: string | null = '';
+ if (items.length > take) {
+ const nextItem = items.pop();
+ nextCursor = `${nextItem!.updatedAt?.toISOString()}_${nextItem!.id}`;
+ }
+ if (nextCursor === '') {
+ nextCursor = null;
+ }
+
+ /**
+ * 返回查询结果
+ * @returns {Object}
+ * - items: 当前页记录
+ * - totalCount: 总记录数
+ * - nextCursor: 下一页游标
+ */
+ return {
+ items: items as R['findMany'],
+ nextCursor: nextCursor,
+ };
+ } catch (error) {
+ this.handleError(error, 'read');
+ }
+ }
+
+ async updateOrder(args: UpdateOrderArgs) {
+ const { id, overId } = args;
+ const [currentObject, targetObject] = (await Promise.all([
+ this.findFirst({ where: { id } } as any),
+ this.findFirst({ where: { id: overId } } as any),
+ ])) as any;
+ if (!currentObject || !targetObject) {
+ throw new Error('Invalid object or target object');
+ }
+ const nextObject = (await this.findFirst({
+ where: {
+ order: { gt: targetObject.order },
+ deletedAt: null,
+ },
+ orderBy: { order: 'asc' }
+ } as any)) as any;
+
+ const newOrder = nextObject
+ ? (targetObject.order + nextObject.order) / 2
+ : targetObject.order + this.ORDER_INTERVAL;
+ return this.update({ where: { id }, data: { order: newOrder } } as any);
+ }
+ /**
+ * Wraps the result of a database operation with a transformation function.
+ * @template T - The type of the result to be transformed.
+ * @param operationPromise - The promise representing the database operation.
+ * @param transformFn - A function that transforms the result.
+ * @returns {Promise} - A promise resolving to the transformed result.
+ * @example
+ * const user = await service.wrapResult(
+ * service.findUnique({ where: { id: 'user_id' } }),
+ * (result) => ({ ...result, fullName: `${result.firstName} ${result.lastName}` })
+ * );
+ */
+ async wrapResult(
+ operationPromise: Promise,
+ transformFn: (result: T) => Promise,
+ ): Promise {
+ try {
+ const result = await operationPromise;
+ return await transformFn(result);
+ } catch (error) {
+ throw error; // Re-throw the error to maintain existing error handling
+ }
+ }
+}
diff --git a/apps/server/src/models/base/base.tree.service.ts b/apps/server/src/models/base/base.tree.service.ts
new file mode 100644
index 0000000..2a1cc7b
--- /dev/null
+++ b/apps/server/src/models/base/base.tree.service.ts
@@ -0,0 +1,389 @@
+import { Prisma, PrismaClient } from '@nicestack/common';
+import { BaseService } from "./base.service";
+import { DataArgs, DelegateArgs, DelegateFuncs, DelegateReturnTypes, UpdateOrderArgs } from "./base.type";
+
+/**
+ * BaseTreeService provides a generic CRUD interface for a tree prisma model.
+ * It enables common data operations such as find, create, update, and delete.
+ *
+ * @template D - Type for the model delegate, defining available operations.
+ * @template A - Arguments for the model delegate's operations.
+ * @template R - Return types for the model delegate's operations.
+ */
+export class BaseTreeService<
+ D extends DelegateFuncs,
+ A extends DelegateArgs = DelegateArgs,
+ R extends DelegateReturnTypes = DelegateReturnTypes,
+> extends BaseService {
+
+ constructor(
+ protected prisma: PrismaClient,
+ protected objectType: string,
+ protected ancestryType: string = objectType + 'Ancestry',
+ protected enableOrder: boolean = false
+ ) {
+ super(prisma, objectType, enableOrder)
+ }
+ async getNextOrder(
+ transaction: any,
+ parentId: string | null,
+ parentOrder?: number
+ ): Promise {
+ // 查找同层级最后一个节点的 order
+ const lastOrder = await transaction[this.objectType].findFirst({
+ where: {
+ parentId: parentId ?? null
+ },
+ select: { order: true },
+ orderBy: { order: 'desc' },
+ } as any);
+
+ // 如果有父节点
+ if (parentId) {
+ // 获取父节点的 order(如果未提供)
+ const parentNodeOrder = parentOrder ?? (
+ await transaction[this.objectType].findUnique({
+ where: { id: parentId },
+ select: { order: true }
+ })
+ )?.order ?? 0;
+
+ // 如果存在最后一个同层级节点,确保新节点 order 大于最后一个节点
+ // 否则,新节点 order 设置为父节点 order + 1
+ return lastOrder
+ ? Math.max(lastOrder.order + this.ORDER_INTERVAL,
+ parentNodeOrder + this.ORDER_INTERVAL)
+ : parentNodeOrder + this.ORDER_INTERVAL;
+ }
+
+ // 对于根节点,直接使用最后一个节点的 order + 1
+ return lastOrder?.order ? lastOrder?.order + this.ORDER_INTERVAL : 1;
+ }
+
+ async create(args: A['create']) {
+ const anyArgs = args as any
+ return this.prisma.$transaction(async (transaction) => {
+ if (this.enableOrder) {
+ // 获取新节点的 order
+ anyArgs.data.order = await this.getNextOrder(
+ transaction,
+ anyArgs?.data.parentId ?? null
+ );
+ }
+ // 创建节点
+ const result: any = await super.create(anyArgs, { tx: transaction });
+
+ // 更新父节点的 hasChildren 状态
+ if (anyArgs.data.parentId) {
+ await transaction[this.objectType].update({
+ where: { id: anyArgs.data.parentId },
+ data: { hasChildren: true }
+ });
+ }
+
+ // 创建祖先关系
+ const newAncestries = anyArgs.data.parentId
+ ? [
+ ...(
+ await transaction[this.ancestryType].findMany({
+ where: { descendantId: anyArgs.data.parentId },
+ select: { ancestorId: true, relDepth: true },
+ })
+ ).map(({ ancestorId, relDepth }) => ({
+ ancestorId,
+ descendantId: result.id,
+ relDepth: relDepth + 1,
+ })),
+ {
+ ancestorId: result.parentId,
+ descendantId: result.id,
+ relDepth: 1,
+ },
+ ]
+ : [{ ancestorId: null, descendantId: result.id, relDepth: 1 }];
+
+ await transaction[this.ancestryType].createMany({ data: newAncestries });
+
+ return result;
+ }) as Promise;
+ }
+
+
+ /**
+ * 更新现有单位,并在parentId更改时管理DeptAncestry关系。
+ * @param data - 用于更新现有单位的数据。
+ * @returns 更新后的单位对象。
+ */
+ async update(args: A['update']) {
+ const anyArgs = args as any
+ return this.prisma.$transaction(async (transaction) => {
+ const current = await transaction[this.objectType].findUnique({
+ where: { id: anyArgs.where.id },
+ });
+
+ if (!current) throw new Error('object not found');
+
+ const result: any = await super.update(anyArgs, { tx: transaction });
+
+ if (anyArgs.data.parentId !== current.parentId) {
+ await transaction[this.ancestryType].deleteMany({
+ where: { descendantId: result.id },
+ });
+ // 更新原父级的 hasChildren 状态
+ if (current.parentId) {
+ const childrenCount = await transaction[this.objectType].count({
+ where: { parentId: current.parentId, deletedAt: null }
+ });
+
+ if (childrenCount === 0) {
+ await transaction[this.objectType].update({
+ where: { id: current.parentId },
+ data: { hasChildren: false }
+ });
+ }
+ }
+ if (anyArgs.data.parentId) {
+ await transaction[this.objectType].update({
+ where: { id: anyArgs.data.parentId },
+ data: { hasChildren: true }
+ });
+ const parentAncestries = await transaction[this.ancestryType].findMany({
+ where: { descendantId: anyArgs.data.parentId },
+ });
+
+ const newAncestries = parentAncestries.map(
+ ({ ancestorId, relDepth }) => ({
+ ancestorId,
+ descendantId: result.id,
+ relDepth: relDepth + 1,
+ }),
+ );
+
+ newAncestries.push({
+ ancestorId: anyArgs.data.parentId,
+ descendantId: result.id,
+ relDepth: 1,
+ });
+
+ await transaction[this.ancestryType].createMany({ data: newAncestries });
+ } else {
+ await transaction[this.ancestryType].create({
+ data: { ancestorId: null, descendantId: result.id, relDepth: 0 },
+ });
+ }
+ }
+
+ return result;
+ }) as Promise;
+ }
+ /**
+ * Soft deletes records by setting `isDeleted` to true for the given IDs.
+ * @param ids - An array of IDs of the records to soft delete.
+ * @param data - Additional data to update on soft delete. (Optional)
+ * @returns {Promise} - A promise resolving to an array of updated records.
+ * @example
+ * const softDeletedUsers = await service.softDeleteByIds(['user_id1', 'user_id2'], { reason: 'Bulk deletion' });
+ */
+ async softDeleteByIds(
+ ids: string[],
+ data: Partial> = {}, // Default to empty object
+ ): Promise {
+ return this.prisma.$transaction(async tx => {
+ // 首先找出所有需要软删除的记录的父级ID
+ const parentIds = await tx[this.objectType].findMany({
+ where: {
+ id: { in: ids },
+ parentId: { not: null }
+ },
+ select: { parentId: true }
+ });
+
+ const uniqueParentIds = [...new Set(parentIds.map(p => p.parentId))];
+
+ // 执行软删除
+ const result = await super.softDeleteByIds(ids, data);
+
+ // 删除相关的祖先关系
+ await tx[this.ancestryType].deleteMany({
+ where: {
+ OR: [
+ { ancestorId: { in: ids } },
+ { descendantId: { in: ids } },
+ ],
+ }
+ });
+ // 更新父级的 hasChildren 状态
+ if (uniqueParentIds.length > 0) {
+ for (const parentId of uniqueParentIds) {
+ const remainingChildrenCount = await tx[this.objectType].count({
+ where: {
+ parentId: parentId,
+ deletedAt: null
+ }
+ });
+ if (remainingChildrenCount === 0) {
+ await tx[this.objectType].update({
+ where: { id: parentId },
+ data: { hasChildren: false }
+ });
+ }
+ }
+ }
+
+ return result;
+ }) as Promise;
+ }
+
+
+ getAncestors(ids: string[]) {
+ if (!ids || ids.length === 0) return [];
+ const validIds = ids.filter(id => id != null);
+ const hasNull = ids.includes(null)
+ return this.prisma[this.ancestryType].findMany({
+ where: {
+ OR: [
+ { ancestorId: { in: validIds } },
+ { ancestorId: hasNull ? null : undefined },
+ ]
+ },
+ });
+ }
+
+ getDescendants(ids: string[]) {
+ if (!ids || ids.length === 0) return [];
+ const validIds = ids.filter(id => id != null);
+ const hasNull = ids.includes(null)
+ return this.prisma[this.ancestryType].findMany({
+ where: {
+ OR: [
+ { ancestorId: { in: validIds } },
+ { ancestorId: hasNull ? null : undefined },
+ ]
+ },
+ });
+ }
+
+ async getDescendantIds(ids: string | string[], includeOriginalIds: boolean = false): Promise {
+ // 将单个 ID 转换为数组
+ const idArray = Array.isArray(ids) ? ids : [ids];
+
+ const res = await this.getDescendants(idArray);
+ const descendantSet = new Set(res?.map((item) => item.descendantId) || []);
+
+ if (includeOriginalIds) {
+ idArray.forEach(id => descendantSet.add(id));
+ }
+
+ return Array.from(descendantSet).filter(Boolean) as string[];
+ }
+
+ async getAncestorIds(ids: string | string[], includeOriginalIds: boolean = false): Promise {
+ // 将单个 ID 转换为数组
+ const idArray = Array.isArray(ids) ? ids : [ids];
+
+ const res = await this.getDescendants(idArray);
+ const ancestorSet = new Set();
+
+ // 按深度排序并添加祖先ID
+ res
+ ?.sort((a, b) => b.relDepth - a.relDepth)
+ ?.forEach((item) => ancestorSet.add(item.ancestorId));
+
+ // 根据参数决定是否添加原始ID
+ if (includeOriginalIds) {
+ idArray.forEach((id) => ancestorSet.add(id));
+ }
+
+ return Array.from(ancestorSet).filter(Boolean) as string[];
+ }
+ async updateOrder(args: UpdateOrderArgs) {
+ const { id, overId } = args;
+
+ return this.prisma.$transaction(async (transaction) => {
+ // 查找当前节点和目标节点
+ const currentObject = await transaction[this.objectType].findUnique({
+ where: { id },
+ select: { id: true, parentId: true, order: true }
+ });
+
+ const targetObject = await transaction[this.objectType].findUnique({
+ where: { id: overId },
+ select: { id: true, parentId: true, order: true }
+ });
+
+ // 验证节点
+ if (!currentObject || !targetObject) {
+ throw new Error('Invalid object or target object');
+ }
+
+ // 查找父节点
+ const parentObject = currentObject.parentId
+ ? await transaction[this.objectType].findUnique({
+ where: { id: currentObject.parentId },
+ select: { id: true, order: true }
+ })
+ : null;
+
+ // 确保在同一父节点下移动
+ if (currentObject.parentId !== targetObject.parentId) {
+ throw new Error('Cannot move between different parent nodes');
+ }
+
+ // 查找同层级的所有节点,按 order 排序
+ const siblingNodes = await transaction[this.objectType].findMany({
+ where: {
+ parentId: targetObject.parentId
+ },
+ select: { id: true, order: true },
+ orderBy: { order: 'asc' }
+ });
+
+ // 找到目标节点和当前节点在兄弟节点中的索引
+ const targetIndex = siblingNodes.findIndex(node => node.id === targetObject.id);
+ const currentIndex = siblingNodes.findIndex(node => node.id === currentObject.id);
+
+ // 移除当前节点
+ siblingNodes.splice(currentIndex, 1);
+
+ // 在目标位置插入当前节点
+ const insertIndex = currentIndex > targetIndex ? targetIndex + 1 : targetIndex;
+ siblingNodes.splice(insertIndex, 0, currentObject);
+
+
+ // 重新分配 order
+ const newOrders = this.redistributeOrder(siblingNodes, parentObject?.order || 0);
+
+ // 批量更新节点的 order
+ const updatePromises = newOrders.map((nodeOrder, index) =>
+ transaction[this.objectType].update({
+ where: { id: siblingNodes[index].id },
+ data: { order: nodeOrder }
+ })
+ );
+
+ await Promise.all(updatePromises);
+
+ // 返回更新后的当前节点
+ return transaction[this.objectType].findUnique({
+ where: { id: currentObject.id }
+ });
+ });
+ }
+
+ // 重新分配 order 的方法
+ private redistributeOrder(nodes: Array<{ id: string, order: number }>, parentOrder: number): number[] {
+ const MIN_CHILD_ORDER = parentOrder + this.ORDER_INTERVAL; // 子节点 order 必须大于父节点
+ const newOrders: number[] = [];
+
+ nodes.forEach((_, index) => {
+ // 使用等差数列分配 order,确保大于父节点
+ const nodeOrder = MIN_CHILD_ORDER + (index + 1) * this.ORDER_INTERVAL;
+ newOrders.push(nodeOrder);
+ });
+
+ return newOrders;
+ }
+
+
+
+}
\ No newline at end of file
diff --git a/apps/server/src/models/base/base.type.ts b/apps/server/src/models/base/base.type.ts
new file mode 100644
index 0000000..8a98d1d
--- /dev/null
+++ b/apps/server/src/models/base/base.type.ts
@@ -0,0 +1,44 @@
+import { db, Prisma, PrismaClient } from "@nicestack/common";
+
+export type Operations =
+ | 'aggregate'
+ | 'count'
+ | 'create'
+ | 'createMany'
+ | 'delete'
+ | 'deleteMany'
+ | 'findFirst'
+ | 'findMany'
+ | 'findUnique'
+ | 'update'
+ | 'updateMany'
+ | 'upsert';
+export type DelegateFuncs = { [K in Operations]: (args: any) => Promise }
+export type DelegateArgs = {
+ [K in keyof T]: T[K] extends (args: infer A) => Promise ? A : never;
+};
+
+export type DelegateReturnTypes = {
+ [K in keyof T]: T[K] extends (args: any) => Promise ? R : never;
+};
+
+export type WhereArgs = T extends { where: infer W } ? W : never;
+export type SelectArgs = T extends { select: infer S } ? S : never;
+export type DataArgs = T extends { data: infer D } ? D : never;
+export type IncludeArgs = T extends { include: infer I } ? I : never;
+export type OrderByArgs = T extends { orderBy: infer O } ? O : never;
+export type UpdateOrderArgs = {
+ id: string
+ overId: string
+}
+export interface FindManyWithCursorType {
+ cursor?: string;
+ limit?: number;
+ where?: WhereArgs['findUnique']>;
+ select?: SelectArgs['findUnique']>;
+ orderBy?: OrderByArgs['findMany']>
+}
+export type TransactionType = Omit<
+ PrismaClient,
+ '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends'
+>;
\ No newline at end of file
diff --git a/apps/server/src/models/base/errorMap.prisma.ts b/apps/server/src/models/base/errorMap.prisma.ts
new file mode 100644
index 0000000..ab234ce
--- /dev/null
+++ b/apps/server/src/models/base/errorMap.prisma.ts
@@ -0,0 +1,198 @@
+import {
+ BadRequestException,
+ NotFoundException,
+ ConflictException,
+ InternalServerErrorException,
+ } from '@nestjs/common';
+
+ export const PrismaErrorCode = Object.freeze({
+ P2000: 'P2000',
+ P2001: 'P2001',
+ P2002: 'P2002',
+ P2003: 'P2003',
+ P2006: 'P2006',
+ P2007: 'P2007',
+ P2008: 'P2008',
+ P2009: 'P2009',
+ P2010: 'P2010',
+ P2011: 'P2011',
+ P2012: 'P2012',
+ P2014: 'P2014',
+ P2015: 'P2015',
+ P2016: 'P2016',
+ P2017: 'P2017',
+ P2018: 'P2018',
+ P2019: 'P2019',
+ P2021: 'P2021',
+ P2023: 'P2023',
+ P2025: 'P2025',
+ P2031: 'P2031',
+ P2033: 'P2033',
+ P2034: 'P2034',
+ P2037: 'P2037',
+ P1000: 'P1000',
+ P1001: 'P1001',
+ P1002: 'P1002',
+ P1015: 'P1015',
+ P1017: 'P1017',
+ });
+
+ export type PrismaErrorCode = keyof typeof PrismaErrorCode;
+
+
+ interface PrismaErrorMeta {
+ target?: string;
+ model?: string;
+ relationName?: string;
+ details?: string;
+ }
+
+ export type operationT = 'create' | 'read' | 'update' | 'delete';
+
+ export type PrismaErrorHandler = (
+ operation: operationT,
+ meta?: PrismaErrorMeta,
+ ) => Error;
+
+ export const ERROR_MAP: Record = {
+ P2000: (_operation, meta) => new BadRequestException(
+ `The provided value for ${meta?.target || 'a field'} is too long. Please use a shorter value.`
+ ),
+
+ P2001: (operation, meta) => new NotFoundException(
+ `The ${meta?.model || 'record'} you are trying to ${operation} could not be found.`
+ ),
+
+ P2002: (operation, meta) => {
+ const field = meta?.target || 'unique field';
+ switch (operation) {
+ case 'create':
+ return new ConflictException(
+ `A record with the same ${field} already exists. Please use a different value.`
+ );
+ case 'update':
+ return new ConflictException(
+ `The new value for ${field} conflicts with an existing record.`
+ );
+ default:
+ return new ConflictException(
+ `Unique constraint violation on ${field}.`
+ );
+ }
+ },
+
+ P2003: (operation) => new BadRequestException(
+ `Foreign key constraint failed. Unable to ${operation} the record because related data is invalid or missing.`
+ ),
+
+ P2006: (_operation, meta) => new BadRequestException(
+ `The provided value for ${meta?.target || 'a field'} is invalid. Please correct it.`
+ ),
+
+ P2007: (operation) => new InternalServerErrorException(
+ `Data validation error during ${operation}. Please ensure all inputs are valid and try again.`
+ ),
+
+ P2008: (operation) => new InternalServerErrorException(
+ `Failed to query the database during ${operation}. Please try again later.`
+ ),
+
+ P2009: (operation) => new InternalServerErrorException(
+ `Invalid data fetched during ${operation}. Check query structure.`
+ ),
+
+ P2010: () => new InternalServerErrorException(
+ `Invalid raw query. Ensure your query is correct and try again.`
+ ),
+
+ P2011: (_operation, meta) => new BadRequestException(
+ `The required field ${meta?.target || 'a field'} is missing. Please provide it to continue.`
+ ),
+
+ P2012: (operation, meta) => new BadRequestException(
+ `Missing required relation ${meta?.relationName || ''}. Ensure all related data exists before ${operation}.`
+ ),
+
+ P2014: (operation) => {
+ switch (operation) {
+ case 'create':
+ return new BadRequestException(
+ `Cannot create record because the referenced data does not exist. Ensure related data exists.`
+ );
+ case 'delete':
+ return new BadRequestException(
+ `Unable to delete record because it is linked to other data. Update or delete dependent records first.`
+ );
+ default:
+ return new BadRequestException(`Foreign key constraint error.`);
+ }
+ },
+
+ P2015: () => new InternalServerErrorException(
+ `A record with the required ID was expected but not found. Please retry.`
+ ),
+
+ P2016: (operation) => new InternalServerErrorException(
+ `Query ${operation} failed because the record could not be fetched. Ensure the query is correct.`
+ ),
+
+ P2017: (operation) => new InternalServerErrorException(
+ `Connected records were not found for ${operation}. Check related data.`
+ ),
+
+ P2018: () => new InternalServerErrorException(
+ `The required connection could not be established. Please check relationships.`
+ ),
+
+ P2019: (_operation, meta) => new InternalServerErrorException(
+ `Invalid input for ${meta?.details || 'a field'}. Please ensure data conforms to expectations.`
+ ),
+
+ P2021: (_operation, meta) => new InternalServerErrorException(
+ `The ${meta?.model || 'model'} was not found in the database.`
+ ),
+
+ P2025: (operation, meta) => new NotFoundException(
+ `The ${meta?.model || 'record'} you are trying to ${operation} does not exist. It may have been deleted.`
+ ),
+
+ P2031: () => new InternalServerErrorException(
+ `Invalid Prisma Client initialization error. Please check configuration.`
+ ),
+
+ P2033: (operation) => new InternalServerErrorException(
+ `Insufficient database write permissions for ${operation}.`
+ ),
+
+ P2034: (operation) => new InternalServerErrorException(
+ `Database read-only transaction failed during ${operation}.`
+ ),
+
+ P2037: (operation) => new InternalServerErrorException(
+ `Unsupported combinations of input types for ${operation}. Please correct the query or input.`
+ ),
+
+ P1000: () => new InternalServerErrorException(
+ `Database authentication failed. Verify your credentials and try again.`
+ ),
+
+ P1001: () => new InternalServerErrorException(
+ `The database server could not be reached. Please check its availability.`
+ ),
+
+ P1002: () => new InternalServerErrorException(
+ `Connection to the database timed out. Verify network connectivity and server availability.`
+ ),
+
+ P1015: (operation) => new InternalServerErrorException(
+ `Migration failed. Unable to complete ${operation}. Check migration history or database state.`
+ ),
+
+ P1017: () => new InternalServerErrorException(
+ `Database connection failed. Ensure the database is online and credentials are correct.`
+ ),
+ P2023: function (operation: operationT, meta?: PrismaErrorMeta): Error {
+ throw new Error('Function not implemented.');
+ }
+ };
+
\ No newline at end of file
diff --git a/apps/server/src/models/base/row-cache.service.ts b/apps/server/src/models/base/row-cache.service.ts
new file mode 100644
index 0000000..8430974
--- /dev/null
+++ b/apps/server/src/models/base/row-cache.service.ts
@@ -0,0 +1,183 @@
+import { UserProfile, RowModelRequest, RowRequestSchema } from "@nicestack/common";
+import { RowModelService } from "./row-model.service";
+import { isFieldCondition, LogicalCondition, SQLBuilder } from "./sql-builder";
+import EventBus from "@server/utils/event-bus";
+import supejson from "superjson-cjs"
+import { deleteByPattern } from "@server/utils/redis/utils";
+import { redis } from "@server/utils/redis/redis.service";
+import { z } from "zod";
+export class RowCacheService extends RowModelService {
+ constructor(tableName: string, private enableCache: boolean = true) {
+ super(tableName)
+ if (this.enableCache) {
+ EventBus.on("dataChanged", async ({ type, data }) => {
+ if (type === tableName) {
+ const dataArray = Array.isArray(data) ? data : [data];
+ for (const item of dataArray) {
+ try {
+ if (item.id) {
+ this.invalidateRowCacheById(item.id)
+ }
+ if (item.parentId) {
+ this.invalidateRowCacheById(item.parentId)
+ }
+ } catch (err) {
+ console.error(`Error deleting cache for type ${tableName}:`, err);
+ }
+ }
+ }
+ });
+ }
+ }
+ protected getRowCacheKey(id: string) {
+ return `row-data-${id}`;
+ }
+ private async invalidateRowCacheById(id: string) {
+ if (!this.enableCache) return;
+ const pattern = this.getRowCacheKey(id);
+ await deleteByPattern(pattern);
+ }
+ createJoinSql(request?: RowModelRequest): string[] {
+ return []
+ }
+ protected async getRowRelation(args: { data: any, staff?: UserProfile }) {
+ return args.data;
+ }
+ protected async setResPermissions(
+ data: any,
+ staff?: UserProfile,
+ ) {
+ return data
+ }
+ protected async getRowDto(
+ data: any,
+ staff?: UserProfile,
+ ): Promise {
+ // 如果没有id,直接返回原数据
+ if (!data?.id) return data;
+ // 如果未启用缓存,直接处理并返回数据
+ if (!this.enableCache) {
+ return this.processDataWithPermissions(data, staff);
+ }
+ const key = this.getRowCacheKey(data.id);
+ try {
+ // 尝试从缓存获取数据
+ const cachedData = await this.getCachedData(key, staff);
+ // 如果缓存命中,直接返回
+ if (cachedData) return cachedData;
+ // 处理数据并缓存
+ const processedData = await this.processDataWithPermissions(data, staff);
+ await redis.set(key, supejson.stringify(processedData));
+ return processedData;
+ } catch (err) {
+ this.logger.error('Error in getRowDto:', err);
+ throw err;
+ }
+ }
+
+ private async getCachedData(
+ key: string,
+ staff?: UserProfile
+ ): Promise {
+ const cachedDataStr = await redis.get(key);
+ if (!cachedDataStr) return null;
+ const cachedData = supejson.parse(cachedDataStr) as any;
+ if (!cachedData?.id) return null;
+ return staff
+ ? this.setResPermissions(cachedData, staff)
+ : cachedData;
+ }
+
+ private async processDataWithPermissions(
+ data: any,
+ staff?: UserProfile
+ ): Promise {
+ // 处理权限
+ const permData = staff
+ ? await this.setResPermissions(data, staff)
+ : data;
+ // 获取关联数据
+ return this.getRowRelation({ data: permData, staff });
+ }
+
+ protected createGetRowsFilters(
+ request: z.infer,
+ staff?: UserProfile,
+ ) {
+ const condition = super.createGetRowsFilters(request);
+ if (isFieldCondition(condition)) return {};
+ const baseCondition: LogicalCondition[] = [
+ {
+ field: `${this.tableName}.deleted_at`,
+ op: 'blank',
+ type: 'date',
+ },
+ ];
+ condition.AND = [...baseCondition, ...condition.AND];
+ return condition;
+ }
+ createUnGroupingRowSelect(request?: RowModelRequest): string[] {
+ return [
+ `${this.tableName}.id AS id`,
+ SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`),
+ ];
+ }
+ protected createGroupingRowSelect(
+ request: RowModelRequest,
+ wrapperSql: boolean,
+ ): string[] {
+ const colsToSelect = super.createGroupingRowSelect(request, wrapperSql);
+ return colsToSelect.concat([
+ SQLBuilder.rowNumber(`${this.tableName}.id`, `${this.tableName}.id`),
+ ]);
+ }
+ protected async getRowsSqlWrapper(
+ sql: string,
+ request?: RowModelRequest,
+ staff?: UserProfile,
+ ): Promise {
+ const groupingSql = SQLBuilder.join([
+ SQLBuilder.select([
+ ...this.createGroupingRowSelect(request, true),
+ `${this.tableName}.id AS id`,
+ ]),
+ SQLBuilder.from(this.tableName),
+ SQLBuilder.join(this.createJoinSql(request)),
+ SQLBuilder.where(this.createGetRowsFilters(request, staff)),
+ ]);
+ const { rowGroupCols, valueCols, groupKeys } = request;
+ if (this.isDoingGroup(request)) {
+ const rowGroupCol = rowGroupCols[groupKeys.length];
+ const groupByField = rowGroupCol?.field?.replace('.', '_');
+ return SQLBuilder.join([
+ SQLBuilder.select([
+ groupByField,
+ ...super.createAggSqlForWrapper(request),
+ 'COUNT(id) AS child_count',
+ ]),
+ SQLBuilder.from(`(${groupingSql})`),
+ SQLBuilder.where({
+ field: 'row_num',
+ value: '1',
+ op: 'equals',
+ }),
+ SQLBuilder.groupBy([groupByField]),
+ SQLBuilder.orderBy(
+ this.getOrderByColumns(request).map((item) => item.replace('.', '_')),
+ ),
+ this.getLimitSql(request),
+ ]);
+ } else
+ return SQLBuilder.join([
+ SQLBuilder.select(['*']),
+ SQLBuilder.from(`(${sql})`),
+ SQLBuilder.where({
+ field: 'row_num',
+ value: '1',
+ op: 'equals',
+ }),
+ this.getLimitSql(request),
+ ]);
+ // return super.getRowsSqlWrapper(sql, request)
+ }
+}
\ No newline at end of file
diff --git a/apps/server/src/models/base/row-model.service.ts b/apps/server/src/models/base/row-model.service.ts
new file mode 100644
index 0000000..1148209
--- /dev/null
+++ b/apps/server/src/models/base/row-model.service.ts
@@ -0,0 +1,240 @@
+import { Logger } from "@nestjs/common";
+import { UserProfile, db, getUniqueItems, ObjectWithId, Prisma, RowModelRequest } from "@nicestack/common";
+import { LogicalCondition, OperatorType, SQLBuilder } from './sql-builder';
+export interface GetRowOptions {
+ id?: string;
+ ids?: string[];
+ extraCondition?: LogicalCondition;
+ staff?: UserProfile;
+}
+export abstract class RowModelService {
+ private keywords: Set = new Set([
+ 'SELECT', 'FROM', 'WHERE', 'ORDER', 'BY', 'GROUP', 'JOIN', 'AND', 'OR'
+ // 添加更多需要引号的关键词
+ ]);
+ protected logger = new Logger(this.tableName);
+ protected constructor(protected tableName: string) { }
+ protected async getRowDto(row: ObjectWithId, staff?: UserProfile): Promise {
+ return row;
+ }
+ protected async getRowsSqlWrapper(sql: string, request?: RowModelRequest, staff?: UserProfile) {
+ return SQLBuilder.join([sql, this.getLimitSql(request)])
+ }
+ protected getLimitSql(request: RowModelRequest) {
+ return SQLBuilder.limit(request.endRow - request.startRow, request.startRow)
+ }
+ abstract createJoinSql(request?: RowModelRequest): string[];
+ async getRows(request: RowModelRequest, staff?: UserProfile): Promise<{ rowCount: number, rowData: any[] }> {
+ try {
+ // this.logger.debug('request', request)
+ let SQL = SQLBuilder.join([
+ SQLBuilder.select(this.getRowSelectCols(request)),
+ SQLBuilder.from(this.tableName),
+ SQLBuilder.join(this.createJoinSql(request)),
+ SQLBuilder.where(this.createGetRowsFilters(request, staff)),
+ SQLBuilder.groupBy(this.getGroupByColumns(request)),
+ SQLBuilder.orderBy(this.getOrderByColumns(request)),
+ ]);
+ SQL = await this.getRowsSqlWrapper(SQL, request, staff)
+
+ this.logger.debug('getrows', SQL)
+
+ const results: any[] = await db.$queryRawUnsafe(SQL) || [];
+
+ let rowDataDto = await Promise.all(results.map(row => this.getRowDto(row, staff)))
+
+ // if (this.getGroupByColumns(request).length === 0)
+ // rowDataDto = getUniqueItems(rowDataDto, "id")
+ // this.logger.debug('result', results.length, this.getRowCount(request, rowDataDto))
+ return { rowCount: this.getRowCount(request, rowDataDto) || 0, rowData: rowDataDto };
+ } catch (error: any) {
+ this.logger.error('Error executing getRows:', error);
+ // throw new Error(`Failed to get rows: ${error.message}`);
+ }
+ }
+ getRowCount(request: RowModelRequest, results: any[]) {
+ if (results === null || results === undefined || results.length === 0) {
+ return null;
+ }
+ const currentLastRow = request.startRow + results.length;
+ return currentLastRow <= request.endRow ? currentLastRow : -1;
+ }
+
+ async getRowById(options: GetRowOptions): Promise {
+ const { id, extraCondition = {
+ field: `${this.tableName}.deleted_at`,
+ op: "blank",
+ type: "date"
+ }, staff } = options;
+ return this.getSingleRow({ AND: [this.createGetByIdFilter(id!), extraCondition] }, staff);
+ }
+
+ async getRowByIds(options: GetRowOptions): Promise {
+ const { ids, extraCondition = {
+ field: `${this.tableName}.deleted_at`,
+ op: "blank",
+ type: "date"
+ }, staff } = options;
+ return this.getMultipleRows({ AND: [this.createGetByIdsFilter(ids!), extraCondition] }, staff);
+ }
+
+ protected createGetRowsFilters(request: RowModelRequest, staff?: UserProfile): LogicalCondition {
+ let groupConditions = []
+ if (this.isDoingTreeGroup(request)) {
+ groupConditions = [
+ {
+ field: 'parent_id',
+ op: "equals" as OperatorType,
+ value: request.groupKeys[request.groupKeys.length - 1]
+ }
+ ]
+ } else {
+ groupConditions = request.groupKeys.map((key, index) => ({
+ field: request.rowGroupCols[index].field,
+ op: "equals" as OperatorType,
+ value: key
+ }))
+ }
+
+ const condition: LogicalCondition = {
+ AND: [...groupConditions, ...this.buildFilterConditions(request.filterModel)]
+ }
+
+ return condition;
+ }
+ private buildFilterConditions(filterModel: any): LogicalCondition[] {
+ return filterModel
+ ? Object.entries(filterModel)?.map(([key, item]) => SQLBuilder.createFilterSql(key === 'ag-Grid-AutoColumn' ? 'name' : key, item))
+ : [];
+ }
+
+ getRowSelectCols(request: RowModelRequest): string[] {
+ return this.isDoingGroup(request)
+ ? this.createGroupingRowSelect(request)
+ : this.createUnGroupingRowSelect(request);
+ }
+ protected createUnGroupingRowSelect(request?: RowModelRequest): string[] {
+ return ['*'];
+ }
+ protected createAggSqlForWrapper(request: RowModelRequest) {
+ const { rowGroupCols, valueCols, groupKeys } = request;
+ return valueCols.map(valueCol =>
+ `${valueCol.aggFunc}(${valueCol.field.replace('.', '_')}) AS ${valueCol.field.split('.').join('_')}`
+ );
+ }
+ protected createGroupingRowSelect(request: RowModelRequest, wrapperSql: boolean = false): string[] {
+ const { rowGroupCols, valueCols, groupKeys } = request;
+ const colsToSelect = [];
+
+ const rowGroupCol = rowGroupCols[groupKeys.length];
+ if (rowGroupCol) {
+ colsToSelect.push(`${rowGroupCol.field} AS ${rowGroupCol.field.replace('.', '_')}`);
+ }
+ colsToSelect.push(...valueCols.map(valueCol =>
+ `${wrapperSql ? "" : valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`
+ ));
+
+ return colsToSelect;
+ }
+
+ getGroupByColumns(request: RowModelRequest): string[] {
+ return this.isDoingGroup(request)
+ ? [request.rowGroupCols[request.groupKeys.length]?.field]
+ : [];
+ }
+
+
+ getOrderByColumns(request: RowModelRequest): string[] {
+ const { sortModel, rowGroupCols, groupKeys } = request;
+ const grouping = this.isDoingGroup(request);
+ const sortParts: string[] = [];
+
+ if (sortModel) {
+ const groupColIds = rowGroupCols.map(groupCol => groupCol.id).slice(0, groupKeys.length + 1);
+ sortModel.forEach(item => {
+ if (!grouping || (groupColIds.indexOf(item.colId) >= 0 && rowGroupCols[groupKeys.length].field === item.colId)) {
+ const colId = this.keywords.has(item.colId.toUpperCase()) ? `"${item.colId}"` : item.colId;
+ sortParts.push(`${colId} ${item.sort}`);
+ }
+ });
+ }
+
+ return sortParts;
+ }
+ isDoingGroup(requset: RowModelRequest): boolean {
+ return requset.rowGroupCols.length > requset.groupKeys.length;
+ }
+ isDoingTreeGroup(requset: RowModelRequest): boolean {
+ return requset.rowGroupCols.length === 0 && requset.groupKeys.length > 0;
+ }
+ private async getSingleRow(condition: LogicalCondition, staff?: UserProfile): Promise {
+ const results = await this.getRowsWithFilters(condition, staff)
+ return results[0]
+ }
+ private async getMultipleRows(condition: LogicalCondition, staff?: UserProfile): Promise {
+ return this.getRowsWithFilters(condition, staff);
+ }
+
+ private async getRowsWithFilters(condition: LogicalCondition, staff?: UserProfile): Promise {
+ try {
+ let SQL = SQLBuilder.join([
+ SQLBuilder.select(this.createUnGroupingRowSelect()),
+ SQLBuilder.from(this.tableName),
+ SQLBuilder.join(this.createJoinSql()),
+ SQLBuilder.where(condition)
+ ]);
+
+ // this.logger.debug(SQL)
+ const results: any[] = await db.$queryRawUnsafe(SQL);
+
+ let rowDataDto = await Promise.all(results.map(item => this.getRowDto(item, staff)));
+
+ // rowDataDto = getUniqueItems(rowDataDto, "id")
+ return rowDataDto
+ } catch (error) {
+ this.logger.error('Error executing query:', error);
+ throw error;
+ }
+ }
+
+ async getAggValues(request: RowModelRequest) {
+ try {
+ const SQL = SQLBuilder.join([
+ SQLBuilder.select(this.buildAggSelect(request.valueCols)),
+ SQLBuilder.from(this.tableName),
+ SQLBuilder.join(this.createJoinSql(request)),
+ SQLBuilder.where(this.createGetRowsFilters(request)),
+ SQLBuilder.groupBy(this.buildAggGroupBy())
+ ]);
+ const result = await db.$queryRawUnsafe(SQL);
+ return result[0];
+ } catch (error) {
+ this.logger.error('Error executing query:', error);
+ throw error;
+ }
+ }
+ protected buildAggGroupBy(): string[] {
+ return [];
+ }
+ protected buildAggSelect(valueCols: any[]): string[] {
+ return valueCols.map(valueCol =>
+ `${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`
+ );
+ }
+
+ private createGetByIdFilter(id: string): LogicalCondition {
+ return {
+ field: `${this.tableName}.id`,
+ value: id,
+ op: "equals"
+ }
+ }
+ private createGetByIdsFilter(ids: string[]): LogicalCondition {
+ return {
+ field: `${this.tableName}.id`,
+ value: ids,
+ op: "in"
+ };
+ }
+}
+
diff --git a/apps/server/src/models/base/sql-builder.ts b/apps/server/src/models/base/sql-builder.ts
new file mode 100644
index 0000000..67cd331
--- /dev/null
+++ b/apps/server/src/models/base/sql-builder.ts
@@ -0,0 +1,138 @@
+export interface FieldCondition {
+ field: string;
+ op: OperatorType
+ type?: "text" | "number" | "date";
+ value?: any;
+ valueTo?: any;
+};
+export type OperatorType = 'equals' | 'notEqual' | 'contains' | 'startsWith' | 'endsWith' | 'blank' | 'notBlank' | 'greaterThan' | 'lessThanOrEqual' | 'inRange' | 'lessThan' | 'greaterThan' | 'in';
+export type LogicalCondition = FieldCondition | {
+ AND?: LogicalCondition[];
+ OR?: LogicalCondition[];
+};
+
+export function isFieldCondition(condition: LogicalCondition): condition is FieldCondition {
+ return (condition as FieldCondition).field !== undefined;
+}
+function buildCondition(condition: FieldCondition): string {
+ const { field, op, value, type = "text", valueTo } = condition;
+ switch (op) {
+ case 'equals':
+ return `${field} = '${value}'`;
+ case 'notEqual':
+ return `${field} != '${value}'`;
+ case 'contains':
+ return `${field} LIKE '%${value}%'`;
+ case 'startsWith':
+ return `${field} LIKE '${value}%'`;
+ case 'endsWith':
+ return `${field} LIKE '%${value}'`;
+ case 'blank':
+ if (type !== "date")
+ return `(${field} IS NULL OR ${field} = '')`;
+ else
+ return `${field} IS NULL`;
+ case 'notBlank':
+ if (type !== 'date')
+ return `${field} IS NOT NULL AND ${field} != ''`;
+ else
+ return `${field} IS NOT NULL`;
+ case 'greaterThan':
+ return `${field} > '${value}'`;
+ case 'lessThanOrEqual':
+ return `${field} <= '${value}'`;
+ case 'lessThan':
+ return `${field} < '${value}'`;
+ case 'greaterThan':
+ return `${field} > '${value}'`;
+ case 'inRange':
+ return `${field} >= '${value}' AND ${field} <= '${valueTo}'`;
+ case 'in':
+ if (!value || (Array.isArray(value) && value.length === 0)) {
+ // Return a condition that is always false if value is empty or an empty array
+ return '1 = 0';
+ }
+ return `${field} IN (${(value as any[]).map(val => `'${val}'`).join(', ')})`;
+ default:
+ return 'true'; // Default return for unmatched conditions
+ }
+}
+function buildLogicalCondition(logicalCondition: LogicalCondition): string {
+ if (isFieldCondition(logicalCondition)) {
+ return buildCondition(logicalCondition);
+ }
+ const parts: string[] = [];
+ if (logicalCondition.AND && logicalCondition.AND.length > 0) {
+ const andParts = logicalCondition.AND
+ .map(c => buildLogicalCondition(c))
+ .filter(part => part !== ''); // Filter out empty conditions
+ if (andParts.length > 0) {
+ parts.push(`(${andParts.join(' AND ')})`);
+ }
+ }
+ // Process OR conditions
+ if (logicalCondition.OR && logicalCondition.OR.length > 0) {
+ const orParts = logicalCondition.OR
+ .map(c => buildLogicalCondition(c))
+ .filter(part => part !== ''); // Filter out empty conditions
+ if (orParts.length > 0) {
+ parts.push(`(${orParts.join(' OR ')})`);
+ }
+ }
+ // Join AND and OR parts with an 'AND' if both are present
+ return parts.length > 1 ? parts.join(' AND ') : parts[0] || '';
+}
+
+export class SQLBuilder {
+ static select(fields: string[], distinctField?: string): string {
+ const distinctClause = distinctField ? `DISTINCT ON (${distinctField}) ` : "";
+ return `SELECT ${distinctClause}${fields.join(", ")}`;
+ }
+ static rowNumber(orderBy: string, partitionBy: string | null = null, alias: string = 'row_num'): string {
+ if (!orderBy) {
+ throw new Error("orderBy 参数不能为空");
+ }
+
+ let partitionClause = '';
+ if (partitionBy) {
+ partitionClause = `PARTITION BY ${partitionBy} `;
+ }
+
+ return `ROW_NUMBER() OVER (${partitionClause}ORDER BY ${orderBy}) AS ${alias}`;
+ }
+ static from(tableName: string): string {
+ return `FROM ${tableName}`;
+ }
+
+ static where(conditions: LogicalCondition): string {
+ const whereClause = buildLogicalCondition(conditions);
+ return whereClause ? `WHERE ${whereClause}` : "";
+ }
+
+ static groupBy(columns: string[]): string {
+ return columns.length ? `GROUP BY ${columns.join(", ")}` : "";
+ }
+
+ static orderBy(columns: string[]): string {
+ return columns.length ? `ORDER BY ${columns.join(", ")}` : "";
+ }
+
+ static limit(pageSize: number, offset: number = 0): string {
+ return `LIMIT ${pageSize + 1} OFFSET ${offset}`;
+ }
+
+ static join(clauses: string[]): string {
+ return clauses.filter(Boolean).join(' ');
+ }
+ static createFilterSql(key: string, item: any): LogicalCondition {
+ const conditionFuncs: Record LogicalCondition> = {
+ text: (item) => ({ value: item.filter, op: item.type, field: key }),
+ number: (item) => ({ value: item.filter, op: item.type, field: key }),
+ date: (item) => ({ value: item.dateFrom, valueTo: item.dateTo, op: item.type, field: key }),
+ set: (item) => ({ value: item.values, op: "in", field: key })
+ }
+ return conditionFuncs[item.filterType](item)
+
+ }
+}
+
diff --git a/apps/server/src/models/base/test.sql b/apps/server/src/models/base/test.sql
new file mode 100644
index 0000000..df6923a
--- /dev/null
+++ b/apps/server/src/models/base/test.sql
@@ -0,0 +1,30 @@
+SELECT *
+FROM (
+ SELECT staff.id AS id,
+ ROW_NUMBER() OVER (
+ PARTITION BY staff.id
+ ORDER BY staff.id
+ ) AS row_num,
+ staff.id AS id,
+ staff.username AS username,
+ staff.showname AS showname,
+ staff.avatar AS avatar,
+ staff.officer_id AS officer_id,
+ staff.phone_number AS phone_number,
+ staff.order AS order,
+ staff.enabled AS enabled,
+ dept.name AS dept_name,
+ domain.name AS domain_name
+ FROM staff
+ LEFT JOIN department dept ON staff.dept_id = dept.id
+ LEFT JOIN department domain ON staff.domain_id = domain.id
+ WHERE (
+ staff.deleted_at IS NULL
+ AND enabled = 'Thu Dec 26 2024 11:55:47 GMT+0800 (中国标准时间)'
+ AND staff.domain_id = '784c7583-c7f3-4179-873d-f8195ccf2acf'
+ AND staff.deleted_at IS NULL
+ )
+ ORDER BY "order" asc
+ )
+WHERE row_num = '1'
+LIMIT 31 OFFSET 0
\ No newline at end of file
diff --git a/apps/server/src/models/department/department.controller.ts b/apps/server/src/models/department/department.controller.ts
new file mode 100755
index 0000000..2f49d4e
--- /dev/null
+++ b/apps/server/src/models/department/department.controller.ts
@@ -0,0 +1,87 @@
+import { Controller, Get, Query, UseGuards } from '@nestjs/common';
+
+import { DepartmentService } from './department.service';
+import { AuthGuard } from '@server/auth/auth.guard';
+import { db } from '@nicestack/common';
+
+@Controller('dept')
+export class DepartmentController {
+ constructor(private readonly deptService: DepartmentService) { }
+ @UseGuards(AuthGuard)
+ @Get('get-detail')
+ async getDepartmentDetails(@Query('dept-id') deptId: string) {
+ try {
+ const result = await this.deptService.findById(deptId);
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+ @UseGuards(AuthGuard)
+ @Get('get-all-child-dept-ids')
+ async getAllChildDeptIds(@Query('dept-id') deptId: string) {
+ try {
+ const result = await this.deptService.getDescendantIds([deptId]);
+
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+ @UseGuards(AuthGuard)
+ @Get('get-all-parent-dept-ids')
+ async getAllParentDeptIds(@Query('dept-id') deptId: string) {
+ try {
+ const result = await this.deptService.getAncestorIds([deptId]);
+
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+ @UseGuards(AuthGuard)
+ @Get('find-by-name-in-dom')
+ async findInDomain(
+ @Query('domain-id') domainId?: string,
+ @Query('name') name?: string,
+ ) {
+ try {
+ const result = await this.deptService.findInDomain(domainId, name);
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+}
diff --git a/apps/server/src/models/department/department.module.ts b/apps/server/src/models/department/department.module.ts
index 99cc160..59e0f8e 100755
--- a/apps/server/src/models/department/department.module.ts
+++ b/apps/server/src/models/department/department.module.ts
@@ -2,9 +2,12 @@ import { Module } from '@nestjs/common';
import { DepartmentService } from './department.service';
import { DepartmentRouter } from './department.router';
import { TrpcService } from '@server/trpc/trpc.service';
+import { DepartmentController } from './department.controller';
+import { DepartmentRowService } from './department.row.service';
@Module({
- providers: [DepartmentService, DepartmentRouter, TrpcService],
- exports: [DepartmentService, DepartmentRouter]
+ providers: [DepartmentService, DepartmentRouter, DepartmentRowService, TrpcService],
+ exports: [DepartmentService, DepartmentRouter],
+ controllers: [DepartmentController],
})
export class DepartmentModule { }
diff --git a/apps/server/src/models/department/department.router.ts b/apps/server/src/models/department/department.router.ts
index 2bced54..a0890b4 100755
--- a/apps/server/src/models/department/department.router.ts
+++ b/apps/server/src/models/department/department.router.ts
@@ -1,66 +1,71 @@
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import { DepartmentService } from './department.service'; // assuming it's in the same directory
-import { DepartmentSchema, z } from '@nicestack/common';
+import { DepartmentMethodSchema, Prisma, UpdateOrderSchema } from '@nicestack/common';
+import { z, ZodType } from 'zod';
+import { DepartmentRowService } from './department.row.service';
+const DepartmentCreateArgsSchema: ZodType = z.any()
+const DepartmentUpdateArgsSchema: ZodType = z.any()
+const DepartmentFindFirstArgsSchema: ZodType = z.any()
+const DepartmentFindManyArgsSchema: ZodType = z.any()
@Injectable()
export class DepartmentRouter {
constructor(
private readonly trpc: TrpcService,
- private readonly departmentService: DepartmentService, // inject DepartmentService
- ) {}
+ private readonly departmentService: DepartmentService, // 注入 DepartmentService
+ private readonly departmentRowService: DepartmentRowService
+ ) { }
router = this.trpc.router({
+ // 创建部门
create: this.trpc.protectProcedure
- .input(DepartmentSchema.create) // expect input according to the schema
+ .input(DepartmentCreateArgsSchema) // 根据 schema 期望输入
.mutation(async ({ input }) => {
return this.departmentService.create(input);
}),
+ // 更新部门
update: this.trpc.protectProcedure
- .input(DepartmentSchema.update) // expect input according to the schema
+ .input(DepartmentUpdateArgsSchema) // 根据 schema 期望输入
.mutation(async ({ input }) => {
return this.departmentService.update(input);
}),
-
- delete: this.trpc.protectProcedure
- .input(DepartmentSchema.delete) // expect input according to the schema
+ // 根据 ID 列表软删除部门
+ softDeleteByIds: this.trpc.protectProcedure
+ .input(z.object({ ids: z.array(z.string()) })) // 根据 schema 期望输入
.mutation(async ({ input }) => {
- return this.departmentService.delete(input);
- }),
- getDepartmentDetails: this.trpc.procedure
- .input(z.object({ deptId: z.string() })) // expect an object with deptId
- .query(async ({ input }) => {
- return this.departmentService.getDepartmentDetails(input.deptId);
- }),
- getAllChildDeptIds: this.trpc.procedure
- .input(z.object({ deptId: z.string() })) // expect an object with deptId
- .query(async ({ input }) => {
- return this.departmentService.getAllChildDeptIds(input.deptId);
- }),
- getAllParentDeptIds: this.trpc.procedure
- .input(z.object({ deptId: z.string() })) // expect an object with deptId
- .query(async ({ input }) => {
- return this.departmentService.getAllParentDeptIds(input.deptId);
- }),
- getChildren: this.trpc.procedure
- .input(z.object({ parentId: z.string().nullish() }))
- .query(async ({ input }) => {
- return this.departmentService.getChildren(input.parentId);
- }),
- getDomainDepartments: this.trpc.procedure
- .input(z.object({ query: z.string().nullish() }))
- .query(async ({ input }) => {
- const { query } = input;
- return this.departmentService.getDomainDepartments(query);
+ return this.departmentService.softDeleteByIds(input.ids);
}),
+ // 更新部门顺序
+ updateOrder: this.trpc.protectProcedure.input(UpdateOrderSchema).mutation(async ({ input }) => {
+ return this.departmentService.updateOrder(input)
+ }),
+ // 查询多个部门
findMany: this.trpc.procedure
- .input(DepartmentSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
+ .input(DepartmentFindManyArgsSchema) // 假设 StaffMethodSchema.findMany 是根据关键字查找员工的 Zod schema
.query(async ({ input }) => {
return await this.departmentService.findMany(input);
}),
- paginate: this.trpc.procedure
- .input(DepartmentSchema.paginate) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
+ // 查询第一个部门
+ findFirst: this.trpc.procedure
+ .input(DepartmentFindFirstArgsSchema) // 假设 StaffMethodSchema.findMany 是根据关键字查找员工的 Zod schema
.query(async ({ input }) => {
- return await this.departmentService.paginate(input);
+ return await this.departmentService.findFirst(input);
+ }),
+ // 获取子部门的简单树结构
+ getChildSimpleTree: this.trpc.procedure
+ .input(DepartmentMethodSchema.getSimpleTree).query(async ({ input }) => {
+ return await this.departmentService.getChildSimpleTree(input)
+ }),
+ // 获取父部门的简单树结构
+ getParentSimpleTree: this.trpc.procedure
+ .input(DepartmentMethodSchema.getSimpleTree).query(async ({ input }) => {
+ return await this.departmentService.getParentSimpleTree(input)
+ }),
+ // 获取部门行数据
+ getRows: this.trpc.protectProcedure
+ .input(DepartmentMethodSchema.getRows)
+ .query(async ({ input, ctx }) => {
+ return await this.departmentRowService.getRows(input, ctx.staff);
}),
});
}
diff --git a/apps/server/src/models/department/department.row.service.ts b/apps/server/src/models/department/department.row.service.ts
new file mode 100644
index 0000000..c4056c8
--- /dev/null
+++ b/apps/server/src/models/department/department.row.service.ts
@@ -0,0 +1,91 @@
+/**
+ * 部门行服务类,继承自 RowCacheService,用于处理部门相关的数据行操作。
+ * 该类提供了生成 SQL 查询字段、构建查询过滤器等功能。
+ */
+import { Injectable } from '@nestjs/common';
+import {
+ db,
+ DepartmentMethodSchema,
+ ObjectType,
+ UserProfile,
+} from '@nicestack/common';
+import { date, z } from 'zod';
+import { RowCacheService } from '../base/row-cache.service';
+import { isFieldCondition } from '../base/sql-builder';
+
+@Injectable()
+export class DepartmentRowService extends RowCacheService {
+ /**
+ * 构造函数,初始化 DepartmentRowService。
+ * 调用父类构造函数,传入部门对象类型并禁用缓存。
+ */
+ constructor() {
+ super(ObjectType.DEPARTMENT, false);
+ }
+
+ /**
+ * 生成未分组行的 SQL 查询字段列表,包括部门特定的字段。
+ * @param requset - 请求对象,符合 DepartmentMethodSchema.getRows 的 schema。
+ * @returns 返回包含 SQL 查询字段的数组。
+ */
+ createUnGroupingRowSelect(
+ requset: z.infer,
+ ): string[] {
+ // 调用父类方法生成基础查询字段,并拼接部门特定的字段
+ const result = super.createUnGroupingRowSelect(requset).concat([
+ `${this.tableName}.name AS name`, // 部门名称
+ `${this.tableName}.is_domain AS is_domain`, // 是否为域
+ `${this.tableName}.order AS order`, // 排序
+ `${this.tableName}.has_children AS has_children`, // 是否有子部门
+ `${this.tableName}.parent_id AS parent_id` // 父部门 ID
+ ]);
+ return result;
+ }
+
+ /**
+ * 根据请求和用户信息构建 getRows 查询的过滤器。
+ * @param request - 请求对象,符合 DepartmentMethodSchema.getRows 的 schema。
+ * @param staff - 发起请求的用户信息。
+ * @returns 返回用于过滤行的条件对象。
+ */
+ protected createGetRowsFilters(
+ request: z.infer,
+ staff: UserProfile,
+ ) {
+ // 调用父类方法生成基础过滤条件
+ const condition = super.createGetRowsFilters(request);
+ const { parentId, includeDeleted = false } = request;
+
+ // 如果条件已经是字段条件,则跳过后续处理
+ if (isFieldCondition(condition)) {
+ return;
+ }
+
+ // 如果请求中没有分组键,则添加父部门 ID 过滤条件
+ if (request.groupKeys.length === 0) {
+ if (parentId) {
+ condition.AND.push({
+ field: `${this.tableName}.parent_id`,
+ value: parentId,
+ op: 'equals', // 等于操作符
+ });
+ } else if (parentId === null) {
+ condition.AND.push({
+ field: `${this.tableName}.parent_id`,
+ op: "blank", // 空白操作符
+ });
+ }
+ }
+
+ // 如果 includeDeleted 为 false,则排除已删除的行
+ if (!includeDeleted) {
+ condition.AND.push({
+ field: `${this.tableName}.deleted_at`,
+ type: 'date',
+ op: 'blank', // 空白操作符
+ });
+ }
+
+ return condition;
+ }
+}
diff --git a/apps/server/src/models/department/department.service.ts b/apps/server/src/models/department/department.service.ts
index bcff592..3d23d09 100755
--- a/apps/server/src/models/department/department.service.ts
+++ b/apps/server/src/models/department/department.service.ts
@@ -1,33 +1,90 @@
import { Injectable } from '@nestjs/common';
-import { db, z, DepartmentSchema } from '@nicestack/common';
-
+import {
+ db,
+ DepartmentMethodSchema,
+ DeptAncestry,
+ getUniqueItems,
+ ObjectType,
+ Prisma,
+} from '@nicestack/common';
+import { BaseTreeService } from '../base/base.tree.service';
+import { z } from 'zod';
+import { mapToDeptSimpleTree, getStaffsByDeptIds } from './utils';
+import EventBus, { CrudOperation } from '@server/utils/event-bus';
@Injectable()
-export class DepartmentService {
- /**
- * 获取某单位的所有子单位(平铺结构)。
- * @param deptId - 单位的唯一标识符。
- * @returns 包含所有子单位的数组,如果未传递deptId则返回undefined。
- */
- async getFlatChildDepts(deptId: string) {
- if (!deptId) return undefined;
-
- return await db.deptAncestry.findMany({
- where: { ancestorId: deptId },
+export class DepartmentService extends BaseTreeService {
+ constructor() {
+ super(db, ObjectType.DEPARTMENT, 'deptAncestry', true);
+ }
+ async getDescendantIdsInDomain(
+ ancestorId: string,
+ includeAncestor = true,
+ ): Promise {
+ // 如果没有提供部门ID,返回空数组
+ if (!ancestorId) return [];
+ // 获取祖先部门信息
+ const ancestorDepartment = await db.department.findUnique({
+ where: { id: ancestorId },
});
+
+ // 如果未找到部门,返回空数组
+ if (!ancestorDepartment) return [];
+
+ // 查询同域下以指定部门为祖先的部门血缘关系
+ const departmentAncestries = await db.deptAncestry.findMany({
+ where: {
+ ancestorId: ancestorId,
+ descendant: {
+ domainId: ancestorDepartment.domainId,
+ },
+ },
+ });
+ // 提取子部门ID列表
+ let descendantDepartmentIds = departmentAncestries.map(
+ (ancestry) => ancestry.descendantId,
+ );
+ // 根据参数决定是否包含祖先部门ID
+ if (includeAncestor && ancestorId) {
+ descendantDepartmentIds.push(ancestorId);
+ }
+ return descendantDepartmentIds;
+ }
+ async getDescendantDomainIds(
+ ancestorDomainId: string,
+ includeAncestorDomain = true,
+ ): Promise {
+ if (!ancestorDomainId) return [];
+ // 查询所有以指定域ID为祖先的域的血缘关系
+ const domainAncestries = await db.deptAncestry.findMany({
+ where: {
+ ancestorId: ancestorDomainId,
+ descendant: {
+ isDomain: true,
+ },
+ },
+ });
+
+ // 提取子域的ID列表
+ let descendantDomainIds = domainAncestries.map(
+ (ancestry) => ancestry.descendantId,
+ );
+ // 根据参数决定是否包含祖先域ID
+ if (includeAncestorDomain && ancestorDomainId) {
+ descendantDomainIds.push(ancestorDomainId);
+ }
+
+ return descendantDomainIds;
}
/**
* 获取指定DOM下的对应name的单位
- * @param domId
+ * @param domainId
* @param name
* @returns
- *
- *
*/
-
- async findByNameInDom(domId: string, name: string) {
+ async findInDomain(domainId: string, name: string) {
const subDepts = await db.deptAncestry.findMany({
where: {
- ancestorId: domId,
+ ancestorId: domainId,
},
include: {
descendant: true,
@@ -35,251 +92,38 @@ export class DepartmentService {
});
const dept = subDepts.find((item) => item.descendant.name === name);
- return dept?.descendant;
- }
- /**
- * 获取某单位的所有父单位(平铺结构)。
- * @param deptId - 单位的唯一标识符。
- * @returns 包含所有父单位的数组,如果未传递deptId则返回undefined。
- */
- async getFlatParentDepts(deptId: string) {
- if (!deptId) return undefined;
-
- return await db.deptAncestry.findMany({
- where: { descendantId: deptId },
- });
+ return dept.descendant;
}
- /**
- * 获取某单位的所有子单位ID。
- * @param deptId - 单位的唯一标识符。
- * @returns 包含所有子单位ID的数组。
- */
- async getAllChildDeptIds(deptId: string) {
- const res = await this.getFlatChildDepts(deptId);
- return res?.map((dept) => dept.descendantId) || [];
+ private async setDomainId(parentId: string) {
+ const parent = await this.findUnique({ where: { id: parentId } });
+ return parent.isDomain ? parentId : parent.domainId;
}
- /**
- * 获取某单位的所有父单位ID。
- * @param deptId - 单位的唯一标识符。
- * @returns 包含所有父单位ID的数组。
- */
- async getAllParentDeptIds(deptId: string) {
- const res = await this.getFlatParentDepts(deptId);
- return res?.map((dept) => dept.ancestorId) || [];
- }
-
- /**
- * 获取单位及其直接子单位的详细信息。
- * @param deptId - 要获取的单位的唯一标识符。
- * @returns 包含单位详细信息的对象,包括其子单位和员工信息。
- */
- async getDepartmentDetails(deptId: string) {
- const department = await db.department.findUnique({
- where: { id: deptId },
- include: { children: true, deptStaffs: true },
- });
-
- const childrenData = await db.deptAncestry.findMany({
- where: { ancestorId: deptId, relDepth: 1 },
- include: { descendant: { include: { children: true } } },
- });
-
- const children = childrenData.map(({ descendant }) => ({
- id: descendant.id,
- name: descendant.name,
- order: descendant.order,
- parentId: descendant.parentId,
- hasChildren: Boolean(descendant.children?.length),
- childrenCount: descendant.children?.length || 0,
- }));
-
- return {
- id: department?.id,
- name: department?.name,
- order: department?.order,
- parentId: department?.parentId,
- children,
- staffs: department?.deptStaffs,
- hasChildren: !!children.length,
- };
- }
-
- /**
- * 获取某单位的所有直接子单位。
- * @param parentId - 父单位的唯一标识符,如果未传递则获取顶级单位。
- * @returns 包含所有直接子单位信息的数组。
- */
- async getChildren(parentId?: string) {
- const departments = await db.department.findMany({
- where: { parentId: parentId ?? null },
- include: { children: true, deptStaffs: true },
- });
-
- return departments.map((dept) => ({
- ...dept,
- hasChildren: dept.children.length > 0,
- staffs: dept.deptStaffs,
- }));
- }
- async paginate(data: z.infer) {
- const { page, pageSize, ids } = data;
- const [items, totalCount] = await Promise.all([
- db.department.findMany({
- skip: (page - 1) * pageSize,
- take: pageSize,
- where: {
- deletedAt: null,
- OR: [{ id: { in: ids } }],
- },
- include: { deptStaffs: true, parent: true },
- orderBy: { order: 'asc' },
- }),
- db.department.count({
- where: {
- deletedAt: null,
- OR: [{ id: { in: ids } }],
- },
- }),
- ]);
-
- return { items, totalCount };
- }
- async findMany(data: z.infer) {
- const { keyword = '', ids } = data;
-
- const departments = await db.department.findMany({
- where: {
- deletedAt: null,
- OR: [{ name: { contains: keyword! } }, ids ? { id: { in: ids } } : {}],
- },
- include: { deptStaffs: true },
- orderBy: { order: 'asc' },
- take: 20,
- });
-
- return departments.map((dept) => ({
- ...dept,
- staffs: dept.deptStaffs,
- }));
- }
-
- /**
- * 获取所有域内单位,根据查询条件筛选结果。
- * @param query - 可选的查询条件,用于模糊匹配单位名称。
- * @returns 包含符合条件的域内单位信息的数组。
- */
- async getDomainDepartments(query?: string) {
- return await db.department.findMany({
- where: { isDomain: true, name: { contains: query } },
- take: 10,
- });
- }
-
- async getDeptIdsByStaffIds(ids: string[]) {
- const staffs = await db.staff.findMany({
- where: { id: { in: ids } },
- });
-
- return staffs.map((staff) => staff.deptId);
- }
-
- /**
- * 创建一个新的单位并管理DeptAncestry关系。
- * @param data - 用于创建新单位的数据。
- * @returns 新创建的单位对象。
- */
- async create(data: z.infer) {
- let newOrder = 0;
-
- // 确定新单位的顺序
- const siblingDepartments = await db.department.findMany({
- where: { parentId: data.parentId ?? null },
- orderBy: { order: 'desc' },
- take: 1,
- });
-
- if (siblingDepartments.length > 0) {
- newOrder = siblingDepartments[0].order + 1;
+ async create(args: Prisma.DepartmentCreateArgs) {
+ if (args.data.parentId) {
+ args.data.domainId = await this.setDomainId(args.data.parentId);
}
-
- // 根据计算的顺序创建新单位
- const newDepartment = await db.department.create({
- data: { ...data, order: newOrder },
+ const result = await super.create(args);
+ EventBus.emit('dataChanged', {
+ type: this.objectType,
+ operation: CrudOperation.CREATED,
+ data: result,
});
-
- // 如果存在parentId,则更新DeptAncestry关系
- if (data.parentId) {
- const parentAncestries = await db.deptAncestry.findMany({
- where: { descendantId: data.parentId },
- orderBy: { relDepth: 'asc' },
- });
-
- // 为新单位创建新的祖先记录
- const newAncestries = parentAncestries.map((ancestry) => ({
- ancestorId: ancestry.ancestorId,
- descendantId: newDepartment.id,
- relDepth: ancestry.relDepth + 1,
- }));
-
- newAncestries.push({
- ancestorId: data.parentId,
- descendantId: newDepartment.id,
- relDepth: 1,
- });
-
- await db.deptAncestry.createMany({ data: newAncestries });
- }
-
- return newDepartment;
+ return result;
}
- /**
- * 更新现有单位,并在parentId更改时管理DeptAncestry关系。
- * @param data - 用于更新现有单位的数据。
- * @returns 更新后的单位对象。
- */
- async update(data: z.infer) {
- return await db.$transaction(async (transaction) => {
- const currentDepartment = await transaction.department.findUnique({
- where: { id: data.id },
- });
- if (!currentDepartment) throw new Error('Department not found');
-
- const updatedDepartment = await transaction.department.update({
- where: { id: data.id },
- data: data,
- });
-
- if (data.parentId !== currentDepartment.parentId) {
- await transaction.deptAncestry.deleteMany({
- where: { descendantId: data.id },
- });
-
- if (data.parentId) {
- const parentAncestries = await transaction.deptAncestry.findMany({
- where: { descendantId: data.parentId },
- });
-
- const newAncestries = parentAncestries.map((ancestry) => ({
- ancestorId: ancestry.ancestorId,
- descendantId: data.id,
- relDepth: ancestry.relDepth + 1,
- }));
-
- newAncestries.push({
- ancestorId: data.parentId,
- descendantId: data.id,
- relDepth: 1,
- });
-
- await transaction.deptAncestry.createMany({ data: newAncestries });
- }
- }
-
- return updatedDepartment;
+ async update(args: Prisma.DepartmentUpdateArgs) {
+ if (args.data.parentId) {
+ args.data.domainId = await this.setDomainId(args.data.parentId as string);
+ }
+ const result = await super.update(args);
+ EventBus.emit('dataChanged', {
+ type: this.objectType,
+ operation: CrudOperation.UPDATED,
+ data: result,
});
+ return result;
}
/**
@@ -287,78 +131,208 @@ export class DepartmentService {
* @param data - 用于删除现有单位的数据。
* @returns 删除的单位对象。
*/
- async delete(data: z.infer) {
- const deletedDepartment = await db.department.update({
- where: { id: data.id },
- data: { deletedAt: new Date() },
+ async softDeleteByIds(ids: string[]) {
+ const descendantIds = await this.getDescendantIds(ids, true);
+ const result = await super.softDeleteByIds(descendantIds);
+ EventBus.emit('dataChanged', {
+ type: this.objectType,
+ operation: CrudOperation.DELETED,
+ data: result,
});
+ return result;
+ }
- await db.deptAncestry.deleteMany({
- where: { OR: [{ ancestorId: data.id }, { descendantId: data.id }] },
- });
- return deletedDepartment;
- }
- async getStaffsByDeptIds(ids: string[]) {
- const depts = await db.department.findMany({
- where: { id: { in: ids } },
- include: { deptStaffs: true },
- });
- return depts.flatMap((dept) => dept.deptStaffs);
- }
/**
* 获取指定部门及其所有子部门的员工。
* @param deptIds - 要获取员工ID的部门ID数组。
* @returns 包含所有员工ID的数组。
*/
- async getAllStaffsByDepts(deptIds: string[]) {
- const allDeptIds = new Set(deptIds);
- for (const deptId of deptIds) {
- const childDeptIds = await this.getAllChildDeptIds(deptId);
- childDeptIds.forEach((id) => allDeptIds.add(id));
- }
- return await this.getStaffsByDeptIds(Array.from(allDeptIds));
+ async getStaffsInDepts(deptIds: string[]) {
+ const allDeptIds = await this.getDescendantIds(deptIds, true);
+ return await getStaffsByDeptIds(Array.from(allDeptIds));
}
-
- /**
- * 根据部门名称和域ID获取部门ID。
- *
- * @param {string} name - 部门名称。
- * @param {string} domainId - 域标识符。
- * @returns {Promise} - 如果找到则返回部门ID,否则返回null。
- */
- async getDeptIdByName(name: string, domainId: string): Promise {
- const dept = await db.department.findFirst({
- where: {
- name,
- ancestors: {
- some: {
- ancestorId: domainId
- }
- }
- }
- });
- return dept ? dept.id : null;
+ async getStaffIdsInDepts(deptIds: string[]) {
+ const result = await this.getStaffsInDepts(deptIds);
+ return result.map((s) => s.id);
}
-
/**
* 根据部门名称列表和域ID获取多个部门的ID。
- *
- * @param {string[]} names - 部门名称列表。
- * @param {string} domainId - 域标识符。
- * @returns {Promise>} - 一个从部门名称到对应ID或null的记录。
+ *
+ * @param {string[]} names - 部门名称列表
+ * @param {string} domainId - 域ID
+ * @returns {Promise>} - 返回一个对象,键为部门名称,值为部门ID或null
*/
- async getDeptIdsByNames(names: string[], domainId: string): Promise> {
+ async getDeptIdsByNames(
+ names: string[],
+ domainId: string,
+ ): Promise> {
+ // 使用 Prisma 的 findMany 方法批量查询部门信息,优化性能
+ const depts = await db.department.findMany({
+ where: {
+ // 查询条件:部门名称在给定的名称列表中
+ name: { in: names },
+ // 查询条件:部门在指定的域下(通过ancestors关系查询)
+ ancestors: {
+ some: {
+ ancestorId: domainId,
+ },
+ },
+ },
+ // 选择查询的字段:只查询部门的id和name字段
+ select: {
+ id: true,
+ name: true,
+ },
+ });
+
+ // 创建一个Map对象,将部门名称映射到部门ID
+ const deptMap = new Map(depts.map((dept) => [dept.name, dept.id]));
+
+ // 初始化结果对象,用于存储最终的结果
const result: Record = {};
- // 遍历每个部门名称并获取对应的部门ID
+ // 遍历传入的部门名称列表
for (const name of names) {
- // 使用之前定义的函数根据名称获取部门ID
- const deptId = await this.getDeptIdByName(name, domainId);
- result[name] = deptId;
+ // 从Map中获取部门ID,如果不存在则返回null
+ result[name] = deptMap.get(name) || null;
}
+ // 返回最终的结果对象
return result;
}
+ async getChildSimpleTree(
+ data: z.infer,
+ ) {
+ const { domain, deptIds, rootId } = data;
+ // 提取非空 deptIds
+ const validDeptIds = deptIds?.filter((id) => id !== null) ?? [];
+ const hasNullDeptId = deptIds?.includes(null) ?? false;
+ const [childrenData, selfData] = await Promise.all([
+ db.deptAncestry.findMany({
+ where: {
+ ...(deptIds && {
+ OR: [
+ ...(validDeptIds.length
+ ? [{ ancestorId: { in: validDeptIds } }]
+ : []),
+ ...(hasNullDeptId ? [{ ancestorId: null }] : []),
+ ],
+ }),
+ ancestorId: rootId,
+ relDepth: 1,
+ descendant: { isDomain: domain },
+ },
+ include: {
+ descendant: { include: { children: true, deptStaffs: true } },
+ },
+ orderBy: { descendant: { order: 'asc' } },
+ }),
+ deptIds
+ ? db.department.findMany({
+ where: {
+ ...(deptIds && {
+ OR: [
+ ...(validDeptIds.length
+ ? [{ id: { in: validDeptIds } }]
+ : []),
+ ],
+ }),
+ isDomain: domain,
+ },
+ include: { children: true },
+ orderBy: { order: 'asc' },
+ })
+ : [],
+ ]);
+ const children = childrenData
+ .map(({ descendant }) => descendant)
+ .filter(Boolean)
+ .map(mapToDeptSimpleTree);
+ const selfItems = selfData.map(mapToDeptSimpleTree);
+ return getUniqueItems([...children, ...selfItems], 'id');
+ }
+ /**
+ * 获取父级部门简单树结构的异步方法
+ *
+ * @param data - 包含部门ID、域和根ID的输入参数
+ * @returns 返回唯一的部门简单树结构数组
+ *
+ * 方法整体设计思路:
+ * 1. 并行查询父级部门ancestry和自身部门数据
+ * 2. 查询根节点的直接子节点
+ * 3. 通过自定义过滤函数筛选超出根节点层级的祖先节点
+ * 4. 将查询结果映射为简单树结构
+ * 5. 去重并返回最终结果
+ */
+ async getParentSimpleTree(
+ data: z.infer,
+ ) {
+ // 解构输入参数
+ const { deptIds, domain, rootId } = data;
+ // 并行查询父级部门ancestry和自身部门数据
+ // 使用Promise.all提高查询效率,减少等待时间
+ const [parentData, selfData] = await Promise.all([
+ // 查询指定部门的所有祖先节点,包含子节点和父节点信息
+ db.deptAncestry.findMany({
+ where: {
+ descendantId: { in: deptIds }, // 查询条件:descendant在给定的部门ID列表中
+ ancestor: { isDomain: domain }, // 限定域
+ },
+ include: {
+ ancestor: {
+ include: {
+ children: true, // 包含子节点信息
+ parent: true, // 包含父节点信息
+ },
+ },
+ },
+ orderBy: { ancestor: { order: 'asc' } }, // 按祖先节点顺序升序排序
+ }),
+
+ // 查询自身部门数据
+ db.department.findMany({
+ where: { id: { in: deptIds }, isDomain: domain },
+ include: { children: true }, // 包含子节点信息
+ orderBy: { order: 'asc' }, // 按顺序升序排序
+ }),
+ ]);
+
+ // 查询根节点的直接子节点
+ const rootChildren = await db.deptAncestry.findMany({
+ where: {
+ ancestorId: rootId, // 祖先ID为根ID
+ descendant: { isDomain: domain }, // 限定域
+ },
+ });
+
+ /**
+ * 判断祖先节点是否超出根节点层级的函数
+ *
+ * @param ancestor - 祖先节点
+ * @returns 是否超出根节点层级
+ */
+ const isDirectDescendantOfRoot = (ancestor: DeptAncestry): boolean => {
+ return (
+ rootChildren.findIndex(
+ (child) => child.descendantId === ancestor.ancestorId,
+ ) !== -1
+ );
+ };
+
+ // 处理父级节点:过滤并映射为简单树结构
+ const parents = parentData
+ .map(({ ancestor }) => ancestor) // 提取祖先节点
+ .filter(
+ (ancestor) => ancestor && isDirectDescendantOfRoot(ancestor as any),
+ ) // 过滤有效且超出根节点层级的节点
+ .map(mapToDeptSimpleTree); // 映射为简单树结构
+
+ // 处理自身节点:映射为简单树结构
+ const selfItems = selfData.map(mapToDeptSimpleTree);
+
+ // 合并并去重父级和自身节点,返回唯一项
+ return getUniqueItems([...parents, ...selfItems], 'id');
+ }
}
diff --git a/apps/server/src/models/department/utils.ts b/apps/server/src/models/department/utils.ts
new file mode 100644
index 0000000..4295fff
--- /dev/null
+++ b/apps/server/src/models/department/utils.ts
@@ -0,0 +1,68 @@
+import { UserProfile, db, DeptSimpleTreeNode, TreeDataNode } from "@nicestack/common";
+
+/**
+ * 将部门数据映射为DeptSimpleTreeNode结构
+ * @param department 部门数据对象
+ * @returns 返回格式化后的DeptSimpleTreeNode对象
+ * 数据结构说明:
+ * - id: 部门唯一标识
+ * - key: 部门唯一标识,通常用于React中的key属性
+ * - value: 部门唯一标识,通常用于表单值
+ * - title: 部门名称
+ * - order: 部门排序值
+ * - pId: 父部门ID,用于构建树形结构
+ * - isLeaf: 是否为叶子节点,即该部门是否没有子部门
+ * - hasStaff: 该部门是否包含员工
+ */
+export function mapToDeptSimpleTree(department: any): DeptSimpleTreeNode {
+ return {
+ id: department.id,
+ key: department.id,
+ value: department.id,
+ title: department.name,
+ order: department.order,
+ pId: department.parentId,
+ isLeaf: !Boolean(department.children?.length),
+ hasStaff: department?.deptStaffs?.length > 0
+ };
+}
+
+/**
+ * 根据部门ID列表获取相关员工信息
+ * @param ids 部门ID列表
+ * @returns 返回与指定部门相关的员工ID列表
+ * 算法说明:
+ * - 使用数据库查询方法findMany,根据部门ID列表查询相关部门的员工信息
+ * - 使用flatMap将查询结果扁平化,提取所有员工ID
+ */
+export async function getStaffsByDeptIds(ids: string[]) {
+ const depts = await db.department.findMany({
+ where: { id: { in: ids } },
+ select: {
+ deptStaffs: {
+ select: { id: true }
+ }
+ },
+ });
+ return depts.flatMap((dept) => dept.deptStaffs);
+}
+
+/**
+ * 提取唯一的员工ID列表
+ * @param params 参数对象,包含部门ID列表、员工ID列表和员工信息
+ * @returns 返回唯一的员工ID列表
+ * 算法说明:
+ * - 根据部门ID列表获取相关员工ID
+ * - 将部门员工ID与传入的员工ID列表合并,并使用Set去重
+ * - 如果传入了员工信息,则从结果中移除该员工的ID
+ * - 最终返回去重后的员工ID列表
+ */
+export async function extractUniqueStaffIds(params: { deptIds?: string[], staffIds?: string[], staff?: UserProfile }): Promise {
+ const { deptIds, staff, staffIds } = params;
+ const deptStaffs = await getStaffsByDeptIds(deptIds);
+ const result = new Set(deptStaffs.map(item => item.id).concat(staffIds));
+ if (staff) {
+ result.delete(staff.id);
+ }
+ return Array.from(result);
+}
\ No newline at end of file
diff --git a/apps/server/src/models/message/message.controller.ts b/apps/server/src/models/message/message.controller.ts
new file mode 100755
index 0000000..e86a115
--- /dev/null
+++ b/apps/server/src/models/message/message.controller.ts
@@ -0,0 +1,125 @@
+import { Controller, Get, Query, UseGuards } from '@nestjs/common';
+
+import { MessageService } from './message.service';
+import { AuthGuard } from '@server/auth/auth.guard';
+import { db, VisitType } from '@nicestack/common';
+
+@Controller('message')
+export class MessageController {
+ constructor(private readonly messageService: MessageService) { }
+ @UseGuards(AuthGuard)
+ @Get('find-last-one')
+ async findLastOne(@Query('staff-id') staffId: string) {
+ try {
+ const result = await db.message.findFirst({
+ where: {
+ OR: [
+ {
+ receivers: {
+ some: {
+ id: staffId,
+ },
+ },
+ },
+ ],
+ },
+ orderBy: { createdAt: 'desc' },
+ select: {
+ title: true,
+ content: true,
+ url: true
+ },
+ });
+
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+ @UseGuards(AuthGuard)
+ @Get('find-unreaded')
+ async findUnreaded(@Query('staff-id') staffId: string) {
+ try {
+ const result = await db.message.findMany({
+ where: {
+ visits: {
+ none: {
+ id: staffId,
+ visitType: VisitType.READED
+ },
+ },
+ receivers: {
+ some: {
+ id: staffId,
+ },
+ },
+ },
+ orderBy: { createdAt: 'desc' },
+ select: {
+ title: true,
+ content: true,
+ url: true,
+ },
+ });
+
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+ @UseGuards(AuthGuard)
+ @Get('count-unreaded')
+ async countUnreaded(@Query('staff-id') staffId: string) {
+ try {
+ const result = await db.message.findMany({
+ where: {
+ visits: {
+ none: {
+ id: staffId,
+ visitType: VisitType.READED
+ },
+ },
+ receivers: {
+ some: {
+ id: staffId,
+ },
+ },
+ },
+ orderBy: { createdAt: 'desc' },
+ select: {
+ title: true,
+ content: true,
+ url: true,
+ },
+ });
+
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+}
diff --git a/apps/server/src/models/message/message.module.ts b/apps/server/src/models/message/message.module.ts
new file mode 100644
index 0000000..ce83a6e
--- /dev/null
+++ b/apps/server/src/models/message/message.module.ts
@@ -0,0 +1,14 @@
+import { Module } from '@nestjs/common';
+import { MessageService } from './message.service';
+import { MessageRouter } from './message.router';
+import { TrpcService } from '@server/trpc/trpc.service';
+import { DepartmentModule } from '../department/department.module';
+import { MessageController } from './message.controller';
+
+@Module({
+ imports: [DepartmentModule],
+ providers: [MessageService, MessageRouter, TrpcService],
+ exports: [MessageService, MessageRouter],
+ controllers: [MessageController],
+})
+export class MessageModule { }
diff --git a/apps/server/src/models/message/message.router.ts b/apps/server/src/models/message/message.router.ts
new file mode 100755
index 0000000..1cb12a1
--- /dev/null
+++ b/apps/server/src/models/message/message.router.ts
@@ -0,0 +1,39 @@
+import { Injectable } from '@nestjs/common';
+import { TrpcService } from '@server/trpc/trpc.service';
+import { MessageService } from './message.service';
+import { ChangedRows, Prisma } from '@nicestack/common';
+import { z, ZodType } from 'zod';
+const MessageUncheckedCreateInputSchema: ZodType = z.any()
+const MessageWhereInputSchema: ZodType = z.any()
+const MessageSelectSchema: ZodType = z.any()
+@Injectable()
+export class MessageRouter {
+ constructor(
+ private readonly trpc: TrpcService,
+ private readonly messageService: MessageService,
+ ) { }
+ router = this.trpc.router({
+ create: this.trpc.procedure
+ .input(MessageUncheckedCreateInputSchema)
+ .mutation(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.messageService.create({ data: input }, { staff });
+ }),
+ findManyWithCursor: this.trpc.protectProcedure
+ .input(z.object({
+ cursor: z.any().nullish(),
+ take: z.number().nullish(),
+ where: MessageWhereInputSchema.nullish(),
+ select: MessageSelectSchema.nullish()
+ }))
+ .query(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.messageService.findManyWithCursor(input, staff);
+ }),
+ getUnreadCount: this.trpc.protectProcedure
+ .query(async ({ ctx }) => {
+ const { staff } = ctx;
+ return await this.messageService.getUnreadCount(staff);
+ })
+ })
+}
diff --git a/apps/server/src/models/message/message.service.ts b/apps/server/src/models/message/message.service.ts
new file mode 100644
index 0000000..68b640a
--- /dev/null
+++ b/apps/server/src/models/message/message.service.ts
@@ -0,0 +1,57 @@
+import { Injectable } from '@nestjs/common';
+import { UserProfile, db, Prisma, VisitType, ObjectType } from '@nicestack/common';
+import { BaseService } from '../base/base.service';
+import EventBus, { CrudOperation } from '@server/utils/event-bus';
+import { setMessageRelation } from './utils';
+@Injectable()
+export class MessageService extends BaseService {
+ constructor() {
+ super(db, ObjectType.MESSAGE);
+ }
+ async create(args: Prisma.MessageCreateArgs, params?: { tx?: Prisma.MessageDelegate, staff?: UserProfile }) {
+ args.data.senderId = params?.staff?.id;
+ args.include = {
+ receivers: {
+ select: { id: true, registerToken: true, username: true }
+ }
+ }
+ const result = await super.create(args);
+ EventBus.emit("dataChanged", {
+ type: ObjectType.MESSAGE,
+ operation: CrudOperation.CREATED,
+ data: result
+ })
+ return result
+ }
+ async findManyWithCursor(
+ args: Prisma.MessageFindManyArgs,
+ staff?: UserProfile,
+ ) {
+
+ return this.wrapResult(super.findManyWithCursor(args), async (result) => {
+ let { items } = result;
+ await Promise.all(
+ items.map(async (item) => {
+ await setMessageRelation(item, staff);
+ }),
+ );
+
+ return { ...result, items };
+ });
+ }
+ async getUnreadCount(staff?: UserProfile) {
+ const count = await db.message.count({
+ where: {
+ receivers: { some: { id: staff?.id } },
+ visits: {
+ none: {
+ visitorId: staff?.id,
+ visitType: VisitType.READED
+ }
+ }
+ }
+ })
+
+ return count
+ }
+}
diff --git a/apps/server/src/models/message/utils.ts b/apps/server/src/models/message/utils.ts
new file mode 100644
index 0000000..8ebbf40
--- /dev/null
+++ b/apps/server/src/models/message/utils.ts
@@ -0,0 +1,20 @@
+import { Message, UserProfile, VisitType, db } from "@nicestack/common"
+export async function setMessageRelation(
+ data: Message,
+ staff?: UserProfile,
+): Promise {
+
+ const readed =
+ (await db.visit.count({
+ where: {
+ messageId: data.id,
+ visitType: VisitType.READED,
+ visitorId: staff?.id,
+ },
+ })) > 0;
+
+
+ Object.assign(data, {
+ readed
+ })
+}
\ No newline at end of file
diff --git a/apps/server/src/models/post/post.controller.ts b/apps/server/src/models/post/post.controller.ts
new file mode 100755
index 0000000..86b21fb
--- /dev/null
+++ b/apps/server/src/models/post/post.controller.ts
@@ -0,0 +1,89 @@
+import { Controller, Get, Query, UseGuards } from '@nestjs/common';
+
+import { PostService } from './post.service';
+import { AuthGuard } from '@server/auth/auth.guard';
+import { db } from '@nicestack/common';
+
+@Controller('post')
+export class PostController {
+ constructor(private readonly postService: PostService) { }
+ @UseGuards(AuthGuard)
+ @Get('find-last-one')
+ async findLastOne(@Query('trouble-id') troubleId: string) {
+ try {
+ const result = await this.postService.findFirst({
+ where: { referenceId: troubleId },
+ orderBy: { createdAt: 'desc' }
+ });
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+ @UseGuards(AuthGuard)
+ @Get('find-all')
+ async findAll(@Query('trouble-id') troubleId: string) {
+ try {
+ const result = await db.post.findMany({
+ where: {
+ OR: [{ referenceId: troubleId }],
+ },
+ orderBy: { createdAt: 'desc' },
+ select: {
+ title: true,
+ content: true,
+ attachments: true,
+ type: true,
+ author: {
+ select: {
+ id: true,
+ showname: true,
+ username: true,
+ },
+ },
+ },
+ });
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+ @UseGuards(AuthGuard)
+ @Get('count')
+ async count(@Query('trouble-id') troubleId: string) {
+ try {
+ const result = await db.post.count({
+ where: {
+ OR: [{ referenceId: troubleId }],
+ },
+ });
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+}
diff --git a/apps/server/src/models/post/post.module.ts b/apps/server/src/models/post/post.module.ts
new file mode 100755
index 0000000..836d47b
--- /dev/null
+++ b/apps/server/src/models/post/post.module.ts
@@ -0,0 +1,18 @@
+import { Module } from '@nestjs/common';
+import { TrpcService } from '@server/trpc/trpc.service';
+import { DepartmentService } from '@server/models/department/department.service';
+
+import { QueueModule } from '@server/queue/queue.module';
+import { MessageModule } from '../message/message.module';
+import { PostRouter } from './post.router';
+import { PostController } from './post.controller';
+import { PostService } from './post.service';
+import { RoleMapModule } from '../rbac/rbac.module';
+
+@Module({
+ imports: [QueueModule, RoleMapModule, MessageModule],
+ providers: [PostService, PostRouter, TrpcService, DepartmentService],
+ exports: [PostRouter, PostService],
+ controllers: [PostController],
+})
+export class PostModule {}
diff --git a/apps/server/src/models/post/post.router.ts b/apps/server/src/models/post/post.router.ts
new file mode 100755
index 0000000..9245377
--- /dev/null
+++ b/apps/server/src/models/post/post.router.ts
@@ -0,0 +1,77 @@
+import { Injectable } from '@nestjs/common';
+import { TrpcService } from '@server/trpc/trpc.service';
+import { ChangedRows, ObjectType, Prisma } from '@nicestack/common';
+import { PostService } from './post.service';
+import { z, ZodType } from 'zod';
+const PostCreateArgsSchema: ZodType = z.any();
+const PostUpdateArgsSchema: ZodType = z.any();
+const PostFindFirstArgsSchema: ZodType = z.any();
+const PostDeleteManyArgsSchema: ZodType = z.any();
+const PostWhereInputSchema: ZodType = z.any();
+const PostSelectSchema: ZodType = z.any();
+const PostUpdateInputSchema: ZodType = z.any();
+@Injectable()
+export class PostRouter {
+ constructor(
+ private readonly trpc: TrpcService,
+ private readonly postService: PostService,
+ ) {}
+ router = this.trpc.router({
+ create: this.trpc.protectProcedure
+ .input(PostCreateArgsSchema)
+ .mutation(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.postService.create(input, { staff });
+ }),
+ softDeleteByIds: this.trpc.protectProcedure
+ .input(
+ z.object({
+ ids: z.array(z.string()),
+ data: PostUpdateInputSchema.nullish(),
+ }),
+ )
+ .mutation(async ({ input }) => {
+ return await this.postService.softDeleteByIds(input.ids, input.data);
+ }),
+ restoreByIds: this.trpc.protectProcedure
+ .input(
+ z.object({
+ ids: z.array(z.string()),
+ args: PostUpdateInputSchema.nullish(),
+ }),
+ )
+ .mutation(async ({ input }) => {
+ return await this.postService.restoreByIds(input.ids, input.args);
+ }),
+ update: this.trpc.protectProcedure
+ .input(PostUpdateArgsSchema)
+ .mutation(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.postService.update(input, staff);
+ }),
+ findById: this.trpc.protectProcedure
+ .input(z.object({ id: z.string(), args: PostFindFirstArgsSchema }))
+ .query(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.postService.findById(input.id, input.args);
+ }),
+ deleteMany: this.trpc.protectProcedure
+ .input(PostDeleteManyArgsSchema)
+ .mutation(async ({ input }) => {
+ return await this.postService.deleteMany(input);
+ }),
+ findManyWithCursor: this.trpc.protectProcedure
+ .input(
+ z.object({
+ cursor: z.any().nullish(),
+ take: z.number().nullish(),
+ where: PostWhereInputSchema.nullish(),
+ select: PostSelectSchema.nullish(),
+ }),
+ )
+ .query(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.postService.findManyWithCursor(input, staff);
+ }),
+ });
+}
diff --git a/apps/server/src/models/post/post.service.ts b/apps/server/src/models/post/post.service.ts
new file mode 100755
index 0000000..50a2fe4
--- /dev/null
+++ b/apps/server/src/models/post/post.service.ts
@@ -0,0 +1,146 @@
+import { Injectable } from '@nestjs/common';
+import {
+ db,
+ Prisma,
+ UserProfile,
+ VisitType,
+ Post,
+ PostType,
+ RolePerms,
+ ResPerm,
+ ObjectType,
+} from '@nicestack/common';
+import { MessageService } from '../message/message.service';
+import { BaseService } from '../base/base.service';
+import { DepartmentService } from '../department/department.service';
+import { setPostRelation } from './utils';
+import EventBus, { CrudOperation } from '@server/utils/event-bus';
+
+@Injectable()
+export class PostService extends BaseService {
+ constructor(
+ private readonly messageService: MessageService,
+ private readonly departmentService: DepartmentService,
+ ) {
+ super(db, ObjectType.POST);
+ }
+ async create(
+ args: Prisma.PostCreateArgs,
+ params: { staff?: UserProfile; tx?: Prisma.PostDelegate },
+ ) {
+ args.data.authorId = params?.staff.id;
+ const result = await super.create(args);
+ EventBus.emit('dataChanged', {
+ type: ObjectType.POST,
+ operation: CrudOperation.CREATED,
+ data: result,
+ });
+ return result;
+ }
+ async update(args: Prisma.PostUpdateArgs, staff?: UserProfile) {
+ args.data.authorId = staff.id;
+ return super.update(args);
+ }
+ async findManyWithCursor(args: Prisma.PostFindManyArgs, staff?: UserProfile) {
+ args.where.OR = await this.preFilter(args.where.OR, staff);
+
+ // console.log(`findwithcursor_post ${JSON.stringify(args.where)}`)
+ return this.wrapResult(super.findManyWithCursor(args), async (result) => {
+ let { items } = result;
+ await Promise.all(
+ items.map(async (item) => {
+ await setPostRelation({ data: item, staff });
+ await this.setPerms(item, staff);
+ }),
+ );
+ return { ...result, items };
+ });
+ }
+
+ protected async setPerms(data: Post, staff: UserProfile) {
+ if (!staff) return;
+ const perms: ResPerm = {
+ delete: false,
+ };
+ const isMySelf = data?.authorId === staff?.id;
+ const isDomain = staff.domainId === data.domainId;
+ const setManagePermissions = (perms: ResPerm) => {
+ Object.assign(perms, {
+ delete: true,
+ // edit: true,
+ });
+ };
+ if (isMySelf) {
+ perms.delete = true;
+ // perms.edit = true;
+ }
+ staff.permissions.forEach((permission) => {
+ switch (permission) {
+ case RolePerms.MANAGE_ANY_POST:
+ setManagePermissions(perms);
+ break;
+ case RolePerms.MANAGE_DOM_POST:
+ if (isDomain) {
+ setManagePermissions(perms);
+ }
+ break;
+ }
+ });
+ Object.assign(data, { perms });
+ }
+ async preFilter(OR?: Prisma.PostWhereInput[], staff?: UserProfile) {
+ const preFilter = (await this.getPostPreFilter(staff)) || [];
+ const outOR = OR ? [...OR, ...preFilter].filter(Boolean) : preFilter;
+ return outOR?.length > 0 ? outOR : undefined;
+ }
+ async getPostPreFilter(staff: UserProfile) {
+ const { deptId, domainId } = staff;
+ if (
+ staff.permissions.includes(RolePerms.READ_ANY_POST) ||
+ staff.permissions.includes(RolePerms.MANAGE_ANY_POST)
+ ) {
+ return undefined;
+ }
+ const parentDeptIds =
+ (await this.departmentService.getAncestorIds(staff.deptId)) || [];
+ const orCondition: Prisma.PostWhereInput[] = [
+ staff?.id && {
+ authorId: staff.id,
+ },
+ staff?.id && {
+ watchStaffs: {
+ some: {
+ id: staff.id,
+ },
+ },
+ },
+ deptId && {
+ watchDepts: {
+ some: {
+ id: {
+ in: parentDeptIds,
+ },
+ },
+ },
+ },
+
+ {
+ AND: [
+ {
+ watchStaffs: {
+ none: {}, // 匹配 watchStaffs 为空
+ },
+ },
+ {
+ watchDepts: {
+ none: {}, // 匹配 watchDepts 为空
+ },
+ },
+ ],
+ },
+ ].filter(Boolean);
+
+ if (orCondition?.length > 0) return orCondition;
+ return undefined;
+ }
+}
diff --git a/apps/server/src/models/post/utils.ts b/apps/server/src/models/post/utils.ts
new file mode 100644
index 0000000..a2e1f10
--- /dev/null
+++ b/apps/server/src/models/post/utils.ts
@@ -0,0 +1,44 @@
+import { db, Post, PostType, UserProfile, VisitType } from "@nicestack/common";
+import { getTroubleWithRelation } from "../trouble/utils";
+export async function setPostRelation(params: { data: Post, staff?: UserProfile }) {
+ const { data, staff } = params
+ const limitedComments = await db.post.findMany({
+ where: {
+ parentId: data.id,
+ type: PostType.POST_COMMENT,
+ },
+ include: {
+ author: true,
+ },
+ take: 5,
+ });
+ const commentsCount = await db.post.count({
+ where: {
+ parentId: data.id,
+ type: PostType.POST_COMMENT,
+ },
+ });
+ const readed =
+ (await db.visit.count({
+ where: {
+ postId: data.id,
+ visitType: VisitType.READED,
+ visitorId: staff?.id,
+ },
+ })) > 0;
+ const readedCount = await db.visit.count({
+ where: {
+ postId: data.id,
+ visitType: VisitType.READED,
+ },
+ });
+ const trouble = await getTroubleWithRelation(data.referenceId, staff)
+ Object.assign(data, {
+ readed,
+ readedCount,
+ limitedComments,
+ commentsCount,
+ trouble
+ })
+
+}
\ No newline at end of file
diff --git a/apps/server/src/rbac/rbac.module.ts b/apps/server/src/models/rbac/rbac.module.ts
old mode 100644
new mode 100755
similarity index 59%
rename from apps/server/src/rbac/rbac.module.ts
rename to apps/server/src/models/rbac/rbac.module.ts
index 9174e0b..9f3e0f8
--- a/apps/server/src/rbac/rbac.module.ts
+++ b/apps/server/src/models/rbac/rbac.module.ts
@@ -4,13 +4,11 @@ import { RoleRouter } from './role.router';
import { TrpcService } from '@server/trpc/trpc.service';
import { RoleService } from './role.service';
import { RoleMapRouter } from './rolemap.router';
-import { DepartmentModule } from '@server/models/department/department.module';
-import { RolePermsService } from './roleperms.service';
-import { RelationService } from '@server/relation/relation.service';
+import { DepartmentModule } from '../department/department.module';
@Module({
imports: [DepartmentModule],
- providers: [RoleMapService, RoleRouter, TrpcService, RoleService, RoleMapRouter, RolePermsService, RelationService],
- exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter, RolePermsService]
+ providers: [RoleMapService, RoleRouter, TrpcService, RoleService, RoleMapRouter],
+ exports: [RoleRouter, RoleService, RoleMapService, RoleMapRouter]
})
-export class RbacModule { }
+export class RoleMapModule { }
diff --git a/apps/server/src/models/rbac/role.router.ts b/apps/server/src/models/rbac/role.router.ts
new file mode 100755
index 0000000..a31316e
--- /dev/null
+++ b/apps/server/src/models/rbac/role.router.ts
@@ -0,0 +1,44 @@
+import { Injectable } from "@nestjs/common";
+import { TrpcService } from "@server/trpc/trpc.service";
+import { RoleService } from "./role.service";
+import { RoleMethodSchema } from "@nicestack/common";
+import { z } from "zod";
+
+@Injectable()
+export class RoleRouter {
+ constructor(
+ private readonly trpc: TrpcService,
+ private readonly roleService: RoleService
+ ) { }
+
+ router = this.trpc.router({
+ create: this.trpc.protectProcedure.input(RoleMethodSchema.create).mutation(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.roleService.create(input);
+ }),
+ deleteMany: this.trpc.protectProcedure.input(RoleMethodSchema.deleteMany).mutation(async ({ input }) => {
+ return await this.roleService.deleteMany(input);
+ }),
+ update: this.trpc.protectProcedure.input(RoleMethodSchema.update).mutation(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.roleService.update(input);
+ }),
+ paginate: this.trpc.protectProcedure.input(RoleMethodSchema.paginate).query(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.roleService.paginate(input);
+ }),
+ findById: this.trpc.protectProcedure
+ .input(z.object({ id: z.string().nullish() }))
+ .query(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.roleService.findById(input.id);
+ }),
+ findMany: this.trpc.procedure
+ .input(RoleMethodSchema.findMany) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
+ .query(async ({ input }) => {
+ return await this.roleService.findMany(input);
+ })
+ }
+ )
+}
+
diff --git a/apps/server/src/models/rbac/role.service.ts b/apps/server/src/models/rbac/role.service.ts
new file mode 100755
index 0000000..a44b7d2
--- /dev/null
+++ b/apps/server/src/models/rbac/role.service.ts
@@ -0,0 +1,180 @@
+import { Injectable } from '@nestjs/common';
+import { db, RoleMethodSchema, RowModelRequest, UserProfile, RowRequestSchema, ObjectWithId, ObjectType } from "@nicestack/common";
+import { DepartmentService } from '@server/models/department/department.service';
+import EventBus, { CrudOperation } from '@server/utils/event-bus';
+
+import { TRPCError } from '@trpc/server';
+import { RowModelService } from '../base/row-model.service';
+import { isFieldCondition, LogicalCondition } from '../base/sql-builder';
+import { z } from 'zod';
+@Injectable()
+export class RoleService extends RowModelService {
+ protected createGetRowsFilters(
+ request: z.infer,
+ staff?: UserProfile
+ ) {
+ const condition = super.createGetRowsFilters(request)
+ if (isFieldCondition(condition))
+ return {}
+ const baseModelCondition: LogicalCondition[] = [{
+ field: `${this.tableName}.deleted_at`,
+ op: "blank",
+ type: "date"
+ }]
+ condition.AND = [...baseModelCondition, ...condition.AND]
+ return condition
+ }
+ createUnGroupingRowSelect(): string[] {
+ return [
+ `${this.tableName}.id AS id`,
+ `${this.tableName}.name AS name`,
+ `${this.tableName}.system AS system`,
+ `${this.tableName}.permissions AS permissions`
+ ];
+ }
+ protected async getRowDto(data: ObjectWithId, staff?: UserProfile): Promise {
+ if (!data.id)
+ return data
+ const roleMaps = await db.roleMap.findMany({
+ where: {
+ roleId: data.id
+ }
+ })
+ const deptIds = roleMaps.filter(item => item.objectType === ObjectType.DEPARTMENT).map(roleMap => roleMap.objectId)
+ const staffIds = roleMaps.filter(item => item.objectType === ObjectType.STAFF).map(roleMap => roleMap.objectId)
+ const depts = await db.department.findMany({ where: { id: { in: deptIds } } })
+ const staffs = await db.staff.findMany({ where: { id: { in: staffIds } } })
+ const result = { ...data, depts, staffs }
+ return result
+ }
+ createJoinSql(request?: RowModelRequest): string[] {
+ return [];
+ }
+ constructor(
+ private readonly departmentService: DepartmentService
+ ) {
+ super("role")
+ }
+ /**
+ * 创建角色
+ * @param data 包含创建角色所需信息的数据
+ * @returns 创建的角色
+ */
+ async create(data: z.infer) {
+ const result = await db.role.create({ data })
+ EventBus.emit('dataChanged', {
+ type: ObjectType.ROLE,
+ operation: CrudOperation.CREATED,
+ data: result,
+ });
+ return result
+ }
+ async findById(id: string) {
+ return await db.role.findUnique({
+ where: {
+ id
+ }
+ })
+ }
+ /**
+ * 更新角色
+ * @param data 包含更新角色所需信息的数据
+ * @returns 更新后的角色
+ */
+ async update(data: z.infer) {
+ const { id, ...others } = data;
+ // 开启事务
+ const result = await db.role.update({
+ where: { id },
+ data: { ...others }
+ });
+
+ EventBus.emit('dataChanged', {
+ type: ObjectType.ROLE,
+ operation: CrudOperation.UPDATED,
+ data: result,
+ });
+ return result
+ }
+ /**
+ * 批量删除角色
+ * @param data 包含要删除的角色ID列表的数据
+ * @returns 删除结果
+ * @throws 如果未提供ID,将抛出错误
+ */
+ async deleteMany(data: z.infer) {
+ const { ids } = data;
+ if (!ids || ids.length === 0) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'No IDs provided for deletion.'
+ });
+ }
+ // 开启事务
+ const result = await db.$transaction(async (prisma) => {
+ await prisma.roleMap.deleteMany({
+ where: {
+ roleId: {
+ in: ids
+ }
+ }
+ });
+ const deletedRoles = await prisma.role.deleteMany({
+ where: {
+ id: { in: ids }
+ }
+ });
+ return { success: true, count: deletedRoles.count };
+ });
+ EventBus.emit('dataChanged', {
+ type: ObjectType.ROLE,
+ operation: CrudOperation.DELETED,
+ data: result,
+ });
+ return result
+ }
+ /**
+ * 分页获取角色
+ * @param data 包含分页信息的数据
+ * @returns 分页结果,包含角色列表和总数
+ */
+ async paginate(data: z.infer) {
+ const { page, pageSize } = data;
+ const [items, totalCount] = await Promise.all([
+ db.role.findMany({
+ skip: (page - 1) * pageSize,
+ take: pageSize,
+ orderBy: { name: "asc" },
+ where: { deletedAt: null },
+ include: {
+ roleMaps: true,
+ }
+ }),
+ db.role.count({ where: { deletedAt: null } }),
+ ]);
+ const result = { items, totalCount };
+ return result;
+ }
+ /**
+ * 根据关键字查找多个角色
+ * @param data 包含关键字的数据
+ * @returns 查找到的角色列表
+ */
+ async findMany(data: z.infer) {
+ const { keyword = '' } = data
+ return await db.role.findMany({
+ where: {
+ deletedAt: null,
+ OR: [
+ {
+ name: {
+ contains: keyword
+ }
+ }
+ ]
+ },
+ orderBy: { createdAt: "asc" },
+ take: 10
+ })
+ }
+}
diff --git a/apps/server/src/models/rbac/rolemap.router.ts b/apps/server/src/models/rbac/rolemap.router.ts
new file mode 100755
index 0000000..d0ecd08
--- /dev/null
+++ b/apps/server/src/models/rbac/rolemap.router.ts
@@ -0,0 +1,72 @@
+import { Injectable } from '@nestjs/common';
+import { TrpcService } from '@server/trpc/trpc.service';
+import {
+ ChangedRows,
+ ObjectType,
+ RoleMapMethodSchema,
+} from '@nicestack/common';
+import { RoleMapService } from './rolemap.service';
+
+@Injectable()
+export class RoleMapRouter {
+ constructor(
+ private readonly trpc: TrpcService,
+ private readonly roleMapService: RoleMapService,
+ ) { }
+ router = this.trpc.router({
+ deleteAllRolesForObject: this.trpc.protectProcedure
+ .input(RoleMapMethodSchema.deleteWithObject)
+ .mutation(({ input }) =>
+ this.roleMapService.deleteAllRolesForObject(input),
+ ),
+ setRoleForObject: this.trpc.protectProcedure
+ .input(RoleMapMethodSchema.create)
+ .mutation(({ input }) => this.roleMapService.setRoleForObject(input)),
+ setRoleForObjects: this.trpc.protectProcedure
+ .input(RoleMapMethodSchema.setRoleForObjects)
+ .mutation(({ input }) => this.roleMapService.setRoleForObjects(input)),
+ addRoleForObjects: this.trpc.protectProcedure
+ .input(RoleMapMethodSchema.setRoleForObjects)
+ .mutation(({ input }) => this.roleMapService.addRoleForObjects(input)),
+ setRolesForObject: this.trpc.protectProcedure
+ .input(RoleMapMethodSchema.setRolesForObject)
+ .mutation(({ input }) => this.roleMapService.setRolesForObject(input)),
+
+ getPermsForObject: this.trpc.procedure
+ .input(RoleMapMethodSchema.getPermsForObject)
+ .query(({ input }) => this.roleMapService.getPermsForObject(input)),
+ deleteMany: this.trpc.protectProcedure
+ .input(RoleMapMethodSchema.deleteMany) // Assuming RoleMapMethodSchema.deleteMany is the Zod schema for batch deleting staff
+ .mutation(async ({ input }) => {
+ return await this.roleMapService.deleteMany(input);
+ }),
+
+ paginate: this.trpc.procedure
+ .input(RoleMapMethodSchema.paginate) // Define the input schema for pagination
+ .query(async ({ input }) => {
+ return await this.roleMapService.paginate(input);
+ }),
+ update: this.trpc.protectProcedure
+ .input(RoleMapMethodSchema.update)
+ .mutation(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.roleMapService.update(input);
+ }),
+ getRoleMapDetail: this.trpc.procedure
+ .input(RoleMapMethodSchema.getRoleMapDetail)
+ .query(async ({ input }) => {
+ return await this.roleMapService.getRoleMapDetail(input);
+ }),
+ getRows: this.trpc.procedure
+ .input(RoleMapMethodSchema.getRows)
+ .query(async ({ input, ctx }) => {
+ const { staff } = ctx;
+ return await this.roleMapService.getRows(input, staff);
+ }),
+ getStaffsNotMap: this.trpc.procedure
+ .input(RoleMapMethodSchema.getStaffsNotMap)
+ .query(async ({ input }) => {
+ return this.roleMapService.getStaffsNotMap(input);
+ }),
+ });
+}
diff --git a/apps/server/src/models/rbac/rolemap.service.ts b/apps/server/src/models/rbac/rolemap.service.ts
new file mode 100755
index 0000000..f026b9a
--- /dev/null
+++ b/apps/server/src/models/rbac/rolemap.service.ts
@@ -0,0 +1,317 @@
+import { Injectable } from '@nestjs/common';
+import {
+ db,
+ RoleMapMethodSchema,
+ ObjectType,
+ Prisma,
+ RowModelRequest,
+ UserProfile,
+ ObjectWithId,
+} from '@nicestack/common';
+import { DepartmentService } from '@server/models/department/department.service';
+import { TRPCError } from '@trpc/server';
+import { RowModelService } from '../base/row-model.service';
+import { isFieldCondition } from '../base/sql-builder';
+import { z } from 'zod';
+
+@Injectable()
+export class RoleMapService extends RowModelService {
+ createJoinSql(request?: RowModelRequest): string[] {
+ return [
+ `LEFT JOIN staff ON staff.id = ${this.tableName}.object_id`,
+ `LEFT JOIN department ON department.id = staff.dept_id`,
+ ];
+ }
+ createUnGroupingRowSelect(): string[] {
+ return [
+ `${this.tableName}.id AS id`,
+ `${this.tableName}.object_id AS object_id`,
+ `${this.tableName}.role_id AS role_id`,
+ `${this.tableName}.domain_id AS domain_id`,
+ `${this.tableName}.object_type AS object_type`,
+ `staff.officer_id AS staff_officer_id`,
+ `staff.username AS staff_username`,
+ `department.name AS department_name`,
+ `staff.showname AS staff_`,
+ ];
+ }
+
+ constructor(private readonly departmentService: DepartmentService) {
+ super('rolemap');
+ }
+ protected createGetRowsFilters(
+ request: z.infer,
+ staff: UserProfile,
+ ) {
+ const { roleId, domainId } = request;
+ // Base conditions
+ let condition = super.createGetRowsFilters(request, staff);
+ if (isFieldCondition(condition)) return;
+ // Adding conditions based on parameters existence
+ if (roleId) {
+ condition.AND.push({
+ field: `${this.tableName}.role_id`,
+ value: roleId,
+ op: 'equals',
+ });
+ }
+ if (domainId) {
+ condition.AND.push({
+ field: `${this.tableName}.domain_id`,
+ value: domainId,
+ op: 'equals',
+ });
+ }
+ return condition;
+ }
+
+ protected async getRowDto(
+ row: ObjectWithId,
+ staff?: UserProfile,
+ ): Promise {
+ if (!row.id) return row;
+ return row;
+ }
+ /**
+ * 删除某对象的所有角色
+ * @param data 包含对象ID的数据
+ * @returns 删除结果
+ */
+ async deleteAllRolesForObject(
+ data: z.infer,
+ ) {
+ const { objectId } = data;
+ return await db.roleMap.deleteMany({
+ where: {
+ objectId,
+ },
+ });
+ }
+ /**
+ * 为某对象设置一个角色
+ * @param data 角色映射数据
+ * @returns 创建的角色映射
+ */
+ async setRoleForObject(data: z.infer) {
+ return await db.roleMap.create({
+ data,
+ });
+ }
+ /**
+ * 批量为多个对象创建角色映射
+ * @param data 角色映射数据
+ * @returns 创建的角色映射列表
+ */
+ async setRoleForObjects(
+ data: z.infer,
+ ) {
+ const { domainId, roleId, objectIds, objectType } = data;
+ const roleMaps = objectIds.map((id) => ({
+ domainId,
+ objectId: id,
+ roleId,
+ objectType,
+ }));
+ // 开启事务
+ const result = await db.$transaction(async (prisma) => {
+ // 首先,删除现有的角色映射
+ await prisma.roleMap.deleteMany({
+ where: {
+ domainId,
+ roleId,
+ objectType,
+ },
+ });
+ // 然后,创建新的角色映射
+ return await prisma.roleMap.createManyAndReturn({
+ data: roleMaps,
+ });
+ });
+ const wrapResult = Promise.all(result.map(async item => {
+ const staff = await db.staff.findMany({
+ include: { department: true },
+ where: {
+ id: item.objectId
+ }
+ })
+ return { ...item, staff }
+ }))
+ return wrapResult;
+ }
+ async addRoleForObjects(
+ data: z.infer,
+ ) {
+ const { domainId, roleId, objectIds, objectType } = data;
+ const objects = await db.roleMap.findMany({
+ where: { domainId, roleId, objectType },
+ });
+ data.objectIds = Array.from(
+ new Set([...objectIds, ...objects.map((obj) => obj.objectId)]),
+ );
+ const result = this.setRoleForObjects(data);
+ return result;
+ }
+ /**
+ * 为某对象设置多个角色
+ * @param data 角色映射数据
+ * @returns 创建的角色映射列表
+ */
+ async setRolesForObject(
+ data: z.infer,
+ ) {
+ const { domainId, objectId, roleIds, objectType } = data;
+ const roleMaps = roleIds.map((id) => ({
+ domainId,
+ objectId,
+ roleId: id,
+ objectType,
+ }));
+
+ return await db.roleMap.createMany({ data: roleMaps });
+ }
+
+ /**
+ * 获取某对象的权限
+ * @param data 包含域ID、部门ID和对象ID的数据
+ * @returns 用户角色的权限列表
+ */
+ async getPermsForObject(
+ data: z.infer,
+ ) {
+ const { domainId, deptId, staffId } = data;
+ // Get all ancestor department IDs if deptId is provided.
+ const ancestorDeptIds = deptId
+ ? await this.departmentService.getAncestorIds(deptId)
+ : [];
+ // Define a common filter for querying roles.
+ const objectFilters: Prisma.RoleMapWhereInput[] = [
+ { objectId: staffId, objectType: ObjectType.STAFF },
+ ...(deptId || ancestorDeptIds.length > 0
+ ? [
+ {
+ objectId: { in: [deptId, ...ancestorDeptIds].filter(Boolean) },
+ objectType: ObjectType.DEPARTMENT,
+ },
+ ]
+ : []),
+ ];
+ // Helper function to fetch roles based on domain ID.
+ const fetchRoles = async (domainId: string) => {
+ return db.roleMap.findMany({
+ where: {
+ AND: {
+ domainId,
+ OR: objectFilters,
+ },
+ },
+ include: { role: true },
+ });
+ };
+ // Fetch roles with and without specific domain IDs.
+ const [nullDomainRoles, userRoles] = await Promise.all([
+ fetchRoles(null),
+ fetchRoles(domainId),
+ ]);
+ // Extract permissions from roles and return them.
+ return [...userRoles, ...nullDomainRoles].flatMap(
+ ({ role }) => role.permissions,
+ );
+ }
+
+ /**
+ * 批量删除角色映射
+ * @param data 包含要删除的角色映射ID列表的数据
+ * @returns 删除结果
+ * @throws 如果未提供ID,将抛出错误
+ */
+ async deleteMany(data: z.infer) {
+ const { ids } = data;
+ if (!ids || ids.length === 0) {
+ throw new TRPCError({
+ code: 'BAD_REQUEST',
+ message: 'No IDs provided for deletion.',
+ });
+ }
+ const rowData = await this.getRowByIds({ ids, extraCondition: {} });
+ await db.roleMap.deleteMany({
+ where: { id: { in: ids } },
+ });
+ return rowData;
+ }
+
+ /**
+ * 分页获取角色映射
+ * @param data 包含分页信息的数据
+ * @returns 分页结果,包含角色映射列表和总数
+ */
+ async paginate(data: z.infer) {
+ const { page, pageSize, domainId, roleId } = data;
+
+ const [items, totalCount] = await Promise.all([
+ db.roleMap.findMany({
+ skip: (page - 1) * pageSize,
+ take: pageSize,
+ where: { domainId, roleId },
+ }),
+ db.roleMap.count({
+ where: { domainId, roleId },
+ }),
+ ]);
+
+ // const processedItems = await Promise.all(items.map(item => this.genRoleMapDto(item)));
+ return { items, totalCount };
+ }
+ async getStaffsNotMap(data: z.infer) {
+ const { domainId, roleId } = data;
+ let staffs = await db.staff.findMany({
+ where: {
+ domainId,
+ },
+ });
+ const roleMaps = await db.roleMap.findMany({
+ where: {
+ domainId,
+ roleId,
+ objectType: ObjectType.STAFF,
+ },
+ });
+ staffs = staffs.filter(
+ (staff) =>
+ roleMaps.findIndex((roleMap) => roleMap.objectId === staff.id) === -1,
+ );
+ return staffs;
+ }
+ /**
+ * 更新角色映射
+ * @param data 包含更新信息的数据
+ * @returns 更新后的角色映射
+ */
+ async update(data: z.infer) {
+ const { id, ...others } = data;
+
+ const updatedRoleMap = await db.roleMap.update({
+ where: { id },
+ data: { ...others },
+ });
+ return updatedRoleMap;
+ }
+
+ /**
+ * 获取角色映射详情
+ * @param data 包含角色ID和域ID的数据
+ * @returns 角色映射详情,包含部门ID和员工ID列表
+ */
+ async getRoleMapDetail(data: z.infer) {
+ const { roleId, domainId } = data;
+ const res = await db.roleMap.findMany({ where: { roleId, domainId } });
+
+ const deptIds = res
+ .filter((item) => item.objectType === ObjectType.DEPARTMENT)
+ .map((item) => item.objectId);
+ const staffIds = res
+ .filter((item) => item.objectType === ObjectType.STAFF)
+ .map((item) => item.objectId);
+
+ return { deptIds, staffIds };
+ }
+}
diff --git a/apps/server/src/models/staff/staff.controller.ts b/apps/server/src/models/staff/staff.controller.ts
new file mode 100755
index 0000000..45e8349
--- /dev/null
+++ b/apps/server/src/models/staff/staff.controller.ts
@@ -0,0 +1,48 @@
+import { Controller, Get, Query, UseGuards } from '@nestjs/common';
+
+import { StaffService } from './staff.service';
+import { AuthGuard } from '@server/auth/auth.guard';
+import { db } from '@nicestack/common';
+
+@Controller('staff')
+export class StaffController {
+ constructor(private readonly staffService: StaffService) {}
+ @UseGuards(AuthGuard)
+ @Get('find-by-id')
+ async findById(@Query('id') id: string) {
+ try {
+ const result = await this.staffService.findById(id);
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+ @Get('find-by-dept')
+ async findByDept(
+ @Query('dept-id') deptId: string,
+ @Query('domain-id') domainId: string,
+ ) {
+ try {
+ const result = await this.staffService.findByDept({ deptId, domainId });
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+}
diff --git a/apps/server/src/models/staff/staff.module.ts b/apps/server/src/models/staff/staff.module.ts
old mode 100644
new mode 100755
index c7240ed..fa681dc
--- a/apps/server/src/models/staff/staff.module.ts
+++ b/apps/server/src/models/staff/staff.module.ts
@@ -1,11 +1,15 @@
import { Module } from '@nestjs/common';
-import { StaffRouter } from './staff.router';
import { StaffService } from './staff.service';
+import { StaffRouter } from './staff.router';
import { TrpcService } from '@server/trpc/trpc.service';
-import { DepartmentService } from '../department/department.service';
+import { DepartmentModule } from '../department/department.module';
+import { StaffController } from './staff.controller';
+import { StaffRowService } from './staff.row.service';
@Module({
- providers: [StaffRouter, StaffService, TrpcService, DepartmentService],
- exports: [StaffRouter, StaffService]
+ imports: [DepartmentModule],
+ providers: [StaffService, StaffRouter, TrpcService, StaffRowService],
+ exports: [StaffService, StaffRouter, StaffRowService],
+ controllers: [StaffController],
})
export class StaffModule { }
diff --git a/apps/server/src/models/staff/staff.router.ts b/apps/server/src/models/staff/staff.router.ts
index f08dde0..5ea3015 100755
--- a/apps/server/src/models/staff/staff.router.ts
+++ b/apps/server/src/models/staff/staff.router.ts
@@ -1,48 +1,79 @@
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import { StaffService } from './staff.service'; // Adjust the import path as necessary
-import { z, StaffSchema } from '@nicestack/common';
-
+import { StaffMethodSchema, Prisma, UpdateOrderSchema } from '@nicestack/common';
+import { z, ZodType } from 'zod';
+import { StaffRowService } from './staff.row.service';
+const StaffCreateArgsSchema: ZodType = z.any();
+const StaffUpdateArgsSchema: ZodType = z.any();
+const StaffFindFirstArgsSchema: ZodType = z.any();
+const StaffDeleteManyArgsSchema: ZodType = z.any();
+const StaffWhereInputSchema: ZodType = z.any();
+const StaffSelectSchema: ZodType = z.any();
+const StaffUpdateInputSchema: ZodType = z.any();
+const StaffFindManyArgsSchema: ZodType = z.any();
@Injectable()
export class StaffRouter {
constructor(
private readonly trpc: TrpcService,
private readonly staffService: StaffService,
+ private readonly staffRowService: StaffRowService
) { }
router = this.trpc.router({
create: this.trpc.procedure
- .input(StaffSchema.create) // Assuming StaffSchema.create is the Zod schema for creating staff
+ .input(StaffCreateArgsSchema) // Assuming StaffMethodSchema.create is the Zod schema for creating staff
.mutation(async ({ input }) => {
return await this.staffService.create(input);
}),
update: this.trpc.procedure
- .input(StaffSchema.update) // Assuming StaffSchema.update is the Zod schema for updating staff
+ .input(StaffUpdateArgsSchema) // Assuming StaffMethodSchema.update is the Zod schema for updating staff
.mutation(async ({ input }) => {
return await this.staffService.update(input);
}),
-
- batchDelete: this.trpc.procedure
- .input(StaffSchema.batchDelete) // Assuming StaffSchema.batchDelete is the Zod schema for batch deleting staff
- .mutation(async ({ input }) => {
- return await this.staffService.batchDelete(input);
+ updateUserDomain: this.trpc.protectProcedure
+ .input(
+ z.object({
+ domainId: z.string()
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ return await this.staffService.updateUserDomain(input, ctx.staff);
}),
-
- paginate: this.trpc.procedure
- .input(StaffSchema.paginate) // Define the input schema for pagination
- .query(async ({ input }) => {
- return await this.staffService.paginate(input);
+ softDeleteByIds: this.trpc.protectProcedure
+ .input(
+ z.object({
+ ids: z.array(z.string()),
+ data: StaffUpdateInputSchema.nullish(),
+ }),
+ )
+ .mutation(async ({ input }) => {
+ return await this.staffService.softDeleteByIds(input.ids, input.data);
}),
findByDept: this.trpc.procedure
- .input(StaffSchema.findByDept)
+ .input(StaffMethodSchema.findByDept)
.query(async ({ input }) => {
return await this.staffService.findByDept(input);
}),
findMany: this.trpc.procedure
- .input(StaffSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
+ .input(StaffFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
.query(async ({ input }) => {
return await this.staffService.findMany(input);
- })
+ }),
+ getRows: this.trpc.protectProcedure
+ .input(StaffMethodSchema.getRows)
+ .query(async ({ input, ctx }) => {
+ return await this.staffRowService.getRows(input, ctx.staff);
+ }),
+ findFirst: this.trpc.protectProcedure
+ .input(StaffFindFirstArgsSchema)
+ .query(async ({ ctx, input }) => {
+ const { staff } = ctx;
+ return await this.staffService.findFirst(input);
+ }),
+ updateOrder: this.trpc.protectProcedure.input(UpdateOrderSchema).mutation(async ({ input }) => {
+ return this.staffService.updateOrder(input)
+ }),
});
}
diff --git a/apps/server/src/models/staff/staff.row.service.ts b/apps/server/src/models/staff/staff.row.service.ts
new file mode 100644
index 0000000..830f54c
--- /dev/null
+++ b/apps/server/src/models/staff/staff.row.service.ts
@@ -0,0 +1,135 @@
+import { Injectable } from '@nestjs/common';
+import {
+ db,
+ ObjectType,
+ StaffMethodSchema,
+ UserProfile,
+ RolePerms,
+ ResPerm,
+ Staff,
+ RowModelRequest,
+} from '@nicestack/common';
+import { DepartmentService } from '../department/department.service';
+import { RowCacheService } from '../base/row-cache.service';
+import { z } from 'zod';
+import { isFieldCondition } from '../base/sql-builder';
+@Injectable()
+export class StaffRowService extends RowCacheService {
+ constructor(
+ private readonly departmentService: DepartmentService,
+ ) {
+ super(ObjectType.STAFF, false);
+ }
+ createUnGroupingRowSelect(request?: RowModelRequest): string[] {
+ const result = super.createUnGroupingRowSelect(request).concat([
+ `${this.tableName}.id AS id`,
+ `${this.tableName}.username AS username`,
+ `${this.tableName}.showname AS showname`,
+ `${this.tableName}.avatar AS avatar`,
+ `${this.tableName}.officer_id AS officer_id`,
+ `${this.tableName}.phone_number AS phone_number`,
+ `${this.tableName}.order AS order`,
+ `${this.tableName}.enabled AS enabled`,
+ 'dept.name AS dept_name',
+ 'domain.name AS domain_name',
+ ]);
+ return result
+ }
+ createJoinSql(request?: RowModelRequest): string[] {
+ return [
+ `LEFT JOIN department dept ON ${this.tableName}.dept_id = dept.id`,
+ `LEFT JOIN department domain ON ${this.tableName}.domain_id = domain.id`,
+ ];
+ }
+ protected createGetRowsFilters(
+ request: z.infer,
+ staff: UserProfile,
+ ) {
+ const condition = super.createGetRowsFilters(request);
+ const { domainId, includeDeleted = false } = request;
+ if (isFieldCondition(condition)) {
+ return;
+ }
+ if (domainId) {
+ condition.AND.push({
+ field: `${this.tableName}.domain_id`,
+ value: domainId,
+ op: 'equals',
+ });
+ } else {
+ condition.AND.push({
+ field: `${this.tableName}.domain_id`,
+ op: 'blank',
+ });
+ }
+ if (!includeDeleted) {
+ condition.AND.push({
+ field: `${this.tableName}.deleted_at`,
+ type: 'date',
+ op: 'blank',
+ });
+ }
+ condition.OR = [];
+ if (!staff.permissions.includes(RolePerms.MANAGE_ANY_STAFF)) {
+ if (staff.permissions.includes(RolePerms.MANAGE_DOM_STAFF)) {
+ condition.OR.push({
+ field: 'dept.id',
+ value: staff.domainId,
+ op: 'equals',
+ });
+ }
+ }
+
+ return condition;
+ }
+
+ async getPermissionContext(id: string, staff: UserProfile) {
+ const data = await db.staff.findUnique({
+ where: { id },
+ select: {
+ deptId: true,
+ domainId: true,
+ },
+ });
+ const deptId = data?.deptId;
+ const isFromSameDept = staff.deptIds?.includes(deptId);
+ const domainChildDeptIds = await this.departmentService.getDescendantIds(
+ staff.domainId, true
+ );
+ const belongsToDomain = domainChildDeptIds.includes(
+ deptId,
+ );
+ return { isFromSameDept, belongsToDomain };
+ }
+ protected async setResPermissions(
+ data: Staff,
+ staff: UserProfile,
+ ) {
+ const permissions: ResPerm = {};
+ const { isFromSameDept, belongsToDomain } = await this.getPermissionContext(
+ data.id,
+ staff,
+ );
+ const setManagePermissions = (permissions: ResPerm) => {
+ Object.assign(permissions, {
+ read: true,
+ delete: true,
+ edit: true,
+ });
+ };
+ staff.permissions.forEach((permission) => {
+ switch (permission) {
+ case RolePerms.MANAGE_ANY_STAFF:
+ setManagePermissions(permissions);
+ break;
+ case RolePerms.MANAGE_DOM_STAFF:
+ if (belongsToDomain) {
+ setManagePermissions(permissions);
+ }
+ break;
+ }
+ });
+ return { ...data, perm: permissions };
+ }
+
+}
diff --git a/apps/server/src/models/staff/staff.service.spec.ts b/apps/server/src/models/staff/staff.service.spec.ts
deleted file mode 100644
index d653df4..0000000
--- a/apps/server/src/models/staff/staff.service.spec.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { Test, TestingModule } from '@nestjs/testing';
-import { StaffService } from './staff.service';
-
-describe('StaffService', () => {
- let service: StaffService;
-
- beforeEach(async () => {
- const module: TestingModule = await Test.createTestingModule({
- providers: [StaffService],
- }).compile();
-
- service = module.get(StaffService);
- });
-
- it('should be defined', () => {
- expect(service).toBeDefined();
- });
-});
diff --git a/apps/server/src/models/staff/staff.service.ts b/apps/server/src/models/staff/staff.service.ts
old mode 100644
new mode 100755
index 20dcac2..c9ccd02
--- a/apps/server/src/models/staff/staff.service.ts
+++ b/apps/server/src/models/staff/staff.service.ts
@@ -1,178 +1,180 @@
import { Injectable } from '@nestjs/common';
-import { db, ObjectType, Staff, StaffSchema, z } from '@nicestack/common';
-import { TRPCError } from '@trpc/server';
+import {
+ db,
+ StaffMethodSchema,
+ ObjectType,
+ UserProfile,
+ Prisma,
+} from '@nicestack/common';
import { DepartmentService } from '../department/department.service';
+import { z } from 'zod';
+import { BaseService } from '../base/base.service';
+import * as argon2 from 'argon2';
+import EventBus, { CrudOperation } from '@server/utils/event-bus';
+
@Injectable()
-export class StaffService {
- constructor(private readonly departmentService: DepartmentService) { }
+export class StaffService extends BaseService {
- /**
- * 获取某一单位下所有staff的记录
- * @param deptId 单位的id
- * @returns 查到的staff记录
- */
- async findByDept(data: z.infer) {
- const { deptId, domainId } = data;
- const childDepts = await this.departmentService.getAllChildDeptIds(deptId);
- const result = await db.staff.findMany({
- where: {
- deptId: { in: [...childDepts, deptId] },
- domainId,
- },
+ constructor(private readonly departmentService: DepartmentService) {
+ super(db, ObjectType.STAFF, true);
+ }
+ /**
+ * 获取某一单位下所有staff的记录
+ * @param deptId 单位的id
+ * @returns 查到的staff记录
+ */
+ async findByDept(data: z.infer) {
+ const { deptId, domainId } = data;
+ const childDepts = await this.departmentService.getDescendantIds(deptId, true);
+ const result = await db.staff.findMany({
+ where: {
+ deptId: { in: childDepts },
+ domainId,
+ },
+ });
+ return result;
+ }
+ async create(args: Prisma.StaffCreateArgs) {
+ const { data } = args;
+ await this.validateUniqueFields(data);
+ const createData = {
+ ...data,
+ password: await argon2.hash((data.password || '123456') as string),
+ };
+ const result = await super.create({ ...args, data: createData });
+ this.emitDataChangedEvent(result, CrudOperation.CREATED);
+ return result;
+ }
+ async update(args: Prisma.StaffUpdateArgs) {
+ const { data, where } = args;
+ await this.validateUniqueFields(data, where.id);
+ const updateData = {
+ ...data,
+ ...(data.password && { password: await argon2.hash(data.password as string) })
+ };
+ const result = await super.update({ ...args, data: updateData });
+ this.emitDataChangedEvent(result, CrudOperation.UPDATED);
+ return result;
+ }
+ private async validateUniqueFields(data: any, excludeId?: string) {
+ const uniqueFields = [
+ { field: 'officerId', errorMsg: (val: string) => `证件号为${val}的用户已存在` },
+ { field: 'phoneNumber', errorMsg: (val: string) => `手机号为${val}的用户已存在` },
+ { field: 'username', errorMsg: (val: string) => `帐号为${val}的用户已存在` }
+ ];
+ for (const { field, errorMsg } of uniqueFields) {
+ if (data[field]) {
+ const count = await db.staff.count({
+ where: {
+ [field]: data[field],
+ ...(excludeId && { id: { not: excludeId } })
+ }
});
- return result;
- }
- /**
- * 创建新的员工记录
- * @param data 员工创建信息
- * @returns 新创建的员工记录
- */
- async create(data: z.infer) {
- const { ...others } = data;
-
- try {
- return await db.$transaction(async (transaction) => {
- // 获取当前最大order值
- const maxOrder = await transaction.staff.aggregate({
- _max: { order: true },
- });
- // 新员工的order值比现有最大order值大1
- const newOrder = (maxOrder._max.order ?? -1) + 1;
- // 创建新员工记录
- const newStaff = await transaction.staff.create({
- data: { ...others, order: newOrder },
- include: { domain: true, department: true },
- });
- return newStaff;
- });
- } catch (error) {
- console.error('Failed to create staff:', error);
- throw error;
+ if (count > 0) {
+ throw new Error(errorMsg(data[field]));
}
+ }
}
- /**
- * 更新员工记录
- * @param data 包含id和其他更新字段的对象
- * @returns 更新后的员工记录
- */
- async update(data: z.infer) {
- const { id, ...others } = data;
- try {
- return await db.$transaction(async (transaction) => {
- // 更新员工记录
- const updatedStaff = await transaction.staff.update({
- where: { id },
- data: others,
- include: { domain: true, department: true },
- });
- return updatedStaff;
- });
- } catch (error) {
- console.error('Failed to update staff:', error);
- throw error;
- }
- }
- /**
- * 批量删除员工记录(软删除)
- * @param data 包含要删除的员工ID数组的对象
- * @returns 删除操作结果,包括删除的记录数
- */
- async batchDelete(data: z.infer) {
- const { ids } = data;
-
- if (!ids || ids.length === 0) {
- throw new TRPCError({
- code: 'BAD_REQUEST',
- message: 'No IDs provided for deletion.',
- });
- }
- const deletedStaffs = await db.staff.updateMany({
- where: { id: { in: ids } },
- data: { deletedAt: new Date() },
- });
- if (!deletedStaffs.count) {
- throw new TRPCError({
- code: 'NOT_FOUND',
- message: 'No taxonomies were found with the provided IDs.',
- });
- }
- return { success: true, count: deletedStaffs.count };
- }
- /**
- * 分页查询员工
- * @param data 包含分页参数、域ID和部门ID的对象
- * @returns 员工列表及总记录数
- */
- async paginate(data: z.infer) {
- const { page, pageSize, domainId, deptId, ids } = data;
- const childDepts = await this.departmentService.getAllChildDeptIds(deptId);
- const [items, totalCount] = await Promise.all([
- db.staff.findMany({
- skip: (page - 1) * pageSize,
- take: pageSize,
- orderBy: { order: 'asc' },
- where: {
- id: ids ? { in: ids } : undefined,
- deletedAt: null,
- domainId,
- deptId: deptId ? { in: [...childDepts, deptId] } : undefined,
- },
- include: { domain: true, department: true },
- }),
- db.staff.count({
- where: {
- deletedAt: null,
- domainId,
- deptId: deptId ? { in: [...childDepts, deptId] } : undefined,
- },
- }),
- ]);
- const processedItems = await Promise.all(
- items.map((item) => this.genStaffDto(item)),
- );
- return { items: processedItems, totalCount };
- }
- /**
- * 根据关键词或ID集合查找员工
- * @param data 包含关键词、域ID和ID集合的对象
- * @returns 匹配的员工记录列表
- */
- async findMany(data: z.infer) {
- const { keyword, domainId, ids } = data;
-
- return await db.staff.findMany({
- where: {
- deletedAt: null,
- domainId,
- OR: [
- { username: { contains: keyword } },
- {
- id: { in: ids },
- },
- ],
- },
- orderBy: { order: 'asc' },
- take: 10,
- });
- }
- /**
- * 生成员工的数据传输对象(DTO)
- * @param staff 员工记录
- * @returns 含角色ID列表的员工DTO
- */
- private async genStaffDto(staff: Staff) {
- const roleMaps = await db.roleMap.findMany({
- where: {
- domainId: staff.domainId,
- objectId: staff.id,
- objectType: ObjectType.STAFF,
- },
- include: { role: true },
- });
- const roleIds = roleMaps.map((roleMap) => roleMap.role.id);
- return { ...staff, roleIds };
- }
+ }
+ private emitDataChangedEvent(data: any, operation: CrudOperation) {
+ EventBus.emit("dataChanged", {
+ type: this.objectType,
+ operation,
+ data,
+ });
+ }
+
+ /**
+ * 更新员工DomainId
+ * @param data 包含domainId对象
+ * @returns 更新后的员工记录
+ */
+ async updateUserDomain(data: { domainId?: string }, staff?: UserProfile) {
+ let { domainId } = data;
+ if (staff.domainId !== domainId) {
+ const result = await this.update({
+ where: { id: staff.id },
+ data: {
+ domainId,
+ deptId: null,
+ },
+ });
+ return result;
+ } else {
+ return staff;
+ }
+ }
+
+
+ // /**
+ // * 根据关键词或ID集合查找员工
+ // * @param data 包含关键词、域ID和ID集合的对象
+ // * @returns 匹配的员工记录列表
+ // */
+ // async findMany(data: z.infer) {
+ // const { keyword, domainId, ids, deptId, limit = 30 } = data;
+ // const idResults = ids
+ // ? await db.staff.findMany({
+ // where: {
+ // id: { in: ids },
+ // deletedAt: null,
+ // domainId,
+ // deptId,
+ // },
+ // select: {
+ // id: true,
+ // showname: true,
+ // username: true,
+ // deptId: true,
+ // domainId: true,
+ // department: true,
+ // domain: true,
+ // },
+ // })
+ // : [];
+
+ // const mainResults = await db.staff.findMany({
+ // where: {
+ // deletedAt: null,
+ // domainId,
+ // deptId,
+ // OR: (keyword || ids) && [
+ // { showname: { contains: keyword } },
+ // {
+ // username: {
+ // contains: keyword,
+ // },
+ // },
+ // { phoneNumber: { contains: keyword } },
+ // // {
+ // // id: { in: ids },
+ // // },
+ // ],
+ // },
+ // select: {
+ // id: true,
+ // showname: true,
+ // username: true,
+ // deptId: true,
+ // domainId: true,
+ // department: true,
+ // domain: true,
+ // },
+ // orderBy: { order: 'asc' },
+ // take: limit !== -1 ? limit : undefined,
+ // });
+ // // Combine results, ensuring no duplicates
+ // const combinedResults = [
+ // ...mainResults,
+ // ...idResults.filter(
+ // (idResult) =>
+ // !mainResults.some((mainResult) => mainResult.id === idResult.id),
+ // ),
+ // ];
+
+ // return combinedResults;
+ // }
}
diff --git a/apps/server/src/models/taxonomy/taxonomy.controller.ts b/apps/server/src/models/taxonomy/taxonomy.controller.ts
new file mode 100755
index 0000000..00e0b6f
--- /dev/null
+++ b/apps/server/src/models/taxonomy/taxonomy.controller.ts
@@ -0,0 +1,28 @@
+import { Controller, Get, Query, UseGuards } from '@nestjs/common';
+
+import { TaxonomyService } from './taxonomy.service';
+import { AuthGuard } from '@server/auth/auth.guard';
+import { db } from '@nicestack/common';
+
+@Controller('tax')
+export class TaxonomyController {
+ constructor(private readonly taxService: TaxonomyService) {}
+ @UseGuards(AuthGuard)
+ @Get('find-by-id')
+ async findById(@Query('id') id: string) {
+ try {
+ const result = await this.taxService.findById({ id });
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+}
diff --git a/apps/server/src/models/taxonomy/taxonomy.module.ts b/apps/server/src/models/taxonomy/taxonomy.module.ts
old mode 100644
new mode 100755
index a8aa9ab..64e6f55
--- a/apps/server/src/models/taxonomy/taxonomy.module.ts
+++ b/apps/server/src/models/taxonomy/taxonomy.module.ts
@@ -2,11 +2,11 @@ import { Module } from '@nestjs/common';
import { TaxonomyRouter } from './taxonomy.router';
import { TaxonomyService } from './taxonomy.service';
import { TrpcService } from '@server/trpc/trpc.service';
-import { RedisModule } from '@server/redis/redis.module';
+import { TaxonomyController } from './taxonomy.controller';
@Module({
- imports: [RedisModule],
- providers: [TaxonomyRouter, TaxonomyService, TrpcService],
- exports: [TaxonomyRouter, TaxonomyService]
+ providers: [TaxonomyRouter, TaxonomyService, TrpcService],
+ exports: [TaxonomyRouter, TaxonomyService],
+ controllers: [TaxonomyController],
})
-export class TaxonomyModule { }
+export class TaxonomyModule {}
diff --git a/apps/server/src/models/taxonomy/taxonomy.router.ts b/apps/server/src/models/taxonomy/taxonomy.router.ts
old mode 100644
new mode 100755
index c575bb3..1f302a7
--- a/apps/server/src/models/taxonomy/taxonomy.router.ts
+++ b/apps/server/src/models/taxonomy/taxonomy.router.ts
@@ -1,42 +1,55 @@
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import { TaxonomyService } from './taxonomy.service';
-import { TaxonomySchema } from '@nicestack/common';
+import { TaxonomyMethodSchema } from '@nicestack/common';
@Injectable()
export class TaxonomyRouter {
- constructor(
- private readonly trpc: TrpcService,
- private readonly taxonomyService: TaxonomyService
- ) { }
+ constructor(
+ private readonly trpc: TrpcService,
+ private readonly taxonomyService: TaxonomyService,
+ ) { }
- router = this.trpc.router({
- create: this.trpc.procedure.input(TaxonomySchema.create).mutation(async ({ input }) => {
- return this.taxonomyService.create(input);
- }),
-
- findById: this.trpc.procedure.input(TaxonomySchema.findById).query(async ({ input }) => {
- return this.taxonomyService.findById(input);
- }),
-
- update: this.trpc.procedure.input(TaxonomySchema.update).mutation(async ({ input }) => {
- return this.taxonomyService.update(input);
- }),
-
- delete: this.trpc.procedure.input(TaxonomySchema.delete).mutation(async ({ input }) => {
- return this.taxonomyService.delete(input);
- }),
-
- batchDelete: this.trpc.procedure.input(TaxonomySchema.batchDelete).mutation(async ({ input }) => {
- return this.taxonomyService.batchDelete(input);
- }),
-
- paginate: this.trpc.procedure.input(TaxonomySchema.paginate!).query(async ({ input }) => {
- return this.taxonomyService.paginate(input);
- }),
-
- getAll: this.trpc.procedure.query(() => {
- return this.taxonomyService.getAll();
- })
- });
+ router = this.trpc.router({
+ create: this.trpc.procedure
+ .input(TaxonomyMethodSchema.create)
+ .mutation(async ({ input }) => {
+ return this.taxonomyService.create(input);
+ }),
+ findById: this.trpc.procedure
+ .input(TaxonomyMethodSchema.findById)
+ .query(async ({ input }) => {
+ return this.taxonomyService.findById(input);
+ }),
+ findBySlug: this.trpc.procedure
+ .input(TaxonomyMethodSchema.findBySlug)
+ .query(async ({ input }) => {
+ return this.taxonomyService.findBySlug(input);
+ }),
+ update: this.trpc.procedure
+ .input(TaxonomyMethodSchema.update)
+ .mutation(async ({ input }) => {
+ return this.taxonomyService.update(input);
+ }),
+ delete: this.trpc.procedure
+ .input(TaxonomyMethodSchema.delete)
+ .mutation(async ({ input }) => {
+ return this.taxonomyService.delete(input);
+ }),
+ deleteMany: this.trpc.procedure
+ .input(TaxonomyMethodSchema.deleteMany)
+ .mutation(async ({ input }) => {
+ return this.taxonomyService.deleteMany(input);
+ }),
+ paginate: this.trpc.procedure
+ .input(TaxonomyMethodSchema.paginate!)
+ .query(async ({ input }) => {
+ return this.taxonomyService.paginate(input);
+ }),
+ getAll: this.trpc.procedure
+ .input(TaxonomyMethodSchema.getAll)
+ .query(async ({ input }) => {
+ return this.taxonomyService.getAll(input);
+ }),
+ });
}
diff --git a/apps/server/src/models/taxonomy/taxonomy.service.ts b/apps/server/src/models/taxonomy/taxonomy.service.ts
index 9655a1d..c43914b 100755
--- a/apps/server/src/models/taxonomy/taxonomy.service.ts
+++ b/apps/server/src/models/taxonomy/taxonomy.service.ts
@@ -1,18 +1,19 @@
import { Injectable } from '@nestjs/common';
-import { db, TaxonomySchema, z } from '@nicestack/common';
-import { RedisService } from '@server/redis/redis.service';
+import { db, TaxonomyMethodSchema, Prisma } from '@nicestack/common';
+import { redis } from '@server/utils/redis/redis.service';
+import { deleteByPattern } from '@server/utils/redis/utils';
import { TRPCError } from '@trpc/server';
+import { z } from 'zod';
@Injectable()
export class TaxonomyService {
- constructor(private readonly redis: RedisService) {}
+ constructor() { }
/**
* 清除分页缓存,删除所有以'taxonomies:page:'开头的键
*/
private async invalidatePaginationCache() {
- const keys = await this.redis.keys('taxonomies:page:*');
- await Promise.all(keys.map((key) => this.redis.deleteKey(key)));
+ deleteByPattern('taxonomies:page:*')
}
/**
@@ -20,7 +21,7 @@ export class TaxonomyService {
* @param input 分类创建信息
* @returns 新创建的分类记录
*/
- async create(input: z.infer) {
+ async create(input: z.infer) {
// 获取当前分类数量,设置新分类的order值为count + 1
const count = await db.taxonomy.count();
const taxonomy = await db.taxonomy.create({
@@ -28,7 +29,7 @@ export class TaxonomyService {
});
// 删除该分类的缓存及分页缓存
- await this.redis.deleteKey(`taxonomy:${taxonomy.id}`);
+ await redis.del(`taxonomy:${taxonomy.id}`);
await this.invalidatePaginationCache();
return taxonomy;
}
@@ -38,16 +39,29 @@ export class TaxonomyService {
* @param input 包含分类name的对象
* @returns 查找到的分类记录
*/
- async findByName(input: z.infer) {
+ async findByName(input: z.infer) {
const { name } = input;
const cacheKey = `taxonomy:${name}`;
- let cachedTaxonomy = await this.redis.getValue(cacheKey);
+ let cachedTaxonomy = await redis.get(cacheKey);
if (cachedTaxonomy) {
return JSON.parse(cachedTaxonomy);
}
- const taxonomy = await db.taxonomy.findUnique({ where: { name: name } });
+ const taxonomy = await db.taxonomy.findUnique({ where: { name } });
if (taxonomy) {
- await this.redis.setWithExpiry(cacheKey, JSON.stringify(taxonomy), 60);
+ await redis.setex(cacheKey, 60, JSON.stringify(taxonomy));
+ }
+ return taxonomy;
+ }
+ async findBySlug(input: z.infer) {
+ const { slug } = input;
+ const cacheKey = `taxonomy-slug:${slug}`;
+ let cachedTaxonomy = await redis.get(cacheKey);
+ if (cachedTaxonomy) {
+ return JSON.parse(cachedTaxonomy);
+ }
+ const taxonomy = await db.taxonomy.findUnique({ where: { slug } });
+ if (taxonomy) {
+ await redis.setex(cacheKey, 60, JSON.stringify(taxonomy));
}
return taxonomy;
}
@@ -56,15 +70,15 @@ export class TaxonomyService {
* @param input 包含分类ID的对象
* @returns 查找到的分类记录
*/
- async findById(input: z.infer) {
+ async findById(input: z.infer) {
const cacheKey = `taxonomy:${input.id}`;
- let cachedTaxonomy = await this.redis.getValue(cacheKey);
+ let cachedTaxonomy = await redis.get(cacheKey);
if (cachedTaxonomy) {
return JSON.parse(cachedTaxonomy);
}
const taxonomy = await db.taxonomy.findUnique({ where: { id: input.id } });
if (taxonomy) {
- await this.redis.setWithExpiry(cacheKey, JSON.stringify(taxonomy), 60);
+ await redis.setex(cacheKey, 60, JSON.stringify(taxonomy));
}
return taxonomy;
}
@@ -79,7 +93,7 @@ export class TaxonomyService {
const updatedTaxonomy = await db.taxonomy.update({ where: { id }, data });
// 删除该分类的缓存及分页缓存
- await this.redis.deleteKey(`taxonomy:${updatedTaxonomy.id}`);
+ await redis.del(`taxonomy:${updatedTaxonomy.id}`);
await this.invalidatePaginationCache();
return updatedTaxonomy;
}
@@ -96,7 +110,7 @@ export class TaxonomyService {
});
// 删除该分类的缓存及分页缓存
- await this.redis.deleteKey(`taxonomy:${deletedTaxonomy.id}`);
+ await redis.del(`taxonomy:${deletedTaxonomy.id}`);
await this.invalidatePaginationCache();
return deletedTaxonomy;
}
@@ -106,7 +120,7 @@ export class TaxonomyService {
* @param input 包含要删除的分类ID数组的对象
* @returns 删除操作结果,包括删除的记录数
*/
- async batchDelete(input: any) {
+ async deleteMany(input: any) {
const { ids } = input;
if (!ids || ids.length === 0) {
throw new TRPCError({
@@ -120,16 +134,9 @@ export class TaxonomyService {
},
data: { deletedAt: new Date() },
});
- if (!deletedTaxonomies.count) {
- throw new TRPCError({
- code: 'NOT_FOUND',
- message: 'No taxonomies were found with the provided IDs.',
- });
- }
-
// 删除每个分类的缓存及分页缓存
await Promise.all(
- ids.map(async (id: string) => this.redis.deleteKey(`taxonomy:${id}`)),
+ ids.map(async (id: string) => redis.del(`taxonomy:${id}`)),
);
await this.invalidatePaginationCache();
return { success: true, count: deletedTaxonomies.count };
@@ -142,7 +149,7 @@ export class TaxonomyService {
*/
async paginate(input: any) {
const cacheKey = `taxonomies:page:${input.page}:size:${input.pageSize}`;
- let cachedData = await this.redis.getValue(cacheKey);
+ let cachedData = await redis.get(cacheKey);
if (cachedData) {
return JSON.parse(cachedData);
}
@@ -159,7 +166,7 @@ export class TaxonomyService {
const result = { items, totalCount };
// 缓存结果并设置过期时间
- await this.redis.setWithExpiry(cacheKey, JSON.stringify(result), 60);
+ await redis.setex(cacheKey, 60, JSON.stringify(result));
return result;
}
@@ -167,10 +174,30 @@ export class TaxonomyService {
* 获取所有未删除的分类记录
* @returns 分类记录列表
*/
- async getAll() {
+ async getAll(input: z.infer) {
+ const { type } = input;
+ let filter: Prisma.TaxonomyWhereInput = {
+ deletedAt: null,
+ };
+
+ if (type !== undefined) {
+ filter = {
+ ...filter,
+ OR: [
+ { objectType: { has: type } }, // objectType 包含 type
+ ],
+ };
+ }
return db.taxonomy.findMany({
- where: { deletedAt: null },
+ where: filter,
orderBy: { order: 'asc' },
+ select: {
+ name: true,
+ id: true,
+ slug: true,
+ objectType: true,
+ order: true,
+ }
});
}
}
diff --git a/apps/server/src/models/term/term.controller.ts b/apps/server/src/models/term/term.controller.ts
new file mode 100755
index 0000000..12a165d
--- /dev/null
+++ b/apps/server/src/models/term/term.controller.ts
@@ -0,0 +1,28 @@
+import { Controller, Get, Query, UseGuards } from '@nestjs/common';
+
+import { TermService } from './term.service';
+import { AuthGuard } from '@server/auth/auth.guard';
+import { db } from '@nicestack/common';
+
+@Controller('term')
+export class TermController {
+ constructor(private readonly termService: TermService) {}
+ @UseGuards(AuthGuard)
+ @Get('get-tree-data')
+ async getTreeData(@Query('tax-id') taxId: string) {
+ try {
+ const result = await this.termService.getTreeData({ taxonomyId: taxId });
+ return {
+ data: result,
+ errmsg: 'success',
+ errno: 0,
+ };
+ } catch (e) {
+ return {
+ data: {},
+ errmsg: (e as any)?.message || 'error',
+ errno: 1,
+ };
+ }
+ }
+}
diff --git a/apps/server/src/models/term/term.module.ts b/apps/server/src/models/term/term.module.ts
index 3017928..850dbc1 100755
--- a/apps/server/src/models/term/term.module.ts
+++ b/apps/server/src/models/term/term.module.ts
@@ -3,13 +3,14 @@ import { TermService } from './term.service';
import { TermRouter } from './term.router';
import { TrpcService } from '@server/trpc/trpc.service';
import { DepartmentModule } from '../department/department.module';
-import { RbacModule } from '@server/rbac/rbac.module';
-import { RelationService } from '@server/relation/relation.service';
-
+import { TermController } from './term.controller';
+import { RoleMapModule } from '../rbac/rbac.module';
+import { TermRowService } from './term.row.service';
@Module({
- imports: [DepartmentModule, RbacModule],
- providers: [TermService, TermRouter, TrpcService, RelationService],
- exports: [TermService, TermRouter]
+ imports: [DepartmentModule, RoleMapModule],
+ providers: [TermService, TermRouter, TrpcService, TermRowService],
+ exports: [TermService, TermRouter],
+ controllers: [TermController],
})
export class TermModule { }
diff --git a/apps/server/src/models/term/term.router.ts b/apps/server/src/models/term/term.router.ts
index 48eb10d..ba44e35 100755
--- a/apps/server/src/models/term/term.router.ts
+++ b/apps/server/src/models/term/term.router.ts
@@ -1,55 +1,83 @@
import { Injectable } from '@nestjs/common';
import { TrpcService } from '@server/trpc/trpc.service';
import { TermService } from './term.service'; // Adjust the import path as necessary
-import { z, TermSchema } from '@nicestack/common';
-
+import { Prisma, TermMethodSchema, UpdateOrderSchema } from '@nicestack/common';
+import { z, ZodType } from 'zod';
+import { TermRowService } from './term.row.service';
+const TermCreateArgsSchema: ZodType = z.any();
+const TermUpdateArgsSchema: ZodType = z.any();
+const TermFindFirstArgsSchema: ZodType = z.any();
+const TermFindManyArgsSchema: ZodType = z.any();
@Injectable()
export class TermRouter {
- constructor(
- private readonly trpc: TrpcService,
- private readonly termService: TermService,
- ) { }
- router = this.trpc.router({
- create: this.trpc.protectProcedure
- .input(TermSchema.create)
- .mutation(async ({ input, ctx }) => {
- const { staff } = ctx
- return this.termService.create(staff, input);
- }),
- update: this.trpc.protectProcedure
- .input(TermSchema.update)
- .mutation(async ({ input }) => {
- return this.termService.update(input);
- }),
- delete: this.trpc.protectProcedure
- .input(TermSchema.delete)
- .mutation(async ({ input }) => {
- return this.termService.delete(input);
- }),
- findById: this.trpc.procedure.input(z.object({
- id: z.string()
- })).query(async ({ input, ctx }) => {
- const { staff } = ctx
- return this.termService.findUnique(staff, input.id)
+ constructor(
+ private readonly trpc: TrpcService,
+ private readonly termService: TermService,
+ private readonly termRowService: TermRowService,
+ ) {}
+ router = this.trpc.router({
+ create: this.trpc.protectProcedure
+ .input(TermCreateArgsSchema)
+ .mutation(async ({ input, ctx }) => {
+ const { staff } = ctx;
+ return this.termService.create(input, { staff });
+ }),
+ update: this.trpc.protectProcedure
+ .input(TermUpdateArgsSchema)
+ .mutation(async ({ input }) => {
+ return this.termService.update(input);
+ }),
+ findMany: this.trpc.procedure
+ .input(TermFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
+ .query(async ({ input }) => {
+ return await this.termService.findMany(input);
+ }),
+ findFirst: this.trpc.procedure
+ .input(TermFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword
+ .query(async ({ input }) => {
+ return await this.termService.findFirst(input);
+ }),
+ softDeleteByIds: this.trpc.protectProcedure
+ .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema
+ .mutation(async ({ input }) => {
+ return this.termService.softDeleteByIds(input.ids);
+ }),
+ updateOrder: this.trpc.protectProcedure
+ .input(UpdateOrderSchema)
+ .mutation(async ({ input }) => {
+ return this.termService.updateOrder(input);
+ }),
+ upsertTags: this.trpc.protectProcedure
+ .input(
+ z.object({
+ tags: z.array(z.string()),
}),
- batchDelete: this.trpc.protectProcedure.input(z.object({
- ids: z.array(z.string())
- })).mutation(async ({ input }) => {
- const { ids } = input
- return this.termService.batchDelete(ids)
- }),
- getChildren: this.trpc.procedure.input(TermSchema.getChildren).query(async ({ input, ctx }) => {
- const { staff } = ctx
- return this.termService.getChildren(staff, input)
- }),
- getAllChildren: this.trpc.procedure.input(TermSchema.getChildren).query(async ({ input, ctx }) => {
- const { staff } = ctx
- return this.termService.getAllChildren(staff, input)
- }),
- findMany: this.trpc.procedure
- .input(TermSchema.findMany) // Assuming StaffSchema.findMany is the Zod schema for finding staffs by keyword
- .query(async ({ input }) => {
- return await this.termService.findMany(input);
- }),
- });
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { staff } = ctx;
+ return this.termService.upsertTags(staff, input.tags);
+ }),
+ getChildSimpleTree: this.trpc.procedure
+ .input(TermMethodSchema.getSimpleTree)
+ .query(async ({ input, ctx }) => {
+ const { staff } = ctx;
+ return await this.termService.getChildSimpleTree(staff, input);
+ }),
+ getParentSimpleTree: this.trpc.procedure
+ .input(TermMethodSchema.getSimpleTree)
+ .query(async ({ input, ctx }) => {
+ const { staff } = ctx;
+ return await this.termService.getParentSimpleTree(staff, input);
+ }),
+ getTreeData: this.trpc.protectProcedure
+ .input(TermMethodSchema.getTreeData)
+ .query(async ({ input }) => {
+ return await this.termService.getTreeData(input);
+ }),
+ getRows: this.trpc.protectProcedure
+ .input(TermMethodSchema.getRows)
+ .query(async ({ input, ctx }) => {
+ return await this.termRowService.getRows(input, ctx.staff);
+ }),
+ });
}
diff --git a/apps/server/src/models/term/term.row.service.ts b/apps/server/src/models/term/term.row.service.ts
new file mode 100644
index 0000000..a518f9b
--- /dev/null
+++ b/apps/server/src/models/term/term.row.service.ts
@@ -0,0 +1,91 @@
+import { Injectable } from '@nestjs/common';
+import {
+ ObjectType,
+ RowModelRequest,
+ TermMethodSchema,
+ UserProfile,
+} from '@nicestack/common';
+import { date, z } from 'zod';
+import { RowCacheService } from '../base/row-cache.service';
+import { isFieldCondition } from '../base/sql-builder';
+
+@Injectable()
+export class TermRowService extends RowCacheService {
+ constructor() {
+ super(ObjectType.TERM, false);
+ }
+ createUnGroupingRowSelect(
+ requset: z.infer,
+ ): string[] {
+ const result = super.createUnGroupingRowSelect(requset).concat([
+ `${this.tableName}.name AS name`,
+ `${this.tableName}.order AS order`,
+ `${this.tableName}.has_children AS has_children`,
+ `${this.tableName}.parent_id AS parent_id`,
+ `${this.tableName}.domain_id AS domain_id`,
+ `taxonomy.name AS taxonomy_name`,
+ `taxonomy.id AS taxonomy_id`
+ ]);
+ return result;
+ }
+ createJoinSql(request?: RowModelRequest): string[] {
+ return [
+ `LEFT JOIN taxonomy ON ${this.tableName}.taxonomy_id = taxonomy.id`
+ ];
+ }
+ protected createGetRowsFilters(
+ request: z.infer,
+ staff: UserProfile,
+ ) {
+ const condition = super.createGetRowsFilters(request);
+ const { parentId, domainId, includeDeleted = false, taxonomyId } = request;
+ if (isFieldCondition(condition)) {
+ return;
+ }
+
+ if (request.groupKeys.length === 0) {
+ if (parentId) {
+ condition.AND.push({
+ field: `${this.tableName}.parent_id`,
+ value: parentId,
+ op: 'equals',
+ });
+ } else if (parentId === null) {
+ condition.AND.push({
+ field: `${this.tableName}.parent_id`,
+ op: "blank",
+ });
+ }
+ }
+ if (domainId) {
+ condition.AND.push({
+ field: `${this.tableName}.domain_id`,
+ value: domainId,
+ op: 'equals',
+ });
+ } else if (domainId === null) {
+ condition.AND.push({
+ field: `${this.tableName}.domain_id`,
+ op: "blank",
+ });
+ }
+ if (taxonomyId) {
+ condition.AND.push({
+ field: `${this.tableName}.taxonomy_id`,
+ value: taxonomyId,
+ op: 'equals',
+ });
+ }
+ if (!includeDeleted) {
+ condition.AND.push({
+ field: `${this.tableName}.deleted_at`,
+ type: 'date',
+ op: 'blank',
+ });
+ }
+
+
+ return condition;
+ }
+
+}
diff --git a/apps/server/src/models/term/term.service.ts b/apps/server/src/models/term/term.service.ts
index 96d6719..c8ce22f 100755
--- a/apps/server/src/models/term/term.service.ts
+++ b/apps/server/src/models/term/term.service.ts
@@ -1,367 +1,425 @@
import { Injectable } from '@nestjs/common';
-import { z, TermSchema, db, Staff, Term, RelationType, ObjectType, Prisma, TermDto } from '@nicestack/common';
-import { RolePermsService } from '@server/rbac/roleperms.service';
-import { RelationService } from '@server/relation/relation.service';
+import {
+ TermMethodSchema,
+ db,
+ Staff,
+ Term,
+ Prisma,
+ TermDto,
+ TreeDataNode,
+ UserProfile,
+ getUniqueItems,
+ RolePerms,
+ TaxonomySlug,
+ ObjectType,
+ TermAncestry,
+} from '@nicestack/common';
+import { z } from 'zod';
+import { BaseTreeService } from '../base/base.tree.service';
+import EventBus, { CrudOperation } from '@server/utils/event-bus';
+import { formatToTermTreeData, mapToTermSimpleTree } from './utils';
-/**
- * Service for managing terms and their ancestries.
- */
@Injectable()
-export class TermService {
- constructor(private readonly permissionService: RolePermsService, private readonly relations: RelationService) { }
+export class TermService extends BaseTreeService {
+ constructor() {
+ super(db, ObjectType.TERM, 'termAncestry', true);
+ }
- /**
- * 生成TermDto对象,包含权限和关系信息
- * @param staff 当前操作的工作人员
- * @param term 当前处理的术语对象,包含其子节点
- * @returns 完整的TermDto对象
- */
- async genTermDto(staff: Staff, term: Term & { children: Term[] }): Promise {
- const { children, ...others } = term as any;
- const permissions = this.permissionService.getTermPerms(staff, term);
- const relationTypes = [
- { type: RelationType.WATCH, object: ObjectType.DEPARTMENT, key: 'watchDeptIds', limit: undefined },
- { type: RelationType.WATCH, object: ObjectType.STAFF, key: 'watchStaffIds', limit: undefined }
- ] as const;
+ async create(args: Prisma.TermCreateArgs, params?: { staff?: UserProfile }) {
+ args.data.createdBy = params?.staff?.id;
+ const result = await super.create(args);
+ EventBus.emit('dataChanged', {
+ type: this.objectType,
+ operation: CrudOperation.CREATED,
+ data: result,
+ });
+ return result;
+ }
+ async update(args: Prisma.TermUpdateArgs) {
+ const result = await super.update(args);
+ EventBus.emit('dataChanged', {
+ type: this.objectType,
+ operation: CrudOperation.UPDATED,
+ data: result,
+ });
+ return result;
+ }
- type RelationResult = {
- [key in typeof relationTypes[number]['key']]: string[];
- };
+ /**
+ * 删除现有单位并清理DeptAncestry关系。
+ * @param data - 用于删除现有单位的数据。
+ * @returns 删除的单位对象。
+ */
+ async softDeleteByIds(ids: string[]) {
+ const descendantIds = await this.getDescendantIds(ids, true);
+ const result = await super.softDeleteByIds(descendantIds);
+ EventBus.emit('dataChanged', {
+ type: this.objectType,
+ operation: CrudOperation.DELETED,
+ data: result,
+ });
+ return result;
+ }
- const promises = relationTypes.map(async ({ type, object, key, limit }) => ({
- [key]: await this.relations.getEROBids(ObjectType.TERM, type, object, term.id, limit)
- }));
+ // async query(data: z.infer) {
+ // const { limit = 10, initialIds, taxonomyId, taxonomySlug } = data;
+ // // Fetch additional objects excluding initialIds
+ // const ids =
+ // typeof initialIds === 'string' ? [initialIds] : initialIds || [];
+ // const initialTerms = await db.term.findMany({
+ // where: {
+ // id: {
+ // in: ids,
+ // },
+ // },
+ // include: {
+ // domain: true,
+ // children: true,
+ // },
+ // });
+ // const terms = await db.term.findMany({
+ // where: {
+ // taxonomyId,
+ // taxonomy: taxonomySlug && { slug: taxonomySlug },
+ // deletedAt: null,
+ // },
+ // take: limit !== -1 ? limit! : undefined,
+ // include: {
+ // domain: true,
+ // taxonomy: true,
+ // },
+ // orderBy: [{ order: 'asc' }, { createdAt: 'desc' }],
+ // });
- const results = await Promise.all(promises);
- const mergedResults = Object.assign({}, ...(results as Partial[]));
+ // const results = getUniqueItems(
+ // [...initialTerms, ...terms].filter(Boolean),
+ // 'id',
+ // );
+ // return results;
+ // }
+ // /**
- return { ...others, ...mergedResults, permissions, hasChildren: term.children.length > 0 };
+ async upsertTags(staff: UserProfile, tags: string[]) {
+ const tagTax = await db.taxonomy.findFirst({
+ where: {
+ slug: TaxonomySlug.TAG,
+ },
+ });
+ // 批量查找所有存在的标签
+ const existingTerms = await db.term.findMany({
+ where: {
+ name: {
+ in: tags,
+ },
+ taxonomyId: tagTax.id,
+ },
+ });
+
+ // 找出不存在的标签
+ const existingTagNames = new Set(existingTerms.map((term) => term.name));
+ const newTags = tags.filter((tag) => !existingTagNames.has(tag));
+ // 批量创建不存在的标签
+ const newTerms = await Promise.all(
+ newTags.map((tag) =>
+ this.create({
+ data: {
+ name: tag,
+ taxonomyId: tagTax.id,
+ domainId: staff.domainId,
+ },
+ }),
+ ),
+ );
+
+ // 合并现有标签和新创建的标签
+ return [...existingTerms, ...newTerms];
+ }
+
+ // /**
+ // * 查找多个术语并生成TermDto对象。
+ // *
+ // * @param staff 当前操作的工作人员
+ // * @param ids 术语ID
+ // * @returns 包含详细信息的术语对象
+ // */
+ // async findByIds(ids: string[], staff?: UserProfile) {
+ // const terms = await db.term.findMany({
+ // where: {
+ // id: {
+ // in: ids,
+ // },
+ // },
+ // include: {
+ // domain: true,
+ // children: true,
+ // },
+ // });
+ // return await Promise.all(
+ // terms.map(async (term) => {
+ // return await this.transformDto(term, staff);
+ // }),
+ // );
+ // }
+ // /**
+ // * 获取指定条件下的术语子节点。
+ // *
+ // * @param staff 当前操作的工作人员
+ // * @param data 查询条件
+ // * @returns 子节点术语列表
+ // */
+ // async getChildren(
+ // staff: UserProfile,
+ // data: z.infer,
+ // ) {
+ // const { parentId, domainId, taxonomyId, cursor, limit = 10 } = data;
+ // let queryCondition: Prisma.TermWhereInput = {
+ // taxonomyId,
+ // parentId: parentId,
+ // OR: [{ domainId: null }],
+ // deletedAt: null,
+ // };
+ // if (
+ // staff?.permissions?.includes(RolePerms.MANAGE_ANY_TERM) ||
+ // staff?.permissions?.includes(RolePerms.READ_ANY_TERM)
+ // ) {
+ // queryCondition.OR = undefined;
+ // } else {
+ // queryCondition.OR = queryCondition.OR.concat([
+ // { domainId: staff?.domainId },
+ // { domainId: null },
+ // ]);
+ // }
+ // const terms = await db.term.findMany({
+ // where: queryCondition,
+ // include: {
+ // children: {
+ // where: {
+ // deletedAt: null,
+ // },
+ // },
+ // },
+ // take: limit + 1,
+ // cursor: cursor
+ // ? { createdAt: cursor.split('_')[0], id: cursor.split('_')[1] }
+ // : undefined,
+ // });
+ // let nextCursor: typeof cursor | undefined = undefined;
+ // if (terms.length > limit) {
+ // const nextItem = terms.pop();
+ // nextCursor = `${nextItem.createdAt.toISOString()}_${nextItem!.id}`;
+ // }
+ // const termDtos = await Promise.all(
+ // terms.map((item) => this.transformDto(item, staff)),
+ // );
+ // return {
+ // items: termDtos,
+ // nextCursor,
+ // };
+ // }
+
+ async getTreeData(data: z.infer) {
+ const { taxonomyId, taxonomySlug, domainId } = data;
+ let terms = [];
+ if (taxonomyId) {
+ terms = await db.term.findMany({
+ where: { taxonomyId, domainId, deletedAt: null },
+ include: { children: true },
+ orderBy: [{ order: 'asc' }],
+ });
+ } else if (taxonomySlug) {
+ terms = await db.term.findMany({
+ where: {
+ taxonomy: {
+ slug: taxonomySlug,
+ },
+ deletedAt: null,
+ domainId,
+ },
+ include: { children: true },
+ orderBy: [{ order: 'asc' }],
+ });
}
-
- /**
- * 获取特定父节点下新的排序值。
- *
- * @param parentId 父节点ID,如果是根节点则为null
- * @returns 下一个排序值
- */
- private async getNextOrder(parentId?: string) {
- let newOrder = 0;
-
- if (parentId) {
- const siblingTerms = await db.term.findMany({
- where: { parentId },
- orderBy: { order: 'desc' },
- take: 1,
- });
-
- if (siblingTerms.length > 0) {
- newOrder = siblingTerms[0].order + 1;
- }
- } else {
- const rootTerms = await db.term.findMany({
- where: { parentId: null },
- orderBy: { order: 'desc' },
- take: 1,
- });
-
- if (rootTerms.length > 0) {
- newOrder = rootTerms[0].order + 1;
- }
+ // Map to store terms by id for quick lookup
+ const termMap = new Map();
+ terms.forEach((term) =>
+ termMap.set(term.id, {
+ ...term,
+ children: [],
+ key: term.id,
+ value: term.id,
+ title: term.name,
+ isLeaf: true, // Initialize as true, will update later if it has children
+ }),
+ );
+ // Root nodes collection
+ const roots = [];
+ // Build the tree structure iteratively
+ terms.forEach((term) => {
+ if (term.parentId) {
+ const parent = termMap.get(term.parentId);
+ if (parent) {
+ parent.children.push(termMap.get(term.id));
+ parent.isLeaf = false; // Update parent's isLeaf field
}
+ } else {
+ roots.push(termMap.get(term.id));
+ }
+ });
+ return roots as TreeDataNode[];
+ }
- return newOrder;
- }
+ async getChildSimpleTree(
+ staff: UserProfile,
+ data: z.infer,
+ ) {
+ const { domainId = null, permissions } = staff;
+ const hasAnyPerms =
+ staff?.permissions?.includes(RolePerms.MANAGE_ANY_TERM) ||
+ staff?.permissions?.includes(RolePerms.READ_ANY_TERM);
+ const { termIds, parentId, taxonomyId } = data;
+ // 提取非空 deptIds
+ const validTermIds = termIds?.filter((id) => id !== null) ?? [];
+ const hasNullTermId = termIds?.includes(null) ?? false;
- /**
- * 创建关系数据用于批量插入。
- *
- * @param termId 术语ID
- * @param watchDeptIds 监控部门ID数组
- * @param watchStaffIds 监控员工ID数组
- * @returns 关系数据数组
- */
- private createRelations(
- termId: string,
- watchDeptIds: string[],
- watchStaffIds: string[]
- ) {
- const relationsData = [
- ...watchDeptIds.map(bId => this.relations.buildRelation(termId, bId, ObjectType.TERM, ObjectType.DEPARTMENT, RelationType.WATCH)),
- ...watchStaffIds.map(bId => this.relations.buildRelation(termId, bId, ObjectType.TERM, ObjectType.STAFF, RelationType.WATCH)),
- ];
- return relationsData;
- }
-
- /**
- * 创建一个新的术语并根据需要创建祖先关系。
- *
- * @param data 创建新术语的数据
- * @returns 新创建的术语
- */
- async create(staff: Staff, data: z.infer) {
- const { parentId, watchDeptIds = [], watchStaffIds = [], ...others } = data;
-
- return await db.$transaction(async (trx) => {
- const order = await this.getNextOrder(parentId);
-
- const newTerm = await trx.term.create({
- data: {
- ...others,
- parentId,
- order,
- createdBy: staff.id
- },
- });
-
- if (parentId) {
- const parentTerm = await trx.term.findUnique({
- where: { id: parentId },
- include: { ancestors: true },
- });
-
- const ancestries = parentTerm.ancestors.map((ancestor) => ({
- ancestorId: ancestor.ancestorId,
- descendantId: newTerm.id,
- relDepth: ancestor.relDepth + 1,
- }));
-
- ancestries.push({
- ancestorId: parentTerm.id,
- descendantId: newTerm.id,
- relDepth: 1,
- });
-
- await trx.termAncestry.createMany({ data: ancestries });
- }
-
- const relations = this.createRelations(newTerm.id, watchDeptIds, watchStaffIds);
- await trx.relation.createMany({ data: relations });
- return newTerm;
- });
- }
-
- /**
- * 更新现有术语的数据,并在parentId改变时管理术语祖先关系。
- *
- * @param data 更新术语的数据
- * @returns 更新后的术语
- */
- async update(data: z.infer) {
- return await db.$transaction(async (prisma) => {
- const currentTerm = await prisma.term.findUnique({
- where: { id: data.id },
- });
- if (!currentTerm) throw new Error('Term not found');
- console.log(data)
- const updatedTerm = await prisma.term.update({
- where: { id: data.id },
- data,
- });
-
- if (data.parentId !== currentTerm.parentId) {
- await prisma.termAncestry.deleteMany({
- where: { descendantId: data.id },
- });
-
- if (data.parentId) {
- const parentAncestries = await prisma.termAncestry.findMany({
- where: { descendantId: data.parentId },
- });
-
- const newAncestries = parentAncestries.map(ancestry => ({
- ancestorId: ancestry.ancestorId,
- descendantId: data.id,
- relDepth: ancestry.relDepth + 1,
- }));
-
- newAncestries.push({
- ancestorId: data.parentId,
- descendantId: data.id,
- relDepth: 1,
- });
-
- await prisma.termAncestry.createMany({
- data: newAncestries,
- });
-
- const order = await this.getNextOrder(data.parentId);
- await prisma.term.update({
- where: { id: data.id },
- data: { order },
- });
- }
- }
-
- if (data.watchDeptIds || data.watchStaffIds) {
- await prisma.relation.deleteMany({ where: { aId: data.id, relationType: { in: [RelationType.WATCH] } } });
-
- const relations = this.createRelations(
- data.id,
- data.watchDeptIds ?? [],
- data.watchStaffIds ?? []
- );
-
- await prisma.relation.createMany({ data: relations });
- }
-
- return updatedTerm;
- });
- }
-
- /**
- * 根据ID删除现有术语。
- *
- * @param data 删除术语的数据
- * @returns 被删除的术语
- */
- async delete(data: z.infer) {
- const { id } = data;
-
- await db.termAncestry.deleteMany({
- where: { OR: [{ ancestorId: id }, { descendantId: id }] },
- });
-
- const deletedTerm = await db.term.update({
- where: { id },
- data: {
- deletedAt: new Date(),
- },
- });
-
- return deletedTerm;
- }
-
- /**
- * 批量删除术语。
- *
- * @param ids 要删除的术语ID数组
- * @returns 已删除的术语列表
- */
- async batchDelete(ids: string[]) {
- await db.termAncestry.deleteMany({
- where: { OR: [{ ancestorId: { in: ids } }, { descendantId: { in: ids } }] },
- });
-
- const deletedTerms = await db.term.updateMany({
- where: { id: { in: ids } },
- data: {
- deletedAt: new Date(),
- }
- });
-
- return deletedTerms;
- }
-
- /**
- * 查找唯一术语并生成TermDto对象。
- *
- * @param staff 当前操作的工作人员
- * @param id 术语ID
- * @returns 包含详细信息的术语对象
- */
- async findUnique(staff: Staff, id: string) {
- const term = await db.term.findUnique({
+ const [childrenData, selfData] = await Promise.all([
+ db.termAncestry.findMany({
+ where: {
+ ...(termIds && {
+ OR: [
+ ...(validTermIds.length
+ ? [{ ancestorId: { in: validTermIds } }]
+ : []),
+ ...(hasNullTermId ? [{ ancestorId: null }] : []),
+ ],
+ }),
+ descendant: {
+ taxonomyId: taxonomyId,
+ // 动态权限控制条件
+ ...(hasAnyPerms
+ ? {} // 当有全局权限时,不添加任何额外条件
+ : {
+ // 当无全局权限时,添加域ID过滤
+ OR: [
+ { domainId: null }, // 通用记录
+ { domainId: domainId }, // 特定域记录
+ ],
+ }),
+ },
+ ancestorId: parentId,
+ relDepth: 1,
+ },
+ include: {
+ descendant: { include: { children: true } },
+ },
+ orderBy: { descendant: { order: 'asc' } },
+ }),
+ termIds
+ ? db.term.findMany({
where: {
- id,
- },
- include: {
- domain: true,
- children: true,
- },
- });
- return await this.genTermDto(staff, term);
- }
-
- /**
- * 获取指定条件下的术语子节点。
- *
- * @param staff 当前操作的工作人员
- * @param data 查询条件
- * @returns 子节点术语列表
- */
- async getChildren(staff: Staff, data: z.infer