From 76f266645db4b67d976c6e43702f57d9a41a7b8c Mon Sep 17 00:00:00 2001 From: daiyongkang Date: Wed, 2 Sep 2026 10:04:03 +0000 Subject: [PATCH] =?UTF-8?q?Initial=20commit:=20=E9=A3=8E=E5=BD=B1=20OA=20?= =?UTF-8?q?=E5=85=A8=E6=A0=88=E6=BA=90=E7=A0=81=EF=BC=88API/Web/Flutter/IM?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor --- .cursor/rules/office-process.mdc | 30 + .cursor/rules/windows-packaging.mdc | 11 + .env.example | 44 + .gitignore | 49 + DELIVERY.md | 661 + Makefile | 14 + README.md | 23 + WINDOWS_BUILD_3.1.4.md | 13 + WINDOWS_BUILD_3.1.7.md | 11 + agconnect-services.json | 1 + apps/api/.prettierrc | 4 + apps/api/README.md | 47 + apps/api/data/app-release.json | 12 + apps/api/eslint.config.mjs | 35 + apps/api/nest-cli.json | 8 + apps/api/package.json | 110 + apps/api/pnpm-lock.yaml | 7234 +++++++++++ apps/api/prisma/backfill-contract-buckets.ts | 121 + apps/api/prisma/backfill-project-ledger.ts | 127 + apps/api/prisma/backfill-report-cc.ts | 79 + apps/api/prisma/backfill-tech-travel.ts | 36 + apps/api/prisma/backfill-won-ledger.ts | 88 + apps/api/prisma/ensure-prod-admin.ts | 65 + apps/api/prisma/fix-approval-fields.ts | 141 + apps/api/prisma/fix-bid-from-oa.ts | 125 + apps/api/prisma/fix-expense-applicants.ts | 125 + apps/api/prisma/fix-expense-lines.ts | 107 + apps/api/prisma/fix-oa-posts.ts | 141 + apps/api/prisma/grant-admin-equivalents.ts | 40 + apps/api/prisma/grant-sun-chunqing.ts | 41 + apps/api/prisma/import-hr.ts | 587 + apps/api/prisma/import-notices-from-oa.ts | 306 + apps/api/prisma/import-prod-complete.ts | 371 + apps/api/prisma/import-prod.ts | 475 + apps/api/prisma/import-salary-from-oa.ts | 239 + apps/api/prisma/menus.ts | 201 + .../20260829065602_init/migration.sql | 567 + .../migration.sql | 34 + .../migration.sql | 50 + .../migration.sql | 56 + .../migration.sql | 67 + .../migration.sql | 57 + .../20260829110000_seal_office/migration.sql | 19 + .../migration.sql | 134 + .../migration.sql | 98 + .../migration.sql | 13 + .../migration.sql | 29 + .../20260829170000_sms_send_log/migration.sql | 19 + .../migration.sql | 67 + .../migration.sql | 3 + .../migration.sql | 48 + .../migration.sql | 27 + .../migration.sql | 70 + .../migration.sql | 1 + .../migration.sql | 16 + .../migration.sql | 51 + .../migration.sql | 24 + .../migration.sql | 29 + .../migration.sql | 2 + .../migration.sql | 3 + .../migration.sql | 1 + .../20260831140000_push_device/migration.sql | 17 + .../migration.sql | 3 + .../migration.sql | 3 + .../migration.sql | 26 + .../api/prisma/migrations/migration_lock.toml | 3 + apps/api/prisma/oa-json.ts | 23 + ...n-clean-orphan-contract-files-20260901.sql | 38 + .../production-deduplicate-20260901.sql | 111 + ...ction-restore-missing-expense-20260901.sql | 61 + apps/api/prisma/schema.prisma | 1421 +++ apps/api/prisma/seed.ts | 383 + apps/api/prisma/sync-menus.ts | 165 + apps/api/prisma/sync-oa-attachments.ts | 329 + apps/api/prisma/sync-tender-methods.ts | 35 + apps/api/prisma/wash-bid-buckets.ts | 131 + apps/api/prisma/wash-legal-entities.ts | 142 + apps/api/prisma/wash-oa-flow-finance.ts | 466 + apps/api/prisma/wash-oa-ledger.ts | 563 + apps/api/prisma/wash-terminated-winners.ts | 186 + apps/api/src/app.module.ts | 93 + apps/api/src/auth/auth.controller.ts | 199 + apps/api/src/auth/auth.module.ts | 14 + apps/api/src/auth/auth.service.ts | 532 + apps/api/src/auth/jwt.strategy.ts | 71 + apps/api/src/bidding/bid-workflow.spec.ts | 161 + apps/api/src/bidding/bid-workflow.ts | 336 + apps/api/src/bidding/bidding.controller.ts | 389 + apps/api/src/bidding/bidding.service.ts | 1810 +++ apps/api/src/business/business.controller.ts | 62 + apps/api/src/common/data-scope.ts | 105 + .../src/common/decorators/auth.decorators.ts | 8 + .../decorators/current-user.decorator.ts | 18 + apps/api/src/common/doc-no.spec.ts | 15 + apps/api/src/common/doc-no.ts | 84 + apps/api/src/common/dto/page-query.dto.ts | 24 + .../common/filters/http-exception.filter.ts | 84 + apps/api/src/common/guards/auth.guards.ts | 37 + .../interceptors/response.interceptor.ts | 40 + .../middleware/request-id.middleware.ts | 15 + apps/api/src/common/org-chain.spec.ts | 42 + apps/api/src/common/org-chain.ts | 209 + apps/api/src/common/role-groups.ts | 60 + apps/api/src/common/tencent-lbs.ts | 288 + apps/api/src/config/env.spec.ts | 22 + apps/api/src/config/env.ts | 60 + .../src/contract/contract-workflow.spec.ts | 26 + apps/api/src/contract/contract-workflow.ts | 29 + apps/api/src/contract/contract.controller.ts | 102 + apps/api/src/contract/contract.service.ts | 666 + apps/api/src/contract/won-ledger.ts | 106 + apps/api/src/files/files.controller.ts | 58 + apps/api/src/files/files.mime.spec.ts | 19 + apps/api/src/files/files.mime.ts | 77 + apps/api/src/files/files.module.ts | 10 + apps/api/src/files/files.service.ts | 433 + apps/api/src/finance/bank-pay.ts | 49 + apps/api/src/finance/finance-workflow.spec.ts | 39 + apps/api/src/finance/finance-workflow.ts | 94 + apps/api/src/finance/finance.controller.ts | 308 + apps/api/src/finance/finance.service.ts | 1461 +++ apps/api/src/health/health.controller.ts | 58 + apps/api/src/hr/hr-workflow.spec.ts | 109 + apps/api/src/hr/hr-workflow.ts | 429 + apps/api/src/hr/hr.controller.ts | 461 + apps/api/src/hr/hr.service.ts | 1997 +++ apps/api/src/im/im-bus.ts | 22 + apps/api/src/im/im.controller.ts | 202 + apps/api/src/im/im.module.ts | 15 + apps/api/src/im/im.service.ts | 998 ++ apps/api/src/im/voice-asr.service.ts | 178 + apps/api/src/legal/legal.controller.ts | 27 + apps/api/src/legal/legal.docs.ts | 127 + apps/api/src/main.ts | 51 + apps/api/src/master/master.controller.ts | 192 + apps/api/src/master/master.service.ts | 380 + apps/api/src/office/office-apply.spec.ts | 16 + apps/api/src/office/office-apply.ts | 765 ++ apps/api/src/office/office.controller.ts | 740 ++ apps/api/src/office/work-report.ts | 325 + apps/api/src/prisma/prisma.module.ts | 9 + apps/api/src/prisma/prisma.service.ts | 56 + apps/api/src/project/project-workflow.spec.ts | 73 + apps/api/src/project/project-workflow.ts | 190 + apps/api/src/project/project.controller.ts | 273 + apps/api/src/project/project.service.ts | 1300 ++ apps/api/src/push/push.controller.ts | 27 + apps/api/src/push/push.module.ts | 11 + apps/api/src/push/push.service.ts | 439 + apps/api/src/report/report.controller.ts | 36 + apps/api/src/report/report.service.ts | 170 + apps/api/src/seal/seal-workflow.spec.ts | 41 + apps/api/src/seal/seal-workflow.ts | 82 + apps/api/src/seal/seal.controller.ts | 152 + apps/api/src/seal/seal.service.ts | 759 ++ apps/api/src/sms/sms-bus.ts | 64 + apps/api/src/sms/sms-events.ts | 238 + apps/api/src/sms/sms-fill.spec.ts | 37 + apps/api/src/sms/sms-fill.ts | 89 + apps/api/src/sms/sms.controller.ts | 168 + apps/api/src/sms/sms.module.ts | 12 + apps/api/src/sms/sms.service.ts | 806 ++ apps/api/src/system/numbering.ts | 31 + apps/api/src/system/op-log.ts | 41 + apps/api/src/system/ops-workflow.spec.ts | 59 + apps/api/src/system/ops-workflow.ts | 109 + apps/api/src/system/system.controller.ts | 242 + apps/api/src/system/system.service.ts | 366 + apps/api/test/app.e2e-spec.ts | 1450 +++ apps/api/test/jest-e2e.json | 9 + apps/api/tsconfig.build.json | 4 + apps/api/tsconfig.json | 24 + apps/harmony/.gitignore | 10 + apps/harmony/AppScope/app.json5 | 10 + .../resources/base/element/string.json | 5 + .../resources/base/media/app_icon.png | Bin 0 -> 4151 bytes apps/harmony/build-profile.json5 | 49 + apps/harmony/entry/build-profile.json5 | 26 + apps/harmony/entry/hvigorfile.ts | 6 + apps/harmony/entry/oh-package.json5 | 9 + .../main/ets/entryability/EntryAbility.ets | 57 + .../entry/src/main/ets/pages/Index.ets | 65 + .../ets/pushability/PushMessageAbility.ets | 13 + apps/harmony/entry/src/main/module.json5 | 87 + .../main/resources/base/element/color.json | 5 + .../main/resources/base/element/string.json | 11 + .../main/resources/base/media/app_icon.png | Bin 0 -> 4151 bytes .../resources/base/profile/main_pages.json | 5 + .../resources/rawfile/agconnect-services.json | 1 + apps/harmony/hvigor/hvigor-config.json5 | 7 + apps/harmony/hvigorfile.ts | 6 + apps/harmony/oh-package-lock.json5 | 20 + apps/harmony/oh-package.json5 | 8 + apps/im-server/README.md | 10 + apps/im-server/package.json | 13 + apps/im-server/server.js | 324 + apps/native/.gitignore | 46 + apps/native/.metadata | 36 + apps/native/README.md | 55 + apps/native/WINDOWS_BUILD.md | 9 + apps/native/analysis_options.yaml | 34 + apps/native/android/.gitignore | 13 + .../android/app/agconnect-services.json | 1 + apps/native/android/app/build.gradle | 68 + .../android/app/libs/push-internal-5.0.5.aar | Bin 0 -> 100389 bytes apps/native/android/app/proguard-rules.pro | 19 + .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 176 + .../src/main/assets/agconnect-services.json | 1 + .../kotlin/com/fysxkj/fengying_oa/Coord.kt | 43 + .../com/fysxkj/fengying_oa/FengyingApp.kt | 104 + .../fysxkj/fengying_oa/HuaweiPushService.kt | 32 + .../com/fysxkj/fengying_oa/MainActivity.kt | 926 ++ .../fysxkj/fengying_oa/MeizuPushReceiver.kt | 71 + .../kotlin/com/fysxkj/fengying_oa/OemAdapt.kt | 183 + .../fengying_oa/OppoCompatPushService.kt | 11 + .../fengying_oa/OppoDataMessageService.kt | 11 + .../fysxkj/fengying_oa/OppoPushCallback.kt | 34 + .../com/fysxkj/fengying_oa/PushStore.kt | 89 + .../fysxkj/fengying_oa/VivoPushReceiver.kt | 11 + .../kotlin/com/fysxkj/fengying_oa/VoiceAsr.kt | 222 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 5613 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 3763 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 7549 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 11306 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 15787 bytes .../app/src/main/res/raw/fengying_dial.mp3 | Bin 0 -> 462097 bytes .../src/main/res/raw/fengying_incoming.mp3 | Bin 0 -> 200398 bytes .../main/res/raw/fengying_notification.wav | Bin 0 -> 594510 bytes .../app/src/main/res/raw/fengying_send.wav | Bin 0 -> 48462 bytes .../src/main/res/values-night-v35/styles.xml | 11 + .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values-v35/styles.xml | 11 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/main/res/xml/file_paths.xml | 6 + .../app/src/profile/AndroidManifest.xml | 7 + apps/native/android/build.gradle | 27 + apps/native/android/gradle.properties | 8 + .../gradle/wrapper/gradle-wrapper.properties | 5 + apps/native/android/settings.gradle | 34 + .../native/assets/brand/fengying-logo-512.png | Bin 0 -> 25574 bytes apps/native/assets/sounds/ring.wav | Bin 0 -> 52998 bytes apps/native/build-linux.sh | 5 + apps/native/build-windows.bat | 8 + apps/native/build-windows.ps1 | 49 + apps/native/installer/bootstrap-windows.ps1 | 118 + apps/native/installer/fengying-oa.iss | 43 + apps/native/lib/api/oa_client.dart | 358 + apps/native/lib/app_config.dart | 13 + apps/native/lib/app_runtime.dart | 19 + apps/native/lib/desktop/desk_platform.dart | 4 + apps/native/lib/desktop/desk_shell.dart | 369 + apps/native/lib/desktop/desk_theme.dart | 47 + .../desktop/pages/desk_attendance_page.dart | 238 + .../desktop/pages/desk_chat_detail_page.dart | 233 + .../lib/desktop/pages/desk_chat_page.dart | 1011 ++ .../lib/desktop/pages/desk_contacts_page.dart | 279 + .../desktop/pages/desk_conv_list_page.dart | 256 + .../lib/desktop/pages/desk_hr_apply_page.dart | 169 + .../lib/desktop/pages/desk_legal_page.dart | 81 + .../desktop/pages/desk_office_list_page.dart | 132 + .../desktop/pages/desk_org_browse_page.dart | 183 + .../desktop/pages/desk_pick_people_page.dart | 139 + .../desktop/pages/desk_profile_edit_page.dart | 177 + .../lib/desktop/pages/desk_profile_page.dart | 166 + .../pages/desk_record_detail_page.dart | 235 + .../pages/desk_system_notice_page.dart | 125 + .../desktop/pages/desk_work_assign_page.dart | 133 + .../desktop/pages/desk_workbench_page.dart | 398 + .../lib/desktop/widgets/desk_data_table.dart | 112 + .../lib/desktop/widgets/desk_widgets.dart | 573 + apps/native/lib/device/device_bridge.dart | 328 + apps/native/lib/device/push_bridge.dart | 98 + apps/native/lib/im/chat_local_store.dart | 32 + apps/native/lib/im/chat_prefs.dart | 90 + apps/native/lib/im/im_bridge.dart | 38 + apps/native/lib/im/im_lifecycle.dart | 42 + apps/native/lib/im/im_seq_store.dart | 33 + apps/native/lib/im/pinyin.dart | 81 + apps/native/lib/im/protocol.dart | 73 + apps/native/lib/im/sticker_store.dart | 32 + apps/native/lib/im/tcp_client.dart | 300 + apps/native/lib/labels.dart | 407 + apps/native/lib/main.dart | 103 + apps/native/lib/nav/biz_route.dart | 85 + apps/native/lib/nav/notice_route.dart | 45 + apps/native/lib/nav/open_module.dart | 116 + apps/native/lib/ota/updater.dart | 251 + apps/native/lib/pages/approvals_page.dart | 95 + apps/native/lib/pages/attendance_page.dart | 169 + apps/native/lib/pages/calendar_page.dart | 62 + apps/native/lib/pages/call_page.dart | 436 + apps/native/lib/pages/chat_detail_page.dart | 344 + apps/native/lib/pages/chat_page.dart | 2001 +++ apps/native/lib/pages/desktop_login_page.dart | 218 + .../lib/pages/desktop_workbench_page.dart | 295 + apps/native/lib/pages/directory_page.dart | 405 + apps/native/lib/pages/flow_page.dart | 83 + apps/native/lib/pages/hr_apply_page.dart | 193 + apps/native/lib/pages/legal_page.dart | 93 + apps/native/lib/pages/location_pick_page.dart | 262 + apps/native/lib/pages/login_page.dart | 207 + apps/native/lib/pages/messages_page.dart | 575 + apps/native/lib/pages/mine_page.dart | 267 + apps/native/lib/pages/module_list_page.dart | 146 + apps/native/lib/pages/notice_detail_page.dart | 73 + apps/native/lib/pages/notices_page.dart | 67 + apps/native/lib/pages/org_browse_page.dart | 181 + apps/native/lib/pages/pick_people_page.dart | 142 + apps/native/lib/pages/profile_page.dart | 140 + apps/native/lib/pages/record_detail_page.dart | 401 + apps/native/lib/pages/scan_login_page.dart | 152 + apps/native/lib/pages/system_notice_page.dart | 162 + apps/native/lib/pages/todos_page.dart | 91 + apps/native/lib/pages/work_assign_page.dart | 125 + apps/native/lib/pages/workbench_page.dart | 281 + apps/native/lib/session/session.dart | 150 + apps/native/lib/shell/desktop_shell.dart | 1 + apps/native/lib/shell/home_shell.dart | 405 + apps/native/lib/theme.dart | 77 + apps/native/lib/widgets/auth_image.dart | 85 + apps/native/lib/widgets/beian_footer.dart | 43 + apps/native/lib/widgets/common.dart | 123 + apps/native/lib/widgets/desktop_ui.dart | 411 + apps/native/lib/widgets/html_body.dart | 157 + .../lib/widgets/location_view_page.dart | 74 + .../native/lib/widgets/tencent_map_thumb.dart | 65 + apps/native/lib/widgets/voice_message.dart | 122 + apps/native/lib/widgets/wecom.dart | 540 + apps/native/linux/.gitignore | 1 + apps/native/linux/CMakeLists.txt | 128 + apps/native/linux/runner/CMakeLists.txt | 26 + apps/native/linux/runner/main.cc | 6 + apps/native/linux/runner/my_application.cc | 130 + apps/native/linux/runner/my_application.h | 18 + apps/native/packaging/build-deb.sh | 107 + apps/native/pubspec.lock | 986 ++ apps/native/pubspec.yaml | 42 + apps/native/scripts/publish-apk.sh | 18 + apps/native/test/widget_test.dart | 11 + apps/native/windows/.gitignore | 17 + apps/native/windows/CMakeLists.txt | 108 + apps/native/windows/runner/CMakeLists.txt | 40 + apps/native/windows/runner/Runner.rc | 121 + apps/native/windows/runner/flutter_window.cpp | 164 + apps/native/windows/runner/flutter_window.h | 39 + apps/native/windows/runner/main.cpp | 43 + apps/native/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../native/windows/runner/runner.exe.manifest | 14 + apps/native/windows/runner/utils.cpp | 65 + apps/native/windows/runner/utils.h | 19 + apps/native/windows/runner/win32_window.cpp | 288 + apps/native/windows/runner/win32_window.h | 102 + apps/web/.gitignore | 24 + apps/web/README.md | 50 + apps/web/eslint.config.js | 28 + apps/web/index.html | 18 + apps/web/package.json | 47 + apps/web/pnpm-lock.yaml | 3217 +++++ .../brand/fengying-logo-1024-opaque.png | Bin 0 -> 57520 bytes apps/web/public/brand/fengying-logo-1024.png | Bin 0 -> 68262 bytes .../public/brand/fengying-logo-512-opaque.png | Bin 0 -> 22310 bytes apps/web/public/brand/fengying-logo-512.png | Bin 0 -> 25574 bytes apps/web/public/brand/fengying-logo.svg | 15 + apps/web/public/vite.svg | 1 + apps/web/src/App.tsx | 405 + apps/web/src/StatusTag.tsx | 8 + apps/web/src/access.ts | 32 + apps/web/src/api/client.ts | 165 + apps/web/src/assets/react.svg | 1 + apps/web/src/components/AmbientBackground.tsx | 67 + apps/web/src/components/BrandLogo.tsx | 40 + apps/web/src/components/DesktopDownload.tsx | 42 + apps/web/src/components/LinuxDownload.tsx | 53 + apps/web/src/components/MobileDownload.tsx | 48 + apps/web/src/components/SiteFooter.tsx | 17 + apps/web/src/deptTree.ts | 76 + apps/web/src/geo/gcj02.ts | 36 + apps/web/src/index.css | 1264 ++ apps/web/src/labels.ts | 224 + apps/web/src/layouts/AppLayout.tsx | 213 + apps/web/src/main.tsx | 51 + apps/web/src/mammoth-browser.d.ts | 3 + apps/web/src/pages/AppReleasePage.tsx | 87 + apps/web/src/pages/ApplyBucketBar.tsx | 44 + .../web/src/pages/AssetPurchaseLedgerPage.tsx | 55 + apps/web/src/pages/AssetRegistryPage.tsx | 209 + apps/web/src/pages/BidCreatePage.tsx | 286 + apps/web/src/pages/BidDetailPage.tsx | 1151 ++ apps/web/src/pages/BidListPage.tsx | 375 + apps/web/src/pages/BondListPage.tsx | 237 + apps/web/src/pages/CalendarPage.tsx | 75 + apps/web/src/pages/ContractDetailPage.tsx | 591 + apps/web/src/pages/ContractListPage.tsx | 243 + apps/web/src/pages/CostObjectFields.tsx | 138 + apps/web/src/pages/DesktopLoginPage.tsx | 118 + apps/web/src/pages/DirectoryPage.tsx | 55 + apps/web/src/pages/EmployeePage.tsx | 1288 ++ apps/web/src/pages/ExpenseDetailPage.tsx | 482 + apps/web/src/pages/ExpenseInvoiceFields.tsx | 287 + apps/web/src/pages/ExpenseListPage.tsx | 211 + apps/web/src/pages/FileAttachments.tsx | 463 + apps/web/src/pages/FinanceCells.tsx | 70 + apps/web/src/pages/FormDrawer.tsx | 141 + apps/web/src/pages/HrApplyPage.tsx | 304 + apps/web/src/pages/HrLeaveDetailPage.tsx | 91 + apps/web/src/pages/HrNoticePage.tsx | 271 + apps/web/src/pages/HrPages.tsx | 450 + apps/web/src/pages/HrSettingsPage.tsx | 352 + apps/web/src/pages/InvoiceListPage.tsx | 191 + apps/web/src/pages/LaborContractPage.tsx | 218 + apps/web/src/pages/LegalPage.tsx | 60 + apps/web/src/pages/LoanDetailPage.tsx | 226 + apps/web/src/pages/LoginPage.tsx | 156 + apps/web/src/pages/MessagesChannelPage.tsx | 53 + apps/web/src/pages/MyAttendancePage.tsx | 156 + apps/web/src/pages/MyExpensesPage.tsx | 226 + apps/web/src/pages/MyFlowPage.tsx | 135 + apps/web/src/pages/NoticeDetailPage.tsx | 66 + apps/web/src/pages/NoticeHtml.tsx | 64 + apps/web/src/pages/NoticePage.tsx | 64 + apps/web/src/pages/OfficeApplyPage.tsx | 347 + apps/web/src/pages/OfficeApprovalsPage.tsx | 355 + apps/web/src/pages/OfficeSmsPage.tsx | 199 + apps/web/src/pages/OfficeTodosPage.tsx | 135 + apps/web/src/pages/OnboardPage.tsx | 351 + apps/web/src/pages/OpsPages.tsx | 1889 +++ apps/web/src/pages/OrganizationPage.tsx | 195 + apps/web/src/pages/OverviewPage.tsx | 271 + apps/web/src/pages/PartyPages.tsx | 573 + apps/web/src/pages/PdfPreview.tsx | 192 + apps/web/src/pages/ProfileDrawer.tsx | 206 + apps/web/src/pages/ProjectDetailPage.tsx | 915 ++ apps/web/src/pages/ProjectListPage.tsx | 387 + apps/web/src/pages/PunchLocationMap.tsx | 205 + apps/web/src/pages/QualificationListPage.tsx | 357 + apps/web/src/pages/ReportCharts.tsx | 154 + apps/web/src/pages/ResourcePage.tsx | 382 + apps/web/src/pages/RichEditor.tsx | 67 + apps/web/src/pages/SealRequestListPage.tsx | 295 + apps/web/src/pages/SmsPage.tsx | 577 + apps/web/src/pages/StatusPages.tsx | 30 + apps/web/src/pages/TaskListPage.tsx | 221 + apps/web/src/pages/WeatherGreeting.tsx | 51 + apps/web/src/pages/WorkAssignPage.tsx | 129 + apps/web/src/pages/bidStatus.ts | 185 + apps/web/src/pages/contractStatus.ts | 45 + apps/web/src/pages/exportExcel.ts | 59 + apps/web/src/pages/financeStatus.ts | 112 + apps/web/src/pages/hrStatus.ts | 188 + apps/web/src/pages/im/ImCallModal.tsx | 129 + apps/web/src/pages/im/ImPage.tsx | 320 + apps/web/src/pages/projectStatus.ts | 47 + apps/web/src/store/auth.ts | 152 + apps/web/src/time.ts | 15 + apps/web/src/vite-env.d.ts | 1 + apps/web/tsconfig.app.json | 26 + apps/web/tsconfig.json | 7 + apps/web/tsconfig.node.json | 24 + apps/web/vite.config.ts | 44 + docker-compose.yml | 58 + docs/architecture/overview.md | 7 + fengying-oa-cursor-prompt-v1.md | 77 + huawei/SHA256.txt | 9 + huawei/agconnect-services.json | 1 + infra/fengying-cloud/docker-compose.yml | 58 + infra/fengying-cloud/fengying-api.service | 16 + infra/fengying-cloud/fengying-im.service | 16 + infra/fengying-cloud/oa-local.conf | 73 + livekit.yaml | 12 + package.json | 20 + pnpm-lock.yaml | 10413 ++++++++++++++++ pnpm-workspace.yaml | 3 + scripts/smoke-api.sh | 31 + skills/frontend-design/SKILL.md | 26 + 企业微信/企业微信安装界面1.png | Bin 0 -> 87659 bytes 企业微信/企业微信安装界面2.png | Bin 0 -> 62134 bytes 企业微信/企业微信安装界面3.png | Bin 0 -> 49805 bytes ...对话界面消息右击效果展示.png | Bin 0 -> 475571 bytes ...信工作台按钮点击效果展示.png | Bin 0 -> 237599 bytes .../企业微信工作台界面展示.png | Bin 0 -> 398760 bytes .../企业微信截图按键悬停展示.png | Bin 0 -> 321385 bytes .../企业微信更多按钮功能展示.png | Bin 0 -> 427095 bytes 企业微信/企业微信登录界面2.png | Bin 0 -> 152546 bytes 企业微信/企业微信登录界面3.png | Bin 0 -> 121243 bytes 企业微信/企业微信登录界面4.png | Bin 0 -> 127623 bytes 企业微信/企业微信登录界面5.png | Bin 0 -> 126379 bytes 企业微信/企业微信登录页面1.png | Bin 0 -> 147890 bytes ...终端消息列表右键功能展示.png | Bin 0 -> 499227 bytes 企业微信/企业微信终端界面1.png | Bin 0 -> 195848 bytes 企业微信/企业微信终端界面2.png | Bin 0 -> 196870 bytes 企业微信/企业微信终端界面3.png | Bin 0 -> 239889 bytes 企业微信/企业微信终端界面4.png | Bin 0 -> 96330 bytes ...终端群聊消息记录悬停展示.png | Bin 0 -> 322917 bytes .../企业微信终端群聊界面展示.png | Bin 0 -> 317311 bytes .../企业微信群聊@功能展示.png | Bin 0 -> 325640 bytes .../企业微信聊天记录页展示.png | Bin 0 -> 387405 bytes 企业微信/企业微信通知界面1.png | Bin 0 -> 242969 bytes 提示音/拨号音.mp3 | Bin 0 -> 462097 bytes 提示音/来电铃声.mp3 | Bin 0 -> 9109 bytes 提示音/消息发送音.wav | Bin 0 -> 48462 bytes 提示音/消息提醒音.wav | Bin 0 -> 594510 bytes 魅族/4439e7cdb9354931a639c0f8d0f116b6.pdf | Bin 0 -> 981949 bytes 魅族/a553aca8aef643b8a3d1d20670697719.pdf | Bin 0 -> 1434164 bytes 507 files changed, 99891 insertions(+) create mode 100644 .cursor/rules/office-process.mdc create mode 100644 .cursor/rules/windows-packaging.mdc create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 DELIVERY.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 WINDOWS_BUILD_3.1.4.md create mode 100644 WINDOWS_BUILD_3.1.7.md create mode 100644 agconnect-services.json create mode 100644 apps/api/.prettierrc create mode 100644 apps/api/README.md create mode 100644 apps/api/data/app-release.json create mode 100644 apps/api/eslint.config.mjs create mode 100644 apps/api/nest-cli.json create mode 100644 apps/api/package.json create mode 100644 apps/api/pnpm-lock.yaml create mode 100644 apps/api/prisma/backfill-contract-buckets.ts create mode 100644 apps/api/prisma/backfill-project-ledger.ts create mode 100644 apps/api/prisma/backfill-report-cc.ts create mode 100644 apps/api/prisma/backfill-tech-travel.ts create mode 100644 apps/api/prisma/backfill-won-ledger.ts create mode 100644 apps/api/prisma/ensure-prod-admin.ts create mode 100644 apps/api/prisma/fix-approval-fields.ts create mode 100644 apps/api/prisma/fix-bid-from-oa.ts create mode 100644 apps/api/prisma/fix-expense-applicants.ts create mode 100644 apps/api/prisma/fix-expense-lines.ts create mode 100644 apps/api/prisma/fix-oa-posts.ts create mode 100644 apps/api/prisma/grant-admin-equivalents.ts create mode 100644 apps/api/prisma/grant-sun-chunqing.ts create mode 100644 apps/api/prisma/import-hr.ts create mode 100644 apps/api/prisma/import-notices-from-oa.ts create mode 100644 apps/api/prisma/import-prod-complete.ts create mode 100644 apps/api/prisma/import-prod.ts create mode 100644 apps/api/prisma/import-salary-from-oa.ts create mode 100644 apps/api/prisma/menus.ts create mode 100644 apps/api/prisma/migrations/20260829065602_init/migration.sql create mode 100644 apps/api/prisma/migrations/20260829071500_bid_workflow_phase1/migration.sql create mode 100644 apps/api/prisma/migrations/20260829073000_bid_draft_to_open/migration.sql create mode 100644 apps/api/prisma/migrations/20260829080000_contract_and_project/migration.sql create mode 100644 apps/api/prisma/migrations/20260829090000_finance_funds/migration.sql create mode 100644 apps/api/prisma/migrations/20260829100000_project_execution/migration.sql create mode 100644 apps/api/prisma/migrations/20260829110000_seal_office/migration.sql create mode 100644 apps/api/prisma/migrations/20260829120000_hr_org_payroll/migration.sql create mode 100644 apps/api/prisma/migrations/20260829130000_reports_and_ops/migration.sql create mode 100644 apps/api/prisma/migrations/20260829140000_work_report_flow/migration.sql create mode 100644 apps/api/prisma/migrations/20260829150000_party_expense_forms/migration.sql create mode 100644 apps/api/prisma/migrations/20260829170000_sms_send_log/migration.sql create mode 100644 apps/api/prisma/migrations/20260829180000_hr_archive_settings/migration.sql create mode 100644 apps/api/prisma/migrations/20260830140000_contract_parties/migration.sql create mode 100644 apps/api/prisma/migrations/20260830150000_leave_and_punch/migration.sql create mode 100644 apps/api/prisma/migrations/20260830170000_labor_contracts/migration.sql create mode 100644 apps/api/prisma/migrations/20260830180000_onboard_assets_assign/migration.sql create mode 100644 apps/api/prisma/migrations/20260830190000_project_party_budget/migration.sql create mode 100644 apps/api/prisma/migrations/20260830200000_project_accept_materials/migration.sql create mode 100644 apps/api/prisma/migrations/20260830210000_office_apply_asset_link/migration.sql create mode 100644 apps/api/prisma/migrations/20260830220000_office_flow_inbound_notices/migration.sql create mode 100644 apps/api/prisma/migrations/20260830230000_sms_events_notice_kind/migration.sql create mode 100644 apps/api/prisma/migrations/20260831093000_project_type_amount/migration.sql create mode 100644 apps/api/prisma/migrations/20260831095000_notice_audience/migration.sql create mode 100644 apps/api/prisma/migrations/20260831102000_role_acl_custom/migration.sql create mode 100644 apps/api/prisma/migrations/20260831140000_push_device/migration.sql create mode 100644 apps/api/prisma/migrations/20260901090000_im_message_lifecycle/migration.sql create mode 100644 apps/api/prisma/migrations/20260901150000_bid_manual_number/migration.sql create mode 100644 apps/api/prisma/migrations/20260902100000_im_message_seq/migration.sql create mode 100644 apps/api/prisma/migrations/migration_lock.toml create mode 100644 apps/api/prisma/oa-json.ts create mode 100644 apps/api/prisma/production-clean-orphan-contract-files-20260901.sql create mode 100644 apps/api/prisma/production-deduplicate-20260901.sql create mode 100644 apps/api/prisma/production-restore-missing-expense-20260901.sql create mode 100644 apps/api/prisma/schema.prisma create mode 100644 apps/api/prisma/seed.ts create mode 100644 apps/api/prisma/sync-menus.ts create mode 100644 apps/api/prisma/sync-oa-attachments.ts create mode 100644 apps/api/prisma/sync-tender-methods.ts create mode 100644 apps/api/prisma/wash-bid-buckets.ts create mode 100644 apps/api/prisma/wash-legal-entities.ts create mode 100644 apps/api/prisma/wash-oa-flow-finance.ts create mode 100644 apps/api/prisma/wash-oa-ledger.ts create mode 100644 apps/api/prisma/wash-terminated-winners.ts create mode 100644 apps/api/src/app.module.ts create mode 100644 apps/api/src/auth/auth.controller.ts create mode 100644 apps/api/src/auth/auth.module.ts create mode 100644 apps/api/src/auth/auth.service.ts create mode 100644 apps/api/src/auth/jwt.strategy.ts create mode 100644 apps/api/src/bidding/bid-workflow.spec.ts create mode 100644 apps/api/src/bidding/bid-workflow.ts create mode 100644 apps/api/src/bidding/bidding.controller.ts create mode 100644 apps/api/src/bidding/bidding.service.ts create mode 100644 apps/api/src/business/business.controller.ts create mode 100644 apps/api/src/common/data-scope.ts create mode 100644 apps/api/src/common/decorators/auth.decorators.ts create mode 100644 apps/api/src/common/decorators/current-user.decorator.ts create mode 100644 apps/api/src/common/doc-no.spec.ts create mode 100644 apps/api/src/common/doc-no.ts create mode 100644 apps/api/src/common/dto/page-query.dto.ts create mode 100644 apps/api/src/common/filters/http-exception.filter.ts create mode 100644 apps/api/src/common/guards/auth.guards.ts create mode 100644 apps/api/src/common/interceptors/response.interceptor.ts create mode 100644 apps/api/src/common/middleware/request-id.middleware.ts create mode 100644 apps/api/src/common/org-chain.spec.ts create mode 100644 apps/api/src/common/org-chain.ts create mode 100644 apps/api/src/common/role-groups.ts create mode 100644 apps/api/src/common/tencent-lbs.ts create mode 100644 apps/api/src/config/env.spec.ts create mode 100644 apps/api/src/config/env.ts create mode 100644 apps/api/src/contract/contract-workflow.spec.ts create mode 100644 apps/api/src/contract/contract-workflow.ts create mode 100644 apps/api/src/contract/contract.controller.ts create mode 100644 apps/api/src/contract/contract.service.ts create mode 100644 apps/api/src/contract/won-ledger.ts create mode 100644 apps/api/src/files/files.controller.ts create mode 100644 apps/api/src/files/files.mime.spec.ts create mode 100644 apps/api/src/files/files.mime.ts create mode 100644 apps/api/src/files/files.module.ts create mode 100644 apps/api/src/files/files.service.ts create mode 100644 apps/api/src/finance/bank-pay.ts create mode 100644 apps/api/src/finance/finance-workflow.spec.ts create mode 100644 apps/api/src/finance/finance-workflow.ts create mode 100644 apps/api/src/finance/finance.controller.ts create mode 100644 apps/api/src/finance/finance.service.ts create mode 100644 apps/api/src/health/health.controller.ts create mode 100644 apps/api/src/hr/hr-workflow.spec.ts create mode 100644 apps/api/src/hr/hr-workflow.ts create mode 100644 apps/api/src/hr/hr.controller.ts create mode 100644 apps/api/src/hr/hr.service.ts create mode 100644 apps/api/src/im/im-bus.ts create mode 100644 apps/api/src/im/im.controller.ts create mode 100644 apps/api/src/im/im.module.ts create mode 100644 apps/api/src/im/im.service.ts create mode 100644 apps/api/src/im/voice-asr.service.ts create mode 100644 apps/api/src/legal/legal.controller.ts create mode 100644 apps/api/src/legal/legal.docs.ts create mode 100644 apps/api/src/main.ts create mode 100644 apps/api/src/master/master.controller.ts create mode 100644 apps/api/src/master/master.service.ts create mode 100644 apps/api/src/office/office-apply.spec.ts create mode 100644 apps/api/src/office/office-apply.ts create mode 100644 apps/api/src/office/office.controller.ts create mode 100644 apps/api/src/office/work-report.ts create mode 100644 apps/api/src/prisma/prisma.module.ts create mode 100644 apps/api/src/prisma/prisma.service.ts create mode 100644 apps/api/src/project/project-workflow.spec.ts create mode 100644 apps/api/src/project/project-workflow.ts create mode 100644 apps/api/src/project/project.controller.ts create mode 100644 apps/api/src/project/project.service.ts create mode 100644 apps/api/src/push/push.controller.ts create mode 100644 apps/api/src/push/push.module.ts create mode 100644 apps/api/src/push/push.service.ts create mode 100644 apps/api/src/report/report.controller.ts create mode 100644 apps/api/src/report/report.service.ts create mode 100644 apps/api/src/seal/seal-workflow.spec.ts create mode 100644 apps/api/src/seal/seal-workflow.ts create mode 100644 apps/api/src/seal/seal.controller.ts create mode 100644 apps/api/src/seal/seal.service.ts create mode 100644 apps/api/src/sms/sms-bus.ts create mode 100644 apps/api/src/sms/sms-events.ts create mode 100644 apps/api/src/sms/sms-fill.spec.ts create mode 100644 apps/api/src/sms/sms-fill.ts create mode 100644 apps/api/src/sms/sms.controller.ts create mode 100644 apps/api/src/sms/sms.module.ts create mode 100644 apps/api/src/sms/sms.service.ts create mode 100644 apps/api/src/system/numbering.ts create mode 100644 apps/api/src/system/op-log.ts create mode 100644 apps/api/src/system/ops-workflow.spec.ts create mode 100644 apps/api/src/system/ops-workflow.ts create mode 100644 apps/api/src/system/system.controller.ts create mode 100644 apps/api/src/system/system.service.ts create mode 100644 apps/api/test/app.e2e-spec.ts create mode 100644 apps/api/test/jest-e2e.json create mode 100644 apps/api/tsconfig.build.json create mode 100644 apps/api/tsconfig.json create mode 100644 apps/harmony/.gitignore create mode 100644 apps/harmony/AppScope/app.json5 create mode 100644 apps/harmony/AppScope/resources/base/element/string.json create mode 100644 apps/harmony/AppScope/resources/base/media/app_icon.png create mode 100644 apps/harmony/build-profile.json5 create mode 100644 apps/harmony/entry/build-profile.json5 create mode 100644 apps/harmony/entry/hvigorfile.ts create mode 100644 apps/harmony/entry/oh-package.json5 create mode 100644 apps/harmony/entry/src/main/ets/entryability/EntryAbility.ets create mode 100644 apps/harmony/entry/src/main/ets/pages/Index.ets create mode 100644 apps/harmony/entry/src/main/ets/pushability/PushMessageAbility.ets create mode 100644 apps/harmony/entry/src/main/module.json5 create mode 100644 apps/harmony/entry/src/main/resources/base/element/color.json create mode 100644 apps/harmony/entry/src/main/resources/base/element/string.json create mode 100644 apps/harmony/entry/src/main/resources/base/media/app_icon.png create mode 100644 apps/harmony/entry/src/main/resources/base/profile/main_pages.json create mode 100644 apps/harmony/entry/src/main/resources/rawfile/agconnect-services.json create mode 100644 apps/harmony/hvigor/hvigor-config.json5 create mode 100644 apps/harmony/hvigorfile.ts create mode 100644 apps/harmony/oh-package-lock.json5 create mode 100644 apps/harmony/oh-package.json5 create mode 100644 apps/im-server/README.md create mode 100644 apps/im-server/package.json create mode 100644 apps/im-server/server.js create mode 100644 apps/native/.gitignore create mode 100644 apps/native/.metadata create mode 100644 apps/native/README.md create mode 100644 apps/native/WINDOWS_BUILD.md create mode 100644 apps/native/analysis_options.yaml create mode 100644 apps/native/android/.gitignore create mode 100644 apps/native/android/app/agconnect-services.json create mode 100644 apps/native/android/app/build.gradle create mode 100644 apps/native/android/app/libs/push-internal-5.0.5.aar create mode 100644 apps/native/android/app/proguard-rules.pro create mode 100644 apps/native/android/app/src/debug/AndroidManifest.xml create mode 100644 apps/native/android/app/src/main/AndroidManifest.xml create mode 100644 apps/native/android/app/src/main/assets/agconnect-services.json create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/Coord.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/FengyingApp.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/HuaweiPushService.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/MainActivity.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/MeizuPushReceiver.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OemAdapt.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoCompatPushService.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoDataMessageService.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoPushCallback.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/PushStore.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/VivoPushReceiver.kt create mode 100644 apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/VoiceAsr.kt create mode 100644 apps/native/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 apps/native/android/app/src/main/res/drawable/launch_background.xml create mode 100644 apps/native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 apps/native/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 apps/native/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 apps/native/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 apps/native/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 apps/native/android/app/src/main/res/raw/fengying_dial.mp3 create mode 100644 apps/native/android/app/src/main/res/raw/fengying_incoming.mp3 create mode 100644 apps/native/android/app/src/main/res/raw/fengying_notification.wav create mode 100644 apps/native/android/app/src/main/res/raw/fengying_send.wav create mode 100644 apps/native/android/app/src/main/res/values-night-v35/styles.xml create mode 100644 apps/native/android/app/src/main/res/values-night/styles.xml create mode 100644 apps/native/android/app/src/main/res/values-v35/styles.xml create mode 100644 apps/native/android/app/src/main/res/values/styles.xml create mode 100644 apps/native/android/app/src/main/res/xml/file_paths.xml create mode 100644 apps/native/android/app/src/profile/AndroidManifest.xml create mode 100644 apps/native/android/build.gradle create mode 100644 apps/native/android/gradle.properties create mode 100644 apps/native/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 apps/native/android/settings.gradle create mode 100644 apps/native/assets/brand/fengying-logo-512.png create mode 100644 apps/native/assets/sounds/ring.wav create mode 100755 apps/native/build-linux.sh create mode 100644 apps/native/build-windows.bat create mode 100644 apps/native/build-windows.ps1 create mode 100644 apps/native/installer/bootstrap-windows.ps1 create mode 100644 apps/native/installer/fengying-oa.iss create mode 100644 apps/native/lib/api/oa_client.dart create mode 100644 apps/native/lib/app_config.dart create mode 100644 apps/native/lib/app_runtime.dart create mode 100644 apps/native/lib/desktop/desk_platform.dart create mode 100644 apps/native/lib/desktop/desk_shell.dart create mode 100644 apps/native/lib/desktop/desk_theme.dart create mode 100644 apps/native/lib/desktop/pages/desk_attendance_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_chat_detail_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_chat_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_contacts_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_conv_list_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_hr_apply_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_legal_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_office_list_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_org_browse_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_pick_people_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_profile_edit_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_profile_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_record_detail_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_system_notice_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_work_assign_page.dart create mode 100644 apps/native/lib/desktop/pages/desk_workbench_page.dart create mode 100644 apps/native/lib/desktop/widgets/desk_data_table.dart create mode 100644 apps/native/lib/desktop/widgets/desk_widgets.dart create mode 100644 apps/native/lib/device/device_bridge.dart create mode 100644 apps/native/lib/device/push_bridge.dart create mode 100644 apps/native/lib/im/chat_local_store.dart create mode 100644 apps/native/lib/im/chat_prefs.dart create mode 100644 apps/native/lib/im/im_bridge.dart create mode 100644 apps/native/lib/im/im_lifecycle.dart create mode 100644 apps/native/lib/im/im_seq_store.dart create mode 100644 apps/native/lib/im/pinyin.dart create mode 100644 apps/native/lib/im/protocol.dart create mode 100644 apps/native/lib/im/sticker_store.dart create mode 100644 apps/native/lib/im/tcp_client.dart create mode 100644 apps/native/lib/labels.dart create mode 100644 apps/native/lib/main.dart create mode 100644 apps/native/lib/nav/biz_route.dart create mode 100644 apps/native/lib/nav/notice_route.dart create mode 100644 apps/native/lib/nav/open_module.dart create mode 100644 apps/native/lib/ota/updater.dart create mode 100644 apps/native/lib/pages/approvals_page.dart create mode 100644 apps/native/lib/pages/attendance_page.dart create mode 100644 apps/native/lib/pages/calendar_page.dart create mode 100644 apps/native/lib/pages/call_page.dart create mode 100644 apps/native/lib/pages/chat_detail_page.dart create mode 100644 apps/native/lib/pages/chat_page.dart create mode 100644 apps/native/lib/pages/desktop_login_page.dart create mode 100644 apps/native/lib/pages/desktop_workbench_page.dart create mode 100644 apps/native/lib/pages/directory_page.dart create mode 100644 apps/native/lib/pages/flow_page.dart create mode 100644 apps/native/lib/pages/hr_apply_page.dart create mode 100644 apps/native/lib/pages/legal_page.dart create mode 100644 apps/native/lib/pages/location_pick_page.dart create mode 100644 apps/native/lib/pages/login_page.dart create mode 100644 apps/native/lib/pages/messages_page.dart create mode 100644 apps/native/lib/pages/mine_page.dart create mode 100644 apps/native/lib/pages/module_list_page.dart create mode 100644 apps/native/lib/pages/notice_detail_page.dart create mode 100644 apps/native/lib/pages/notices_page.dart create mode 100644 apps/native/lib/pages/org_browse_page.dart create mode 100644 apps/native/lib/pages/pick_people_page.dart create mode 100644 apps/native/lib/pages/profile_page.dart create mode 100644 apps/native/lib/pages/record_detail_page.dart create mode 100644 apps/native/lib/pages/scan_login_page.dart create mode 100644 apps/native/lib/pages/system_notice_page.dart create mode 100644 apps/native/lib/pages/todos_page.dart create mode 100644 apps/native/lib/pages/work_assign_page.dart create mode 100644 apps/native/lib/pages/workbench_page.dart create mode 100644 apps/native/lib/session/session.dart create mode 100644 apps/native/lib/shell/desktop_shell.dart create mode 100644 apps/native/lib/shell/home_shell.dart create mode 100644 apps/native/lib/theme.dart create mode 100644 apps/native/lib/widgets/auth_image.dart create mode 100644 apps/native/lib/widgets/beian_footer.dart create mode 100644 apps/native/lib/widgets/common.dart create mode 100644 apps/native/lib/widgets/desktop_ui.dart create mode 100644 apps/native/lib/widgets/html_body.dart create mode 100644 apps/native/lib/widgets/location_view_page.dart create mode 100644 apps/native/lib/widgets/tencent_map_thumb.dart create mode 100644 apps/native/lib/widgets/voice_message.dart create mode 100644 apps/native/lib/widgets/wecom.dart create mode 100644 apps/native/linux/.gitignore create mode 100644 apps/native/linux/CMakeLists.txt create mode 100644 apps/native/linux/runner/CMakeLists.txt create mode 100644 apps/native/linux/runner/main.cc create mode 100644 apps/native/linux/runner/my_application.cc create mode 100644 apps/native/linux/runner/my_application.h create mode 100755 apps/native/packaging/build-deb.sh create mode 100644 apps/native/pubspec.lock create mode 100644 apps/native/pubspec.yaml create mode 100755 apps/native/scripts/publish-apk.sh create mode 100644 apps/native/test/widget_test.dart create mode 100644 apps/native/windows/.gitignore create mode 100644 apps/native/windows/CMakeLists.txt create mode 100644 apps/native/windows/runner/CMakeLists.txt create mode 100644 apps/native/windows/runner/Runner.rc create mode 100644 apps/native/windows/runner/flutter_window.cpp create mode 100644 apps/native/windows/runner/flutter_window.h create mode 100644 apps/native/windows/runner/main.cpp create mode 100644 apps/native/windows/runner/resource.h create mode 100644 apps/native/windows/runner/resources/app_icon.ico create mode 100644 apps/native/windows/runner/runner.exe.manifest create mode 100644 apps/native/windows/runner/utils.cpp create mode 100644 apps/native/windows/runner/utils.h create mode 100644 apps/native/windows/runner/win32_window.cpp create mode 100644 apps/native/windows/runner/win32_window.h create mode 100644 apps/web/.gitignore create mode 100644 apps/web/README.md create mode 100644 apps/web/eslint.config.js create mode 100644 apps/web/index.html create mode 100644 apps/web/package.json create mode 100644 apps/web/pnpm-lock.yaml create mode 100644 apps/web/public/brand/fengying-logo-1024-opaque.png create mode 100644 apps/web/public/brand/fengying-logo-1024.png create mode 100644 apps/web/public/brand/fengying-logo-512-opaque.png create mode 100644 apps/web/public/brand/fengying-logo-512.png create mode 100644 apps/web/public/brand/fengying-logo.svg create mode 100644 apps/web/public/vite.svg create mode 100644 apps/web/src/App.tsx create mode 100644 apps/web/src/StatusTag.tsx create mode 100644 apps/web/src/access.ts create mode 100644 apps/web/src/api/client.ts create mode 100644 apps/web/src/assets/react.svg create mode 100644 apps/web/src/components/AmbientBackground.tsx create mode 100644 apps/web/src/components/BrandLogo.tsx create mode 100644 apps/web/src/components/DesktopDownload.tsx create mode 100644 apps/web/src/components/LinuxDownload.tsx create mode 100644 apps/web/src/components/MobileDownload.tsx create mode 100644 apps/web/src/components/SiteFooter.tsx create mode 100644 apps/web/src/deptTree.ts create mode 100644 apps/web/src/geo/gcj02.ts create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/labels.ts create mode 100644 apps/web/src/layouts/AppLayout.tsx create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/mammoth-browser.d.ts create mode 100644 apps/web/src/pages/AppReleasePage.tsx create mode 100644 apps/web/src/pages/ApplyBucketBar.tsx create mode 100644 apps/web/src/pages/AssetPurchaseLedgerPage.tsx create mode 100644 apps/web/src/pages/AssetRegistryPage.tsx create mode 100644 apps/web/src/pages/BidCreatePage.tsx create mode 100644 apps/web/src/pages/BidDetailPage.tsx create mode 100644 apps/web/src/pages/BidListPage.tsx create mode 100644 apps/web/src/pages/BondListPage.tsx create mode 100644 apps/web/src/pages/CalendarPage.tsx create mode 100644 apps/web/src/pages/ContractDetailPage.tsx create mode 100644 apps/web/src/pages/ContractListPage.tsx create mode 100644 apps/web/src/pages/CostObjectFields.tsx create mode 100644 apps/web/src/pages/DesktopLoginPage.tsx create mode 100644 apps/web/src/pages/DirectoryPage.tsx create mode 100644 apps/web/src/pages/EmployeePage.tsx create mode 100644 apps/web/src/pages/ExpenseDetailPage.tsx create mode 100644 apps/web/src/pages/ExpenseInvoiceFields.tsx create mode 100644 apps/web/src/pages/ExpenseListPage.tsx create mode 100644 apps/web/src/pages/FileAttachments.tsx create mode 100644 apps/web/src/pages/FinanceCells.tsx create mode 100644 apps/web/src/pages/FormDrawer.tsx create mode 100644 apps/web/src/pages/HrApplyPage.tsx create mode 100644 apps/web/src/pages/HrLeaveDetailPage.tsx create mode 100644 apps/web/src/pages/HrNoticePage.tsx create mode 100644 apps/web/src/pages/HrPages.tsx create mode 100644 apps/web/src/pages/HrSettingsPage.tsx create mode 100644 apps/web/src/pages/InvoiceListPage.tsx create mode 100644 apps/web/src/pages/LaborContractPage.tsx create mode 100644 apps/web/src/pages/LegalPage.tsx create mode 100644 apps/web/src/pages/LoanDetailPage.tsx create mode 100644 apps/web/src/pages/LoginPage.tsx create mode 100644 apps/web/src/pages/MessagesChannelPage.tsx create mode 100644 apps/web/src/pages/MyAttendancePage.tsx create mode 100644 apps/web/src/pages/MyExpensesPage.tsx create mode 100644 apps/web/src/pages/MyFlowPage.tsx create mode 100644 apps/web/src/pages/NoticeDetailPage.tsx create mode 100644 apps/web/src/pages/NoticeHtml.tsx create mode 100644 apps/web/src/pages/NoticePage.tsx create mode 100644 apps/web/src/pages/OfficeApplyPage.tsx create mode 100644 apps/web/src/pages/OfficeApprovalsPage.tsx create mode 100644 apps/web/src/pages/OfficeSmsPage.tsx create mode 100644 apps/web/src/pages/OfficeTodosPage.tsx create mode 100644 apps/web/src/pages/OnboardPage.tsx create mode 100644 apps/web/src/pages/OpsPages.tsx create mode 100644 apps/web/src/pages/OrganizationPage.tsx create mode 100644 apps/web/src/pages/OverviewPage.tsx create mode 100644 apps/web/src/pages/PartyPages.tsx create mode 100644 apps/web/src/pages/PdfPreview.tsx create mode 100644 apps/web/src/pages/ProfileDrawer.tsx create mode 100644 apps/web/src/pages/ProjectDetailPage.tsx create mode 100644 apps/web/src/pages/ProjectListPage.tsx create mode 100644 apps/web/src/pages/PunchLocationMap.tsx create mode 100644 apps/web/src/pages/QualificationListPage.tsx create mode 100644 apps/web/src/pages/ReportCharts.tsx create mode 100644 apps/web/src/pages/ResourcePage.tsx create mode 100644 apps/web/src/pages/RichEditor.tsx create mode 100644 apps/web/src/pages/SealRequestListPage.tsx create mode 100644 apps/web/src/pages/SmsPage.tsx create mode 100644 apps/web/src/pages/StatusPages.tsx create mode 100644 apps/web/src/pages/TaskListPage.tsx create mode 100644 apps/web/src/pages/WeatherGreeting.tsx create mode 100644 apps/web/src/pages/WorkAssignPage.tsx create mode 100644 apps/web/src/pages/bidStatus.ts create mode 100644 apps/web/src/pages/contractStatus.ts create mode 100644 apps/web/src/pages/exportExcel.ts create mode 100644 apps/web/src/pages/financeStatus.ts create mode 100644 apps/web/src/pages/hrStatus.ts create mode 100644 apps/web/src/pages/im/ImCallModal.tsx create mode 100644 apps/web/src/pages/im/ImPage.tsx create mode 100644 apps/web/src/pages/projectStatus.ts create mode 100644 apps/web/src/store/auth.ts create mode 100644 apps/web/src/time.ts create mode 100644 apps/web/src/vite-env.d.ts create mode 100644 apps/web/tsconfig.app.json create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/tsconfig.node.json create mode 100644 apps/web/vite.config.ts create mode 100644 docker-compose.yml create mode 100644 docs/architecture/overview.md create mode 100644 fengying-oa-cursor-prompt-v1.md create mode 100644 huawei/SHA256.txt create mode 100644 huawei/agconnect-services.json create mode 100644 infra/fengying-cloud/docker-compose.yml create mode 100644 infra/fengying-cloud/fengying-api.service create mode 100644 infra/fengying-cloud/fengying-im.service create mode 100644 infra/fengying-cloud/oa-local.conf create mode 100644 livekit.yaml create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100755 scripts/smoke-api.sh create mode 100644 skills/frontend-design/SKILL.md create mode 100644 企业微信/企业微信安装界面1.png create mode 100644 企业微信/企业微信安装界面2.png create mode 100644 企业微信/企业微信安装界面3.png create mode 100644 企业微信/企业微信对话界面消息右击效果展示.png create mode 100644 企业微信/企业微信工作台按钮点击效果展示.png create mode 100644 企业微信/企业微信工作台界面展示.png create mode 100644 企业微信/企业微信截图按键悬停展示.png create mode 100644 企业微信/企业微信更多按钮功能展示.png create mode 100644 企业微信/企业微信登录界面2.png create mode 100644 企业微信/企业微信登录界面3.png create mode 100644 企业微信/企业微信登录界面4.png create mode 100644 企业微信/企业微信登录界面5.png create mode 100644 企业微信/企业微信登录页面1.png create mode 100644 企业微信/企业微信终端消息列表右键功能展示.png create mode 100644 企业微信/企业微信终端界面1.png create mode 100644 企业微信/企业微信终端界面2.png create mode 100644 企业微信/企业微信终端界面3.png create mode 100644 企业微信/企业微信终端界面4.png create mode 100644 企业微信/企业微信终端群聊消息记录悬停展示.png create mode 100644 企业微信/企业微信终端群聊界面展示.png create mode 100644 企业微信/企业微信群聊@功能展示.png create mode 100644 企业微信/企业微信聊天记录页展示.png create mode 100644 企业微信/企业微信通知界面1.png create mode 100644 提示音/拨号音.mp3 create mode 100644 提示音/来电铃声.mp3 create mode 100644 提示音/消息发送音.wav create mode 100644 提示音/消息提醒音.wav create mode 100644 魅族/4439e7cdb9354931a639c0f8d0f116b6.pdf create mode 100644 魅族/a553aca8aef643b8a3d1d20670697719.pdf diff --git a/.cursor/rules/office-process.mdc b/.cursor/rules/office-process.mdc new file mode 100644 index 0000000..d33bc22 --- /dev/null +++ b/.cursor/rules/office-process.mdc @@ -0,0 +1,30 @@ +--- +description: 个人办公申请必须走统一流程,禁止再做孤立页面和三五个文字链筛选 +alwaysApply: true +--- + +# 办公申请怎么长 + +个人办公是「人发起、人跟踪、落地到台账」。不要再按用户点到哪一页就只改那一页。 + +## 列表筛选 + +- 用 `ApplyBucketBar`(Segmented + `?bucket=`),有选中态。 +- 标准桶:全部 / 待审批 / 已通过 / 已驳回。领域附加桶(外带未还、占用中、待入库)可以加,不能替代标准桶。 +- 禁止再在页头放三个无选中态的 `Link`(「全部」「待审批」「外带未还」这种)。 + +## 菜单 + +- 「我的申请」是跟踪入口,人事/用章/办公申请是发起入口,待我审批是审批入口。不要做成三套互不相通的列表。 +- 申请人页只看自己的单;台账(用章登记、资产管理、采购申请)在业务模块。 + +## 审批通过必须写下游 + +| 申请 | 落到 | +|---|---| +| 资产购置 | 采购单 + 待入库台账 + 付款待办 | +| 资产领用 / 用车 | 占用记录(挂台账 id),交回不改保管人 | +| 出差 | 差旅报销草稿 | +| 用章外带 | 归还待办 | + +保管人是台账责任人,占用是谁正在用。占用时不要把保管人改成持有人。 diff --git a/.cursor/rules/windows-packaging.mdc b/.cursor/rules/windows-packaging.mdc new file mode 100644 index 0000000..c05c7eb --- /dev/null +++ b/.cursor/rules/windows-packaging.mdc @@ -0,0 +1,11 @@ +--- +description: Windows 客户端打包机与 SSH 端口 +alwaysApply: true +--- + +# Windows 端打包 + +- 打包机:`10.1.1.202`,SSH **端口 22022**(不是 22),用户 `administrator`。 +- 产出必须是可安装的 **`.exe`**(Inno Setup 安装包),不要只丢 zip。 +- 安装包上传到生产:`https://oa.fysxkj.com/fengying-oa-setup.exe`,并写入 `apps/api/data/app-release.json` 的 `windowsUrl`。 +- 安卓仍在本机 `/opt/apps/native` 打 APK。 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..34a2c44 --- /dev/null +++ b/.env.example @@ -0,0 +1,44 @@ +NODE_ENV=development +APP_PORT=3000 +APP_NAME=enterprise-ops +API_PREFIX=api/v1 +CORS_ORIGINS=http://localhost:5173 + +DATABASE_URL=postgresql://ops:ops_dev_only@localhost:5432/enterprise_ops +REDIS_URL=redis://localhost:6379 +MINIO_ENDPOINT=localhost +MINIO_PORT=9000 +MINIO_USE_SSL=false +MINIO_ACCESS_KEY=opsadmin +MINIO_SECRET_KEY=opsadmin_dev_only +MINIO_BUCKET=enterprise-ops + +JWT_ACCESS_SECRET=change-me-access-secret-dev-only +JWT_REFRESH_SECRET=change-me-refresh-secret-dev-only +JWT_ACCESS_EXPIRES=15m +JWT_REFRESH_EXPIRES=7d + +ALIYUN_SMS_ACCESS_KEY_ID= +ALIYUN_SMS_ACCESS_KEY_SECRET= +ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com +ALIYUN_SMS_REGION=cn-qingdao +ALIYUN_SMS_DEFAULT_SIGN=江苏风影随行科技 + +VIVO_PUSH_APP_ID= +VIVO_PUSH_APP_KEY= +VIVO_PUSH_APP_SECRET= +OPPO_PUSH_APP_ID= +OPPO_PUSH_APP_KEY= +OPPO_PUSH_APP_SECRET= +OPPO_PUSH_MASTER_SECRET= +MEIZU_PUSH_APP_ID= +MEIZU_PUSH_APP_KEY= +MEIZU_PUSH_APP_SECRET= + +POSTGRES_USER=ops +POSTGRES_PASSWORD=ops_dev_only +POSTGRES_DB=enterprise_ops +POSTGRES_PORT=5432 +REDIS_PORT=6379 +MINIO_API_PORT=9000 +MINIO_CONSOLE_PORT=9001 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a2fe668 --- /dev/null +++ b/.gitignore @@ -0,0 +1,49 @@ +node_modules +dist +build +coverage +.env +*.log +.DS_Store +apps/api/generated +apps/web/dist +.idea +.vscode +apps/api/uploads +import +apps/api/.aliyun-sms.env* +apps/api/.application-sms.yml* + +# 本地 SDK / 工具链(不进仓库) +flutter/ +android-sdk/ +.pnpm-store/ +backup/ +node_modules/ + +# 安装包与归档 +*.apk +*.exe +*.deb +*.dmg +*.msi +*.aab +*.ipa +*.tar.gz +*.zip +*.mp4 + +# Flutter 构建缓存 +.dart_tool/ +apps/native/build/ +apps/native/dist/ +apps/native/.flutter-plugins +apps/native/.flutter-plugins-dependencies + +# Android 签名(勿上传) +**/key.properties +**/*.jks +**/*.keystore + +# 临时打包 +fengying-oa-code-*.tar.gz diff --git a/DELIVERY.md b/DELIVERY.md new file mode 100644 index 0000000..8a031e2 --- /dev/null +++ b/DELIVERY.md @@ -0,0 +1,661 @@ +# 风影智慧办公 · 全系统转交文档 + +更新日期:2026-09-01 +当前手机包:**3.1.8+318**(`com.fengying.oa.mobile`) +本次生产修复与当前真实状态:**先读 `/opt/HANDOFF.md`,再读本文件第 0、2、3、14 节和 `.cursor/rules/`。** + +本文件覆盖 **Web + API + 手机 + IM + 库表 + 密钥 + 发版**。不是只写手机端。 +**含生产密钥,不要推到公开 git。** + +--- + +## 0. 你接手后立刻要知道的事 + +1. 这是一家约四十人的工程/软件公司内部 OA,从旧 Java OA(生产 MySQL 库 `oa`)迁到本仓库。 +2. 用户同时用 **网页** `https://oa.fysxkj.com` 和 **安卓包**。IM 是自研 TCP,不是环信。 +3. 办公模块有固定产品规则(第 3 节)。乱改列表筛选或审批不写下游,会被用户打回。 +4. 2026-09-01 的 API 数据修复、旧终止投标恢复兼容和项目类型修复已经发到生产;精确结果与备份位置见 `/opt/HANDOFF.md`。后续不要依据本文件旧版本描述判断是否已部署。 +5. 回复用户用**简体中文**。不要主动 `git commit` / `push`,除非用户说。 +6. 改菜单后必须跑 `apps/api/prisma/sync-menus.ts`,侧栏才更新。 +7. 手机登录/刷新必须带 `clientKind: 'mobile'`。 + +--- + +## 1. 产品与架构 + +一套 API,三端消费: + +| 端 | 路径 | 技术 | 入口 | +|---|---|---|---| +| API | `apps/api` | NestJS 11 + Prisma 6 + PostgreSQL | `/api/v1`,Swagger `/api/v1/docs` | +| Web | `apps/web` | React 18 + Vite 5 + Ant Design 5 + React Query + Zustand | 路由 `apps/web/src/App.tsx` | +| 手机 | `apps/native` | Flutter,包名 **必须** `com.fengying.oa.mobile` | 底栏:消息 / 工作台 / 通讯录 / 我 | +| IM | `apps/im-server/server.js` | Node TCP 8901 + 本机 HTTP 8902 | MobileIMSDK 协议 | + +生产: + +- 站点:`https://oa.fysxkj.com` +- REST:`https://oa.fysxkj.com/api/v1` +- IM TCP:`47.96.23.244:8901` +- APK:`https://oa.fysxkj.com/fengying-oa.apk` +- 远端代码:`/data/fengying-oa/` +- systemd:`fengying-api.service`、`fengying-im.service`(模板在 `infra/fengying-cloud/`) +- 生产 API 端口 **3010**(nginx 反代);本机 nest 常在 **3000** + +本机构建机 hostname `fykj`,公网不是 `47.96.23.244`。 + +租户字段 `tenantId` 目前固定 `'1'`,没有做多租户产品化。 + +--- + +## 2. 仓库地图 + +``` +/opt +├── DELIVERY.md ← 本文件 +├── .cursor/rules/office-process.mdc +├── apps/api Nest 后端 +│ ├── prisma/schema.prisma 库表 +│ ├── prisma/menus.ts 侧栏菜单源 +│ ├── prisma/seed.ts 演示数据 +│ ├── prisma/sync-menus.ts 菜单写入 Permission +│ ├── src/*.controller.ts 业务入口 +│ ├── data/app-release.json 手机 OTA +│ └── .env 本机密钥 +├── apps/web/src +│ ├── App.tsx 全部路由 +│ ├── pages/ 业务页 +│ ├── store/auth.ts 登录态 +│ ├── api/client.ts axios + sessionStorage +│ └── labels.ts 枚举中文 +├── apps/native Flutter +├── apps/im-server/server.js +└── infra/fengying-cloud nginx / systemd / compose 模板 +``` + +本机常用: + +```bash +cd /opt/apps/api && npx nest start # http://localhost:3000/api/v1 +cd /opt/apps/web && pnpm dev # http://localhost:5173 +cd /opt/apps/im-server && node server.js # TCP 8901 +cd /opt/apps/native && flutter build apk --release && bash scripts/publish-apk.sh +``` + +改 Prisma:`cd /opt/apps/api && npx prisma migrate dev`。 +改菜单:改 `prisma/menus.ts` 后 `npx ts-node --transpile-only prisma/sync-menus.ts`。 + +--- + +## 3. 硬约束(违反就是返工) + +### 3.1 个人办公怎么长 + +规则文件:`.cursor/rules/office-process.mdc` + +- 列表筛选用 **`ApplyBucketBar`**(Segmented + `?bucket=`),有选中态。 +- 标准桶:全部 / 待审批 / 已通过 / 已驳回。领域附加桶可以加,不能替代标准桶。 +- 禁止页头三个无选中态的 `Link`(「全部」「待审批」「外带未还」这种)。 +- 「我的申请」=`/office/flow` 是**跟踪**;人事/用章/办公申请是**发起**;「待我审批」是**审批**。不要做成三套互不相通的列表。 +- 申请人页只看自己的单;台账在业务模块(用章登记、资产管理、采购申请)。 + +审批通过必须写下游: + +| 申请 | 落到 | +|---|---| +| 资产购置 `ASSET_BUY` | 采购单 + 待入库台账 + 付款待办 | +| 资产领用 / 用车 | 占用记录(挂台账 id),**交回不改保管人** | +| 出差(人事) | 差旅报销草稿 | +| 用章外带 | 归还待办 | + +保管人 = 台账责任人。占用 = 谁正在用。占用时不要把保管人改成持有人。 + +实现:`apps/api/src/office/office-apply.ts`。 + +### 3.2 登录会话 + +`apps/api/src/auth/auth.service.ts` + Web `apps/web/src/api/client.ts` + +- Web 和手机**可以同时在线**。 +- 同一账号不能开两个 Web(后登挤掉先登)。JWT 含 `sid`。 +- 手机 `clientKind: 'mobile'`:refresh **不轮换**,access 7 天,refresh 3650 天,除非退出一直在线。 +- Web token 用 **sessionStorage** + BroadcastChannel(同浏览器多标签共享)。关浏览器再开要重登。 +- 桌面端只能扫码,**不能账密**。`clientKind: 'desktop'` 账密会被拒绝。页 `/login/desktop`;手机「我 → 确认电脑登录」;Deep Link `fengyingoa://qr`。 +- 登出不带 refresh 时只撤 web/desktop,不踢手机。 + +### 3.3 安卓 + +- `applicationId`:`com.fengying.oa.mobile`。`namespace` 仍是 `com.fysxkj.fengying_oa`,不要改混。 +- 当前 release 用 **debug 签名**。华为控制台正式 SHA256 对不上 debug,HMS token 可能拿不到。 +- **不要**加 `com.huawei.agconnect` Gradle 插件(AGP 8.6 会炸)。HMS 依赖 + `agconnect-services.json` + Manifest `appid` 留着即可。 + +### 3.4 数据范围 + +`AuthUser.dataScope`:`ALL` / `DEPT` / `SELF`。列表过滤在 `apps/api/src/common/data-scope.ts`。改列表不要只改前端。 + +--- + +## 4. 角色、权限、菜单 + +菜单源:`apps/api/prisma/menus.ts`,写入 `Permission`(`permType=MENU`)。 +`admin` / `owner` 前端 `canSee` 直接放行全部。其他人比 `permissions` 前缀。 + +种子角色(`prisma/seed.ts`,生产还会有导入岗位): + +| code | 名称 | dataScope | 大致权限 | +|---|---|---|---| +| admin | 系统管理员 | ALL | * | +| owner | 企业负责人 | ALL | * | +| biz_director | 商务总监 | DEPT | office/party/seal/bid/contract/finance/report | +| biz_staff | 商务专员 | SELF | office/party/seal/bid/contract | +| tech_director | 技术总监 | DEPT | office/bid/project/report | +| rd_director / 3d_director / video_director / material_director | 各技术条线总监 | DEPT | 见 `role-groups.ts` | +| finance | 财务 | ALL | office/finance/contract/report | +| hr | 人事 | ALL | office/hr + 人事设置 | +| employee / pm / 各 *\_staff | 员工/项目经理 | SELF | office + 工时等 | +| seal / asset / cashier … | 导入岗位 | 视脚本 | `fix-oa-posts.ts` | + +总监集合:`DIRECTOR_ROLES`。可发短信:`SMS_SEND_ROLES`。可派工作:`WORK_ASSIGN_ROLES`。 +`bid-workflow.ts` 仍保留旧的 `SPECIAL_COORDINATORS` 常量用于兼容,但当前特殊筛选/特殊制标的办理人加载逻辑是全体在职人员,禁止再改回写死两人。 + +首次登录非 admin:改密 + 完善档案(`/office/onboard`)。`mustChangePassword` / `profileCompletedAt`。 + +Web 侧栏:`AppLayout` 用 `/auth/me` 带回的 `menus`。 +发公告:`hr` 与各总监(`NOTICE_PUBLISH_ROLES` in `App.tsx`)。 + +--- + +## 5. Web 功能全表(按侧栏) + +路由全部在 `apps/web/src/App.tsx`。没有单独的移动端 Web。 + +### 5.1 工作台 `/workbench` + +`OverviewPage`:待办/待审批数字、天气问候、快捷入口。API:`GET /office/overview`、`GET /office/weather`。 + +### 5.2 个人办公 + +| 菜单 | 路径 | 页面 | API 要点 | 说明 | +|---|---|---|---|---| +| 待办事项 | `/office/todos` | OfficeTodosPage | `GET /office/todos` `PATCH /office/todos/:id` | 待办可标已办 | +| 待我审批 | `/office/approvals` | OfficeApprovalsPage | `GET /office/approvals` `POST .../decide` | 统一审批入口 | +| 我的申请 | `/office/flow` | MyFlowPage | `GET /office/flow` | 只跟踪自己的单 | +| 我的日程 | `/office/calendar` | CalendarPage | `GET /office/calendar` | 含证照/印章逾期、开标等 | +| 工作汇报 | `/office/reports` | WorkReportPage | `/office/reports*` | 可从工时草稿带入 | +| 我的费用 | `/office/expenses` | MyExpensesPage + Expense/Loan 详情 | `/office/my-expenses` `/expenses` `/loans` | 员工发起报销/借款 | +| 人事申请 | `/office/hr-apply` | HrApplyPage / HrLeaveDetailPage | `/leave-requests` | 请假加班出差外出离职晋升 | +| 用章申请 | `/office/seal-apply` | SealRequestListPage mode=apply | `/seal-requests` | 发起;台账在证照用章 | +| 办公申请 | `/office/apply` | OfficeApplyPage | `/office/applies` | 购置/领用/用车 | +| 公司公告 | `/office/notices` | NoticePage / NoticeDetailPage | `/office/notices` | 员工阅读 | +| 发送短信 | `/office/sms` | OfficeSmsPage | `/sms/*` | 总监/人事/admin | +| 工作安排 | `/office/assign` | WorkAssignPage | `/office/work-assignments` | 总监派任务 | +| 消息 | `/office/im` | ImPage | `/im/*` | Web IM + 通话弹层 | +| 员工通讯 | `/office/directory` | DirectoryPage | `/staff` `/directors` | 点人可开聊 | + +出差通过后应落差旅报销草稿(人事申请下游)。 + +### 5.3 往来单位 + +| 菜单 | 路径 | 页面 | API | +|---|---|---|---| +| 招标单位 | `/party/tenders` | TenderPartyPage | `/parties` | +| 参与主体 | `/party/entities` | LegalEntityPage | `/legal-entities` | +| 联系名录 | `/party/contacts` | ContactDirectoryPage | `/contacts` | + +本机已洗过 5 家主体:随行科技(默认,历史投标多挂这里)、风影软件、风影工程、山东鲁兵、隆创。脚本 `prisma/wash-legal-entities.ts`。OA dump 几乎没有「软件/工程」历史投标,不会自动拆单。 + +### 5.4 证照用章 + +| 菜单 | 路径 | 页面 | API | +|---|---|---|---| +| 资质台账 | `/seal/qualifications` | QualificationListPage | `/qualifications` | +| 证照借用 | `/seal/borrows` | BorrowListPage | `/credential-borrows` | +| 用章登记 | `/seal/requests` | SealRequestListPage mode=register | `/seal-requests` | +| 印章台账 | `/seal/registry` | SealRegistryPage | `/seals` | + +印章状态:`IN_STOCK` 在库,`OUT` 外带(Web 台账有特判)。外带审批通过写归还待办。 + +### 5.5 投标管理(核心) + +状态机:`apps/api/src/bidding/bid-workflow.ts` +列表桶:`BidListPage` + `?bucket=` + +``` +普通 FLOW: + 项目筛选 TECH_INITIAL → FETCH → TECH_FINAL + → 标书制作 DRAFTING → REVIEW1 → REVIEW2 → PRINT + → 投标执行 LIST → PENDING + → 开标结果 WON / LOST / FAILED + 随时可 TERMINATED(权限内) + +特殊 SPECIAL:特殊筛选 / 特殊制标的办理人从**全公司在职人员**中选择,不再写死刘运泽、陈铖。 +``` + +| 菜单 | 路径 | bucket | +|---|---|---| +| 项目筛选 | `/bid/screening` | screening(可新建) | +| 特殊筛选 | `/bid/special-screening` | 特殊 | +| 标书制作 | `/bid/drafting` | drafting | +| 特殊制标 | `/bid/special-drafting` | drafting+special | +| 投标执行 | `/bid/execution` | execution | +| 开标结果 | `/bid/result` | result | +| 终止投标 | `/bid/terminated` | terminated | + +详情 `/bid/cases/:id`:`BidDetailPage`。新建:`BidCreatePage`。 +中标:`ensureWonLedger` 立刻落 **合同草稿 + 项目台账**(幂等)。 +未中/终止:可「编辑」登记对方中标单位/金额 → 报表「落标对照」。 +API:`/bid-cases`,`PATCH /bid-cases/:id/competitor`。 + +### 5.6 合同管理 + +中标自动生成 `kind=TENDER` 草稿。直签/其他在本页新建,不互相重复。 + +| 菜单 | 路径 | 说明 | +|---|---|---| +| 投标合同 | `/contract/tender` | 中标来的 | +| 直签合同 | `/contract/direct` | 不经投标 | +| 其他合同 | `/contract/incremental` | 其余 | +| 人事合同 | `/contract/hr` | `LaborContractPage` `/labor-contracts` | + +详情 `/contract/:id`。API `/contracts`。 + +### 5.7 资产管理 + +数字资产 `/asset/digital`、固定资产 `/asset/fixed`:`AssetRegistryPage`,`/registered-assets`。 +状态含 `IN_STOCK` / `PENDING_IN` / 占用。入库:`POST /registered-assets/:id/inbound`。 +领用占用走办公申请,不在本页把保管人改掉。 + +### 5.8 项目管理 + +中标自动写入台账。直签可在台账补登。 + +| 菜单 | 路径 | API | +|---|---|---| +| 项目台账 | `/project/list` | `/projects` | +| 任务分解 | `/project/tasks` | `/project-tasks` | +| 工时填报 | `/project/timesheets` | `/timesheets` | +| 资源看板 | `/project/resources` | OpsPages ResourceBoardPage | +| 项目验收 | `/project/acceptance` | 同一 ProjectListPage mode=acceptance | +| 采购申请 | `/project/purchases` | PurchasePage(购置审批下游) | +| 器材占用 | `/project/assets` | AssetPage 占用记录 | + +详情 `/project/:id`。预算/报销:商务差旅进商务,技术部门差旅/工时/采购进技术(页面 description 已写)。 + +### 5.9 财务收支 + +员工在「我的费用」发起;财务在本模块确认、付款、开票。草稿财务看不到(`canSeeSubmittedFinance`)。 + +| 菜单 | 路径 | API | +|---|---|---| +| 费用报销 | `/finance/expenses` | `/expenses` | +| 借支记录 | `/finance/loans` | `/loans` | +| 投标/履约/质保金 | `/finance/bonds/*` | `/bonds` type=TENDER/PERFORMANCE/WARRANTY | +| 开票收款 | `/finance/invoices` | `/invoices` | +| 付款执行 | `/finance/payments` | `/payments` | + +报销必须挂费用对象:投标 / 正式项目 / 部门 / 行政,规则 `assertCostObject`。中标前挂投标,中标后挂项目,不要同时挂。 + +### 5.10 组织人事 + +| 菜单 | 路径 | 说明 | +|---|---|---| +| 在职员工 | `/hr/employees` | 档案、证件、银行卡 | +| 组织管理 | `/hr/org` | 部门树 | +| 入转调离 | `/hr/lifecycle` | EmploymentEvent | +| 假勤结果 | `/hr/attendance` | 月导入 + 打卡 | +| 薪酬核算 | `/hr/payroll` | 核算批次 | +| 绩效发放 | `/hr/performance` | | +| 发布公告 | `/hr/notices` | 人事发 | +| 规章制度 | `/hr/policies` | 同类 Notice,kind 不同 | + +### 5.11 经营报表 + +| 菜单 | 路径 | API | +|---|---|---| +| 投标转化 | `/report/conversion` | `/reports/bid-conversion` | +| 落标对照 | `/report/lost` | `/reports/competitor-wins` | +| 项目利润 | `/report/profit` | `/reports/profit` | +| 部门人效 | `/report/efficiency` | `/reports/efficiency` | +| 资金占用 | `/report/cash` | `/reports/cash` | + +页面在 `OpsPages.tsx`。 + +### 5.12 系统设置 + +| 菜单 | 路径 | 说明 | +|---|---|---| +| 员工账号 | `/system/acl` | 用户/角色 | +| 人事参数 | `/system/hr-params` | | +| 考勤设置 | `/system/attendance-rules` | | +| 数据字典 | `/system/dicts` | | +| 权限设置 | `/system/perms` | 菜单授权 | +| 编号规则 | `/system/numbers` | `nextBizNo` | +| 短信服务 | `/system/sms` | 签名/模板/绑定事件 | +| 消息通道 | `/system/messages` | 占位页,文案可能过时(IM 已做) | +| 手机端升级 | `/system/app-release` | 写 `app-release.json` | +| 操作日志 | `/system/logs` | | + +公开:`GET /system/app-release`(手机 OTA)。 + +--- + +## 6. 核心流程(改业务先对这条链) + +### 投标 → 合同/项目 + +筛选 → 取标 → 终审 → 制标 → 一审二审 → 印封 → 执行 → 开标。 +**WON** → 合同草稿 + 项目台账。售前报销仍挂投标;正式花费挂项目。 + +### 报销 / 借款 + +员工草稿 → 提交 PENDING → 财务/总监审批 APPROVED → 付款执行。借款还有 OPEN / OFFSET 冲账。 + +### 办公申请 + +发起 PENDING → 审批(admin/owner/hr/biz_director)→ 按 kind 写采购/占用/待入库。 + +### 用章 + +申请 → 审批 → 外带则归还待办;台账 status 在库/外带。 + +### 人事假勤 + +`LeaveRequest.kind`:LEAVE / OVERTIME / TRIP / OUT / RESIGN / PROMOTE。出差通过 → 报销草稿。 + +统一待办 `TodoTask`、统一审批 `ApprovalTask`(`bizType` 区分投标/合同/报销/用章/人事/办公)。 +待我审批一页消化,不要为每个 biz 再做孤立审批页。 + +--- + +## 7. API 地图 + +前缀均 `/api/v1`。鉴权 JWT,除标注 Public。 + +| 模块 | 文件 | 主要路径 | +|---|---|---| +| 登录 | `auth/` | `/auth/login` `/auth/refresh` `/auth/me` `/auth/logout` `/auth/profile` `/auth/password` `/auth/qr/*` | +| 办公 | `office/` | `/office/*` 待办审批日程公告申请汇报工作安排天气地理 | +| 投标 | `bidding/` | `/bid-cases` | +| 合同 | `contract/` | `/contracts` | +| 项目 | `project/` | `/projects` `/project-tasks` `/timesheets` | +| 用章 | `seal/` | `/seal-requests` `/seals` `/qualifications` `/credential-borrows` | +| 财务 | `finance/` | `/expenses` `/loans` `/bonds` `/invoices` `/payments` | +| 人事 | `hr/` | `/employees` `/leave-requests` `/payroll` `/attendance` `/labor-contracts` `/org` | +| 主数据 | `master/` | `/parties` `/legal-entities` `/contacts` `/registered-assets` | +| 系统 | `system/` | `/system/menus` `/system/acl` `/system/dicts` `/system/numbers` `/system/app-release` `/system/logs` | +| 报表 | `report/` | `/reports/*` | +| 文件 | `files/` | `POST /files` `GET /files/:id` | +| IM | `im/` | `/im/conversations` `/im/messages` `PATCH /im/messages/:id` `/im/calls*` `/im/read` | +| 推送 | `push/` | 设备 token 注册与下发 | +| 短信 | `sms/` | 签名模板发送 OTP | +| 通讯录 | `business/` | `/staff` `/directors` | +| 健康 | `health/` | `/health` | +| 协议 | `legal/` | 用户协议页 | + +响应外包一层 `{ data }`,Web 用 `unwrapList` / `body.data`。手机 `oa_client.dart` 同样解包。 +错误:`HttpErrorFilter`。请求号:`X-Request-Id`。 + +地理:`/office/geo/*` 走腾讯位置服务,`TENCENT_MAP_KEY`。 + +--- + +## 8. 数据模型(Prisma) + +完整定义:`apps/api/prisma/schema.prisma`。 + +**账号**:User、RefreshToken(`clientKind`)、AuthQrTicket、Role、Permission、UserRole、PushDevice +**组织人事**:Department、Employee、LaborContract、EmploymentEvent、AttendanceMonth、AttendancePunch、LeaveRequest、HrSetting、AttendanceRule、PayrollRun、PayrollItem +**主数据**:LegalEntity、BusinessParty、PartyContact、DictItem、NumberRule、Qualification、SealItem、RegisteredAsset +**投标**:BidCase(含 `winnerName/Amount/Note`)、BidReviewer、BidAssignee、BidDocVersion、BidActionLog +**合同项目**:Contract、ContractReviewer、PaymentMilestone、Project、ProjectBudget、ProjectTask、Timesheet、ProjectAcceptance、ProjectAcceptMaterial、ProjectChange +**财务**:ExpenseClaim、ExpenseLine、LoanRecord、BondRecord、InvoiceRecord、PaymentRecord、PurchaseRequest +**办公**:TodoTask、ApprovalTask、WorkReport、WorkAssignment、OfficeApply、Notice、AssetOccupancy、CredentialBorrow、SealRequest +**IM**:ImConversation、ImConversationMember、ImMessage(`fingerprint` 唯一)、ImReadCursor、ImMessageReceipt、ImCall、ImCallMember、ImCallSignal +**其它**:FileAsset、SmsSendLog、SmsEventBind、SmsOtp、OperationLog + +编号:`NumberRule` + `src/system/numbering.ts`。注意:**项目筛选、特殊筛选的招标编号和合同编号必须手填,系统不得另生成隐藏编号**;报销等内部单据仍可使用系统编号。 + +--- + +## 9. 手机端 + +Flutter。版本:`apps/native/lib/app_config.dart` + `pubspec.yaml` + `test/widget_test.dart` + `apps/api/data/app-release.json`,四者一起改。 + +默认连生产 API / IM。底栏四页:`messages_page`、`workbench_page`、`directory_page`、`mine_page`。 + +工作台宫格来自登录菜单。`open_module.dart` / `module_list_page.dart` 把菜单码映射到 REST 列表(投标→`/bid-cases` 等)。系统设置、发短信、工作台里的「消息」入口故意不打开(消息在底栏)。详情 `record_detail_page.dart` + `biz_route.dart` 反查详情 API。中文:`labels.dart`。 + +聊天/通话的关键文件:聊天 `chat_page.dart`,来电 `home_shell.dart` + `call_page.dart`。当前版本为 3.1.8+318;音视频、截屏提醒、撤回再编辑、会话已读后保留等需求必须按 `/opt/HANDOFF.md` 的“未完成/需真机回归”继续验证,不能仅凭旧交接宣称已修。 + +打包: + +```bash +cd /opt/apps/native && flutter build apk --release && bash scripts/publish-apk.sh +``` + +产物:`/opt/apps/native/dist/fengying-oa.apk`。本机 root 跑 Flutter 会警告,仍能编过。下一版必须在 **3.1.8+318** 基础上递增。 + +OTA:比较 `build`。后台「手机端升级」。 + +--- + +## 10. IM + +协议:4 字节大端长度 + UTF-8 JSON Protocal。登录用 OA accessToken 调 `GET /auth/me`。 + +**新 `server.js`**:`Map>`,同一账号多端/重连 **不再 kick**。旧版会踢旧连接,客户端若把 `_want=false` 会永久断线。 +生产必须换新 `server.js` 长连接才稳。新 APK 即使对旧 IM 也不会再「被踢后永不再连」。 + +HTTP 消息仍落 Postgres。TCP 负责推送。通话信令走 `/im/calls/:id/signal`(HTTP 轮询),WebRTC + 国内 STUN。 + +`PATCH /im/messages/:id`:2 分钟内改自己的文本。 + +--- + +## 11. 文件、短信、推送、地图 + +- 文件:`FilesService` 存 MinIO(本机 docker)或本地 `uploads/`。下载带鉴权。手机 `AuthImage` / `fileBytes` 有内存缓存。 +- 短信:阿里云 Dysmsapi。系统设置申请签名/模板,事件绑定在 `SmsEventBind`。投标节点会 `emitSms`。 +- 推送:vivo / OPPO / 魅族 / 华为。设备 `PushDevice`。华为 App ID `118301849`。 +- 地图:腾讯。选点 `location_pick_page.dart` + `tencent-lbs`;POI 用真实距离,不要写死「100m内」。 + +--- + +## 12. 前端约定 + +**Web** + +- 新列表:Ant Design Table + 中文 `zh()` / `StatusTag`,不要直接渲染枚举码。 +- 申请类列表:`ApplyBucketBar` + URL `bucket`。 +- 抽屉表单:`FormDrawer.tsx`。附件:`FileAttachments.tsx`。 +- 枚举中文:`apps/web/src/labels.ts`,与手机 `labels.dart` 对齐。 +- token 只放 sessionStorage。 + +**手机** + +- 企业微信风:`theme.dart`、`widgets/wecom.dart`。 +- 英文字段:补 `fieldZh` / `zhMap`,不要在单个页写死。`OUT` 人事=外出、印章=外带,有冲突时按字段上下文。 + +--- + +## 13. 脚本与历史数据 + +`apps/api/prisma/` 里常见脚本(跑前看文件头,生产库只读): + +| 脚本 | 用途 | +|---|---| +| seed.ts | 空库演示 | +| sync-menus.ts | 菜单/权限 | +| import-hr.ts / import-prod.ts | 从旧 OA 导人事/业务 | +| wash-legal-entities.ts | 参与主体清洗 | +| wash-oa-ledger.ts / wash-oa-flow-finance.ts | 台账/财务清洗 | +| sync-oa-attachments.ts | 附件 | +| grant-admin-equivalents.ts / ensure-prod-admin.ts | 管理员 | +| fix-oa-posts.ts / fix-expense-applicants.ts | 岗位与报销申请人 | + +旧 OA 在生产 MySQL `oa`(只读扒数,用户禁止改生产旧库、禁止整用户过来)。dump 曾放 `/opt/import/prod/`。 + +--- + +## 14. 密钥与账号(Codex 直接用,不必再问) + +### 生产 SSH + +``` +主机:47.96.23.244 +用户:root +密码:VYTnNdfFfrX7xTP +代码目录:/data/fengying-oa/ +``` + +```bash +export SSHPASS='VYTnNdfFfrX7xTP' +RSH='ssh -o StrictHostKeyChecking=no' +DEST=root@47.96.23.244 +sshpass -e ssh -o StrictHostKeyChecking=no "$DEST" 'hostname; systemctl is-active fengying-api fengying-im' +``` + +发版: + +```bash +export SSHPASS='VYTnNdfFfrX7xTP' +RSH='ssh -o StrictHostKeyChecking=no' +DEST=root@47.96.23.244 +# 先 nest build、确认 APK 已 publish +sshpass -e rsync -az -e "$RSH" /opt/apps/api/dist/ $DEST:/data/fengying-oa/apps/api/dist/ +sshpass -e rsync -az -e "$RSH" /opt/apps/api/data/app-release.json $DEST:/data/fengying-oa/apps/api/data/app-release.json +sshpass -e rsync -az -e "$RSH" /opt/apps/web/dist/ $DEST:/data/fengying-oa/apps/web/dist/ # 若本机有最新 web 构建 +sshpass -e rsync -az -e "$RSH" /opt/apps/im-server/server.js $DEST:/data/fengying-oa/apps/im-server/server.js +sshpass -e rsync -az -e "$RSH" /opt/apps/native/dist/fengying-oa.apk $DEST:/data/fengying-oa/apps/web/dist/fengying-oa.apk +sshpass -e ssh -o StrictHostKeyChecking=no "$DEST" 'systemctl restart fengying-api fengying-im; sleep 2; systemctl is-active fengying-api fengying-im' +``` + +生产 API `.env` 在远端 `/data/fengying-oa/apps/api/.env`。上去先 `cat`,**不要用本机 development 的 JWT/Postgres 覆盖生产**。发 IM/API 后确认 Prisma 已有 `RefreshToken.clientKind`、`AuthQrTicket`、`BidCase.winner*`。 + +### 旧 OA MySQL(只读) + +``` +用户:root +密码:BtH9ywG8gFTXG9w! +库:oa +``` + +### 本机 API `apps/api/.env` + +``` +NODE_ENV=development +APP_PORT=3000 +DATABASE_URL=postgresql://ops:ops_dev_only@localhost:5432/enterprise_ops +REDIS_URL=redis://localhost:6379 +MINIO_ACCESS_KEY=opsadmin +MINIO_SECRET_KEY=opsadmin_dev_only +JWT_ACCESS_SECRET=change-me-access-secret-dev-only +JWT_REFRESH_SECRET=change-me-refresh-secret-dev-only +POSTGRES_USER=ops +POSTGRES_PASSWORD=ops_dev_only +POSTGRES_DB=enterprise_ops + +ALIYUN_SMS_ACCESS_KEY_ID=LTAI5t9DdPycJp7sabQyEWRc +ALIYUN_SMS_ACCESS_KEY_SECRET=RPFVqKDUpEvxo3IsqlxLB5qIywfheM +ALIYUN_SMS_ENDPOINT=dysmsapi.aliyuncs.com +ALIYUN_SMS_REGION=cn-hangzhou +ALIYUN_SMS_DEFAULT_SIGN=江苏风影随行科技 +ALIYUN_RAM_LOGIN=yongkangdai10@1801267455383083.onaliyun.com + +TENCENT_MAP_KEY=L6NBZ-47KLI-OL7G2-UMROZ-XPI3T-SLFC7 + +IM_TCP_PORT=8901 +IM_HTTP_PORT=8902 +IM_INTERNAL_TOKEN=fengying-im-internal + +VIVO_PUSH_APP_ID=106114371 +VIVO_PUSH_APP_KEY=b521c805229a00e85a53d4b535a0cafe +VIVO_PUSH_APP_SECRET=2e1536ed-ed2f-4cd6-88d1-67aa9a93eeba + +OPPO_PUSH_APP_ID=37438377 +OPPO_PUSH_APP_KEY=e23832b52fbb46e18de6eef5affda2e9 +OPPO_PUSH_APP_SECRET=e9cf0bf35052484ebcaf15d56576468b +OPPO_PUSH_MASTER_SECRET=e9cf0bf35052484ebcaf15d56576468b + +MEIZU_PUSH_APP_ID=157643 +MEIZU_PUSH_APP_KEY=4263a8d18de442b6a17203866ce1546d +MEIZU_PUSH_APP_SECRET=2a0bafc2d47a470f8b2aad3f182d2377 + +HUAWEI_PUSH_APP_ID=118301849 +HUAWEI_PUSH_CLIENT_ID=118301849 +HUAWEI_PUSH_CLIENT_SECRET=85c01c5c853d571e2d83fd8629efb21e9b2cad0bdfadd6f32c2c5b42e38d7888 +``` + +本机 IM `.env` 写的是 `OA_API_URL=http://127.0.0.1:3010/api/v1`,本机 nest 常在 3000,连本机 IM 请改 3000。生产 systemd:`OA_API_URL=http://127.0.0.1:3010/api/v1`。 + +### 华为 AGC + +- 包名 `com.fengying.oa.mobile`,App ID `118301849` +- 服务端 Client Secret:`85c01c5c853d571e2d83fd8629efb21e9b2cad0bdfadd6f32c2c5b42e38d7888` +- JSON:`apps/native/android/app/agconnect-services.json` +- JSON `client.client_id`:`1993069421077225280` +- JSON `client.client_secret`:`93585E101B911EFF0F99F215050CD3E4297F67CB004E0D8E070AAB1EEDD7E725` +- JSON `client.api_key`:`DgEDAFLpMeUxEsIEiKams+kYWThvAg0ioMuL1rW9BEP4i71BuChEPD7Udg1AbDKbg+ljwD+pBACxktLqHwmUlSxOCwsLHiRAKQDBFw==` +- `cp_id`:`10086000847744494`;`product_id`:`101653523864520415` + +### OPPO Maven + +``` +https://maven.columbus.heytapmobi.com/repository/releases/ +username: nexus +password: c0b08da17e3ec36c3870fed674a0bcb36abc2e23 +``` + +### 腾讯地图 / vivo Manifest + +- `TENCENT_MAP_KEY=L6NBZ-47KLI-OL7G2-UMROZ-XPI3T-SLFC7` +- Manifest vivo:`api_key=b521c805229a00e85a53d4b535a0cafe`,`app_id=106114371` + +### 本机演示账号 + +`admin` / `Admin@123456`。种子其他人常见 `Demo@123456` 或导入后 `123456`。不要拿去撞生产。 + +--- + +## 15. 发生产清单 + +1. `npx nest build`,同步 `apps/api/dist/` +2. 若改了 Web:`pnpm --filter web build`,同步 `apps/web/dist/` +3. 同步 `server.js`、APK、`app-release.json` +4. `systemctl restart fengying-api fengying-im` +5. 后台确认手机端升级为当前最新 build(现为 318) +6. 真机覆盖安装当前最新版(现为 3.1.8) + +**APK 保留规则:**云主机只保留当前 OTA 指向的最新安装包(对外统一为 `apps/web/dist/fengying-oa.apk`)。每次发版先精确列出 `/data/fengying-oa` 下的 `.apk` / `.aab`,确认新包可下载后删除其他旧包,不要在 `apps/native/dist` 等目录留历史副本。 + +API/IM/APK 是否为同一版必须分别核对,不能只看网页或 OTA JSON。 + +--- + +## 16. 已知坑 / 未完成 + +1. 生产 API 已同步 2026-09-01 的数据修复代码;Web、IM、APK 后续每次仍要分别核对版本和构建时间。 +2. 华为正式推送:缺正式签名 + AGConnect 插件未进构建。 +3. `OUT` 中文:人事外出 vs 印章外带。 +4. 系统设置「消息通道」文案还写「本期不做原生 IM」,已过时。 +5. Web IM 与手机 IM 都可用,但通话依赖双方打开页 + STUN,失败别先改来电弹层。 +6. 手机工作台对报表/部分系统页映射为 `null`,点了可能没页。 +7. 打开 docx 依赖手机已装 WPS/Office。 +8. 历史投标主体不会自动拆到软件/工程公司。 +9. `apps/native/README.md`、部分 api README 版本号过时,以 `app_config.dart` 和本文件为准。 +10. 薪酬/绩效/资源看板有页面,深度以现网数据为准,缺的是补流程不是再铺空壳。 +11. 问题视频:`/opt/问题视频/`。来电测试对象常为「戴永康」。 + +--- + +## 17. 建议接着做的顺序 + +1. 用第 14 节发生产(API + Web + IM + APK + OTA),不要再问密码。 +2. 真机覆盖 2.3.2:来电能关、消息上屏、图片/地图能点、印章「在库」。 +3. 网页重新登录:确认扫码登电脑、Web 互踢、侧栏「落标对照」。 +4. 再改业务:遵守第 3 节办公规则;改投标走 `bid-workflow.ts`,不要只改一个列表页。 +5. 英文残留:补 `labels.ts` / `labels.dart`。 +6. 华为推送真机(正式签名)。 +7. 用户新需求再开功能,不要平行重构目录。 + +改完菜单同步 `sync-menus.ts`。改完枚举两边 labels 一起改。改完版本四个文件一起改。 diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..efb4f40 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +up: + docker compose up -d + +migrate: + cd apps/api && pnpm exec prisma migrate dev --name init + +seed: + cd apps/api && pnpm prisma:seed + +api: + cd apps/api && pnpm start:dev + +web: + cd apps/web && pnpm dev diff --git a/README.md b/README.md new file mode 100644 index 0000000..7830992 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +# 企业综合运营管理平台 + +内部使用。先上 Web,手机和桌面端以后再说。 + +## 启动 + +```bash +cp .env.example .env +docker compose up -d +cd apps/api && pnpm exec prisma migrate dev --name init && pnpm prisma:seed && pnpm start:dev +# 另一个终端 +cd apps/web && pnpm dev +``` + +打开 http://localhost:5173 ,账号 `admin` / `Admin@123456`。 + +Swagger:http://localhost:3000/api/v1/docs + +## 模块 + +个人办公、往来单位、证照用章、投标管理、合同管理、项目管理、财务收支、组织人事、经营报表、系统设置。 + +投标筛选可以新建。其余页面已接通列表接口,数据会随使用增加。 diff --git a/WINDOWS_BUILD_3.1.4.md b/WINDOWS_BUILD_3.1.4.md new file mode 100644 index 0000000..e89b911 --- /dev/null +++ b/WINDOWS_BUILD_3.1.4.md @@ -0,0 +1,13 @@ +# 风影办公 Windows 3.1.7 构建 + +在 Windows PowerShell 中解压源码后执行: + +```powershell +flutter config --enable-windows-desktop +flutter pub get +flutter build windows --release +``` + +产物目录:`build\windows\x64\runner\Release\`。发布时请连同该目录内 DLL 和 `data` 目录一起分发,不能只复制 exe。 + +本版本的 Android 与 Windows 共用 Flutter 业务代码,包含通话状态同步和工作台业务卡片转发功能。 diff --git a/WINDOWS_BUILD_3.1.7.md b/WINDOWS_BUILD_3.1.7.md new file mode 100644 index 0000000..2d9254b --- /dev/null +++ b/WINDOWS_BUILD_3.1.7.md @@ -0,0 +1,11 @@ +# 风影办公 Windows 3.1.7 构建 + +在 Windows PowerShell 中解压源码后执行: + +```powershell +flutter config --enable-windows-desktop +flutter pub get +flutter build windows --release +``` + +产物目录:`build\\windows\\x64\\runner\\Release\\`。发布时请连同该目录内 DLL 和 `data` 目录一起分发,不能只复制 exe。 diff --git a/agconnect-services.json b/agconnect-services.json new file mode 100644 index 0000000..d57482c --- /dev/null +++ b/agconnect-services.json @@ -0,0 +1 @@ +{"agcgw":{"url":"connect-drcn.dbankcloud.cn","backurl":"connect-drcn.hispace.hicloud.com","websocketurl":"connect-ws-drcn.hispace.dbankcloud.cn","websocketbackurl":"connect-ws-drcn.hispace.dbankcloud.com"},"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.hicloud.com","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.hispace.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-ws-dre.hispace.dbankcloud.cn","DE_back":"connect-ws-dre.hispace.dbankcloud.com"},"client":{"cp_id":"10086000847744494","product_id":"101653523864520415","client_id":"1993069421077225280","client_secret":"93585E101B911EFF0F99F215050CD3E4297F67CB004E0D8E070AAB1EEDD7E725","project_id":"101653523864520415","app_id":"118301849","api_key":"DgEDAFLpMeUxEsIEiKams+kYWThvAg0ioMuL1rW9BEP4i71BuChEPD7Udg1AbDKbg+ljwD+pBACxktLqHwmUlSxOCwsLHiRAKQDBFw==","package_name":"com.fengying.oa.mobile"},"oauth_client":{"client_id":"118301849","client_type":1},"app_info":{"app_id":"118301849","package_name":"com.fengying.oa.mobile"},"service":{"analytics":{"collector_url":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_cn":"datacollector-drcn.dt.hicloud.com,datacollector-drcn.dt.dbankcloud.cn","collector_url_de":"datacollector-dre.dt.hicloud.com,datacollector-dre.dt.dbankcloud.cn","collector_url_ru":"datacollector-drru.dt.dbankcloud.ru,datacollector-drru.dt.hicloud.com","collector_url_sg":"datacollector-dra.dt.hicloud.com,datacollector-dra.dt.dbankcloud.cn","resource_id":"p1","channel_id":""},"ml":{"mlservice_url":"ml-api-drcn.ai.dbankcloud.com,ml-api-drcn.ai.dbankcloud.cn"},"cloudstorage":{"storage_url":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg":"https://ops-dra.agcstorage.link","storage_url_de":"https://ops-dre.agcstorage.link","storage_url_cn":"https://agc-storage-drcn.platform.dbankcloud.cn","storage_url_ru_back":"https://agc-storage-drru.cloud.huawei.ru","storage_url_sg_back":"https://agc-storage-dra.cloud.huawei.asia","storage_url_de_back":"https://agc-storage-dre.cloud.huawei.eu","storage_url_cn_back":"https://agc-storage-drcn.cloud.huawei.com.cn"},"search":{"url":"https://search-drcn.cloud.huawei.com"},"edukit":{"edu_url":"edukit.cloud.huawei.com.cn","dh_url":"edukit.cloud.huawei.com.cn"}},"region":"CN","configuration_version":"3.0","appInfos":[{"package_name":"com.fengying.oa.mobile","client":{"app_id":"118301849"},"oauth_client":{"client_id":"118301849","client_type":1},"app_info":{"app_id":"118301849","package_name":"com.fengying.oa.mobile"}}]} \ No newline at end of file diff --git a/apps/api/.prettierrc b/apps/api/.prettierrc new file mode 100644 index 0000000..a20502b --- /dev/null +++ b/apps/api/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "trailingComma": "all" +} diff --git a/apps/api/README.md b/apps/api/README.md new file mode 100644 index 0000000..d375c6c --- /dev/null +++ b/apps/api/README.md @@ -0,0 +1,47 @@ +# 企业综合运营管理平台 + +内部运营系统。当前先交付 **Web 端**,手机端和桌面端后续用同一套 API 再做。 + +## 技术栈 + +- 后端:NestJS 11 + Prisma 6 + PostgreSQL 16 +- 前端:React 18 + Vite 5 + Ant Design 5 +- 依赖:Redis、MinIO(本地 Docker) + +## 目录 + +``` +apps/api 后端 +apps/web Web +infra/ 预留脚本 +docs/ 架构说明 +``` + +## 启动 + +```bash +cp .env.example .env +docker compose up -d +pnpm --filter api exec prisma migrate dev --name init +pnpm --filter api prisma:seed +pnpm --filter api start:dev +pnpm --filter web dev +``` + +- Web:http://localhost:5173 +- API:http://localhost:3000/api/v1 +- Swagger:http://localhost:3000/api/v1/docs + +演示账号:`admin` / `Admin@123456` +其他人:`owner` `bizdir` `bizstaff` `dirsoft` `dir3d` `dirfilm` `dirmaterial` `finance` `hr` `employee`,密码均为 `Demo@123456` + +## 测试 + +```bash +pnpm --filter api test +pnpm --filter api test:e2e +``` + +## 范围 + +本期把十个一级模块和二级菜单全部铺到 Web 上,登录、权限、投标筛选可写。合同、工时、薪酬等以台账和空数据为主,后续按模块补流程。不含外包、线索、仓库,也不做原生 IM。 diff --git a/apps/api/data/app-release.json b/apps/api/data/app-release.json new file mode 100644 index 0000000..8950118 --- /dev/null +++ b/apps/api/data/app-release.json @@ -0,0 +1,12 @@ +{ + "platform": "android", + "version": "3.1.10", + "build": 324, + "url": "https://oa.fysxkj.com/fengying-oa.apk", + "windowsUrl": "https://oa.fysxkj.com/fengying-oa-setup.exe", + "linuxUrl": "https://oa.fysxkj.com/fengying-oa-linux.tar.gz", + "linuxDebUrl": "https://oa.fysxkj.com/fengying-oa_amd64.deb", + "changelog": "桌面端功能补全:语音播放/录制/转写、位置/转发/编辑、工作台模块路由、Linux deb 安装包(Ubuntu/Debian/Deepin25)、OTA 修复。", + "force": false, + "publishedAt": "2026-09-02T07:15:00.000Z" +} diff --git a/apps/api/eslint.config.mjs b/apps/api/eslint.config.mjs new file mode 100644 index 0000000..4e9f827 --- /dev/null +++ b/apps/api/eslint.config.mjs @@ -0,0 +1,35 @@ +// @ts-check +import eslint from '@eslint/js'; +import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { + ignores: ['eslint.config.mjs'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + eslintPluginPrettierRecommended, + { + languageOptions: { + globals: { + ...globals.node, + ...globals.jest, + }, + sourceType: 'commonjs', + parserOptions: { + projectService: true, + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'warn', + '@typescript-eslint/no-unsafe-argument': 'warn', + "prettier/prettier": ["error", { endOfLine: "auto" }], + }, + }, +); diff --git a/apps/api/nest-cli.json b/apps/api/nest-cli.json new file mode 100644 index 0000000..f9aa683 --- /dev/null +++ b/apps/api/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..c659c77 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,110 @@ +{ + "name": "api", + "version": "0.0.1", + "description": "", + "author": "", + "private": true, + "license": "UNLICENSED", + "scripts": { + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", + "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", + "test:e2e": "jest --config ./test/jest-e2e.json", + "prisma:generate": "prisma generate", + "prisma:migrate": "prisma migrate dev", + "prisma:seed": "ts-node --transpile-only prisma/seed.ts" + }, + "dependencies": { + "@alicloud/dysmsapi20170525": "^4.6.0", + "@alicloud/openapi-client": "^0.4.15", + "@alicloud/openapi-core": "^1.0.8", + "@alicloud/tea-util": "^1.4.11", + "@darabonba/typescript": "^1.0.5", + "@nestjs/common": "^11.0.1", + "@nestjs/config": "^4.0.2", + "@nestjs/core": "^11.0.1", + "@nestjs/jwt": "^11.0.0", + "@nestjs/mapped-types": "^2.1.0", + "@nestjs/passport": "^11.0.5", + "@nestjs/platform-express": "^11.0.1", + "@nestjs/swagger": "^11.2.1", + "@nestjs/terminus": "^11.1.1", + "@nestjs/throttler": "^6.5.0", + "@prisma/client": "^6.19.0", + "argon2": "^0.45.1", + "class-transformer": "^0.5.1", + "class-validator": "^0.14.1", + "helmet": "^8.3.0", + "ioredis": "^6.0.0", + "livekit-server-sdk": "^2.18.0", + "passport": "^0.7.0", + "passport-jwt": "^4.0.1", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1", + "zod": "^3.24.2" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "^9.18.0", + "@nestjs/cli": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.1", + "@types/express": "^5.0.0", + "@types/jest": "^30.0.0", + "@types/node": "^24.0.0", + "@types/passport": "^1.0.17", + "@types/passport-jwt": "^4.0.1", + "@types/supertest": "^7.0.0", + "eslint": "^9.18.0", + "eslint-config-prettier": "^10.0.1", + "eslint-plugin-prettier": "^5.2.2", + "globals": "^17.0.0", + "jest": "^30.0.0", + "prettier": "^3.4.2", + "prisma": "6.19.0", + "source-map-support": "^0.5.21", + "supertest": "^7.0.0", + "ts-jest": "^29.2.5", + "ts-loader": "^9.5.2", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^5.7.3", + "typescript-eslint": "^8.20.0" + }, + "jest": { + "moduleFileExtensions": [ + "js", + "json", + "ts" + ], + "rootDir": "src", + "testRegex": ".*\\.spec\\.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + }, + "collectCoverageFrom": [ + "**/*.(t|j)s" + ], + "coverageDirectory": "../coverage", + "testEnvironment": "node" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "argon2", + "@prisma/client", + "prisma", + "@prisma/engines" + ] + }, + "prisma": { + "seed": "ts-node --transpile-only prisma/seed.ts" + } +} diff --git a/apps/api/pnpm-lock.yaml b/apps/api/pnpm-lock.yaml new file mode 100644 index 0000000..4efa1c0 --- /dev/null +++ b/apps/api/pnpm-lock.yaml @@ -0,0 +1,7234 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@nestjs/common': + specifier: ^11.0.1 + version: 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/config': + specifier: ^4.0.2 + version: 4.0.2(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2) + '@nestjs/core': + specifier: ^11.0.1 + version: 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/jwt': + specifier: ^11.0.0 + version: 11.0.0(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)) + '@nestjs/mapped-types': + specifier: ^2.1.0 + version: 2.1.0(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + '@nestjs/passport': + specifier: ^11.0.5 + version: 11.0.5(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0) + '@nestjs/platform-express': + specifier: ^11.0.1 + version: 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3) + '@nestjs/swagger': + specifier: ^11.2.1 + version: 11.2.1(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + '@nestjs/terminus': + specifier: ^11.1.1 + version: 11.1.1(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)(@prisma/client@6.19.0(prisma@6.19.0(typescript@5.9.3))(typescript@5.9.3))(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/throttler': + specifier: ^6.5.0 + version: 6.5.0(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)(reflect-metadata@0.2.2) + '@prisma/client': + specifier: ^6.19.0 + version: 6.19.0(prisma@6.19.0(typescript@5.9.3))(typescript@5.9.3) + argon2: + specifier: ^0.45.1 + version: 0.45.1 + class-transformer: + specifier: ^0.5.1 + version: 0.5.1 + class-validator: + specifier: ^0.14.1 + version: 0.14.1 + helmet: + specifier: ^8.3.0 + version: 8.3.0 + ioredis: + specifier: ^6.0.0 + version: 6.0.0 + passport: + specifier: ^0.7.0 + version: 0.7.0 + passport-jwt: + specifier: ^4.0.1 + version: 4.0.1 + reflect-metadata: + specifier: ^0.2.2 + version: 0.2.2 + rxjs: + specifier: ^7.8.1 + version: 7.8.2 + zod: + specifier: ^3.24.2 + version: 3.24.2 + devDependencies: + '@eslint/eslintrc': + specifier: ^3.2.0 + version: 3.3.6 + '@eslint/js': + specifier: ^9.18.0 + version: 9.39.5 + '@nestjs/cli': + specifier: ^11.0.0 + version: 11.0.24(@types/node@24.13.3)(prettier@3.9.6) + '@nestjs/schematics': + specifier: ^11.0.0 + version: 11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3) + '@nestjs/testing': + specifier: ^11.0.1 + version: 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)(@nestjs/platform-express@11.2.3) + '@types/express': + specifier: ^5.0.0 + version: 5.0.6 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 + '@types/node': + specifier: ^24.0.0 + version: 24.13.3 + '@types/passport': + specifier: ^1.0.17 + version: 1.0.17 + '@types/passport-jwt': + specifier: ^4.0.1 + version: 4.0.1 + '@types/supertest': + specifier: ^7.0.0 + version: 7.2.1 + eslint: + specifier: ^9.18.0 + version: 9.39.5(jiti@2.7.0) + eslint-config-prettier: + specifier: ^10.0.1 + version: 10.1.8(eslint@9.39.5(jiti@2.7.0)) + eslint-plugin-prettier: + specifier: ^5.2.2 + version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6) + globals: + specifier: ^17.0.0 + version: 17.11.0 + jest: + specifier: ^30.0.0 + version: 30.5.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)) + prettier: + specifier: ^3.4.2 + version: 3.9.6 + prisma: + specifier: 6.19.0 + version: 6.19.0(typescript@5.9.3) + source-map-support: + specifier: ^0.5.21 + version: 0.5.21 + supertest: + specifier: ^7.0.0 + version: 7.2.2 + ts-jest: + specifier: ^29.2.5 + version: 29.4.12(@babel/core@7.29.7)(@jest/transform@30.5.0)(@jest/types@30.5.0)(babel-jest@30.5.0(@babel/core@7.29.7))(jest-util@30.5.0)(jest@30.5.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)))(typescript@5.9.3) + ts-loader: + specifier: ^9.5.2 + version: 9.6.2(typescript@5.9.3)(webpack@5.106.2) + ts-node: + specifier: ^10.9.2 + version: 10.9.2(@types/node@24.13.3)(typescript@5.9.3) + tsconfig-paths: + specifier: ^4.2.0 + version: 4.2.0 + typescript: + specifier: ^5.7.3 + version: 5.9.3 + typescript-eslint: + specifier: ^8.20.0 + version: 8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + +packages: + + '@angular-devkit/core@19.2.24': + resolution: {integrity: sha512-Kd49warf6U/EyWe5BszF/eebN3zQ3bk7tgfEljAw8q/rX95UUtriJubWvp6pgzHfzBA4jwq8f+QiNZB8eBEXPA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/core@19.2.27': + resolution: {integrity: sha512-3amNzoCVSKd7ah6l6lBQL4onwwJvqvam7FMoQBILrxtW5LB5ezh8gMSPuA4zJjKjoRzf9uoWdlzqv/84I52xZA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + peerDependencies: + chokidar: ^4.0.0 + peerDependenciesMeta: + chokidar: + optional: true + + '@angular-devkit/schematics-cli@19.2.27': + resolution: {integrity: sha512-wHYH6SVXVykhLzovUHtYor3Nl4SpIiITi7r9DQDaKYUD4hpRBx25W6N9eGuakT9Vd5tV/x6wmvQFWQZQwFB7eA==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + hasBin: true + + '@angular-devkit/schematics@19.2.24': + resolution: {integrity: sha512-lnw+ZM1Io+cJAkReC0NPDjqObL8NtKzKIkdgEEKC8CUmkhurYhedbicN8Y8NYHgG1uLd2GozW3+/QqPRZaN+Lw==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@angular-devkit/schematics@19.2.27': + resolution: {integrity: sha512-/PZmyAlb2NGWPikRRuiWLdfHQd8Wrx6lX4HqvTcaDhlU43M3T0ud4PH2T3QDp7BzHYY92xtD8iPxX2asg67G1A==} + engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} + + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.7': + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-async-generators@7.8.4': + resolution: {integrity: sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-bigint@7.8.3': + resolution: {integrity: sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-properties@7.12.13': + resolution: {integrity: sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-class-static-block@7.14.5': + resolution: {integrity: sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.29.7': + resolution: {integrity: sha512-zGYcYfq/WmZ4V+kBIXQon9dSSc8ircGZqw9ZaNhhGj9nZkeBu1jHLBDQqYYi5WA9uawvA2sIMbry2nCFhf5Djg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-json-strings@7.8.3': + resolution: {integrity: sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4': + resolution: {integrity: sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3': + resolution: {integrity: sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-numeric-separator@7.10.4': + resolution: {integrity: sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-object-rest-spread@7.8.3': + resolution: {integrity: sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3': + resolution: {integrity: sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-optional-chaining@7.8.3': + resolution: {integrity: sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-private-property-in-object@7.14.5': + resolution: {integrity: sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-top-level-await@7.14.5': + resolution: {integrity: sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@borewit/text-codec@0.2.2': + resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} + + '@colors/colors@1.5.0': + resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==} + engines: {node: '>=0.1.90'} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@epic-web/invariant@1.0.0': + resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==} + + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.6': + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.5': + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@inquirer/ansi@1.0.2': + resolution: {integrity: sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==} + engines: {node: '>=18'} + + '@inquirer/checkbox@4.3.2': + resolution: {integrity: sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/confirm@5.1.21': + resolution: {integrity: sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/core@10.3.2': + resolution: {integrity: sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/editor@4.2.23': + resolution: {integrity: sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/expand@4.0.23': + resolution: {integrity: sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/external-editor@1.0.3': + resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@4.3.1': + resolution: {integrity: sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/number@3.0.23': + resolution: {integrity: sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/password@4.0.23': + resolution: {integrity: sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.10.1': + resolution: {integrity: sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/prompts@7.3.2': + resolution: {integrity: sha512-G1ytyOoHh5BphmEBxSwALin3n1KGNYB6yImbICcRQdzXfOGbuJ9Jske/Of5Sebk339NSGGNfUshnzK8YWkTPsQ==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/rawlist@4.1.11': + resolution: {integrity: sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/search@3.2.2': + resolution: {integrity: sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/select@4.4.2': + resolution: {integrity: sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@inquirer/type@3.0.10': + resolution: {integrity: sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==} + engines: {node: '>=18'} + peerDependencies: + '@types/node': '>=18' + peerDependenciesMeta: + '@types/node': + optional: true + + '@ioredis/commands@2.0.0': + resolution: {integrity: sha512-vrx0AE/T0h7cRZwfo1M39Cr+ZhZrkf0V8mQN75wucKCxCLD9l/VX6no3gFvrLqD1IlG/1LtzWovqEw3t0Vr9zg==} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/load-nyc-config@1.1.0': + resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==} + engines: {node: '>=8'} + + '@istanbuljs/schema@0.1.6': + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} + + '@jest/console@30.5.0': + resolution: {integrity: sha512-BI1DpOedrJqbrYVi9yNhDWGjqphR/+gsM4STmg2+VaeXm7851hvpBDyKOZGMXATTrGEnFuEseQvLCtrrRmG0GQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/core@30.5.0': + resolution: {integrity: sha512-DLjRME+NY//j+UDTSfWnjoP0srrdR3DrJRy7yFZktGIwzpN2iVy2vMo0jziZ5c2Ij7bOwlJRXKVWtxZusazOJg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/diff-sequences@30.5.0': + resolution: {integrity: sha512-OsqBjHXCn8cadasoAZBP6nWYvMsRhpMzGXTpxJ5aO04NlbdhIz+FVe3q49l0AwVhsz/cEmIpBes6gAFl1/dWQg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/environment@30.5.0': + resolution: {integrity: sha512-HUaqexIauIh69IQ4NTuPDEUCB8g8T4TOPSIzQOS18mwI/KEHKQk1j013K2o6ra031szZE2t5jGmVx3xbzdjgKA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect-utils@30.5.0': + resolution: {integrity: sha512-5j0ztPxSy3McUJihjkDdCyCfjvT2hxykFTWsgEBZKB8qsw9ALdCiGTpTRH5gnf/d+qI4SflYUJ0dWNbzjQCWbA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/expect@30.5.0': + resolution: {integrity: sha512-jEmgmgJEobJ3zEhDOGp1VAJ6JkoVelpS8uZ1ae1Ul/5lP78UKJKmmU0lciJwd6JdnqOXHaaS/QCKwbf1dHI9MA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/fake-timers@30.5.0': + resolution: {integrity: sha512-sg8xIbYwe5GdB/vT3/0qrDIpO7Ov9mazHi++M95uynmDKEZ70G1r169AWct73H07VrTZhrz1SJEfLtjYv8tE3A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/get-type@30.5.0': + resolution: {integrity: sha512-9/2VUPitAjmBzbvDvqrxmvB7BzWsBW0WmkkojX1ODuxX1NLGxx9gfaZpHB0z8DtJ9uhGNmZG/VXBhf8uO0OV8Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/globals@30.5.0': + resolution: {integrity: sha512-h7eJx534czwL8lQMYB0hwLT4/HquO8EX/RtYL7RNUHyUyWWVciYjoudN4Ns5JmNvn2/jh0Vm9UstZjEzJJ5EsQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/pattern@30.5.0': + resolution: {integrity: sha512-HdNQYSdRTEBNrginaqzQtTjG0HRMfrra/z6Ok7uL3S87vSlarIVohEsJsSj5edu3MiHoHjAkvPROz5ZjoKai+w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/reporters@30.5.0': + resolution: {integrity: sha512-FEAuusWm+PUOn9ydjaHhpOpyPmS6IbGE04HaKTuUI4zd8eqYZiXiNLoCsdCog/l2XnNj53E9pFy64UltcvfJKg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + '@jest/schemas@30.5.0': + resolution: {integrity: sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/snapshot-utils@30.5.0': + resolution: {integrity: sha512-iWQtIsi2dRsO2oWzVceOeynuRJiYTW8gsDVp5wFQ02ipHluQsNgBpasWSHiawVxukIlsGLdCtCSbIEK7fPtpvQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/source-map@30.5.0': + resolution: {integrity: sha512-xWpTJP9D0bDFGbPGT8XuWSwwha/iHADyyKzUnMx4UbdgnHugxrDaQFO4RZ8x4ZsFzRP6pNii8uvlgKCDxCIuDg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-result@30.5.0': + resolution: {integrity: sha512-9IlPqUzUMkVDmoDqSSrVVLroVotgN3hTUPWPwq24XWXhh1Zpg915RXZ5pgRJ/7j4YE/kng5nqjSn4p3S68SZJA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/test-sequencer@30.5.0': + resolution: {integrity: sha512-TXlvSDIVv482b83hD8A8WtxDJEmGF47f60W6jRXOQL0Isfs3hKtk4rZIm9R/jLzAibg8+c56y+Q00BQs8R7Xcg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/transform@30.5.0': + resolution: {integrity: sha512-n1cYhoByyULEIXi64wbT4Lq91qeT1E6bwpM//sprFXhw955qaiHTdAmy1c1rNFGB6fCf1J+nxDUSf3RGwgZP5A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jest/types@30.5.0': + resolution: {integrity: sha512-s1N+79S4Yp9ZgklCauZXi+YPJdCdtStNYQT32stuD6EeQaIBGHoUfyj2P0YWy8RmuQfaJboO+ulxEvEheR/POQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.11': + resolution: {integrity: sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@microsoft/tsdoc@0.15.1': + resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==} + + '@napi-rs/wasm-runtime@1.2.3': + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} + peerDependencies: + '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + + '@nestjs/cli@11.0.24': + resolution: {integrity: sha512-aIHxQLSYtXShifA3zwWIeznEsZnNa3Iz2QRykFj+sl9IcbERBHr5nH87FRgywM+He3NxoF5WazHfR8FsmVeWxw==} + engines: {node: '>= 20.11'} + hasBin: true + peerDependencies: + '@swc/cli': ^0.1.62 || ^0.3.0 || ^0.4.0 || ^0.5.0 || ^0.6.0 || ^0.7.0 || ^0.8.0 + '@swc/core': ^1.3.62 + peerDependenciesMeta: + '@swc/cli': + optional: true + '@swc/core': + optional: true + + '@nestjs/common@11.2.3': + resolution: {integrity: sha512-obdauJXHfthhepbV+LpGe88OeBlR/Kw9lwjLo0Utzc//agoLXYb9DUGhPQWtm81IpBWMv+19eiwcve9MsBZwXA==} + peerDependencies: + class-transformer: '>=0.4.1' + class-validator: '>=0.13.2' + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/config@4.0.2': + resolution: {integrity: sha512-McMW6EXtpc8+CwTUwFdg6h7dYcBUpH5iUILCclAsa+MbCEvC9ZKu4dCHRlJqALuhjLw97pbQu62l4+wRwGeZqA==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + rxjs: ^7.1.0 + + '@nestjs/core@11.2.3': + resolution: {integrity: sha512-vkA9/Ja0Z3hvqXErSa+HaxrfF+cNXthNFi8VPNEKVli4rMd009yExAl0gLmko/Kf8peDXr72u1RN+j9Da2ukHg==} + engines: {node: '>= 20'} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + '@nestjs/websockets': ^11.0.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + rxjs: ^7.1.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + '@nestjs/websockets': + optional: true + + '@nestjs/jwt@11.0.0': + resolution: {integrity: sha512-v7YRsW3Xi8HNTsO+jeHSEEqelX37TVWgwt+BcxtkG/OfXJEOs6GZdbdza200d6KqId1pJQZ6UPj1F0M6E+mxaA==} + peerDependencies: + '@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + + '@nestjs/mapped-types@2.1.0': + resolution: {integrity: sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + class-transformer: ^0.4.0 || ^0.5.0 + class-validator: ^0.13.0 || ^0.14.0 + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/passport@11.0.5': + resolution: {integrity: sha512-ulQX6mbjlws92PIM15Naes4F4p2JoxGnIJuUsdXQPT+Oo2sqQmENEZXM7eYuimocfHnKlcfZOuyzbA33LwUlOQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + passport: ^0.5.0 || ^0.6.0 || ^0.7.0 + + '@nestjs/platform-express@11.2.3': + resolution: {integrity: sha512-YFQvRXT2de1qNL9LJPUBQ31+RsfI4cJ+sbpU9ENM/hDCgoHSEhm7oxUuGGKmhTZBNZEYm8mDYdfoTFmAH1LIJg==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + + '@nestjs/schematics@11.1.0': + resolution: {integrity: sha512-lVxGZ46tcdItFMoXr6vyKWlnOsm1SZm/GUqAEDvy2RL4Q4O+3bkziAhrO7Y8JLssFUUvNFEGqAizI52WAxhjDw==} + peerDependencies: + prettier: ^3.0.0 + typescript: '>=4.8.2' + peerDependenciesMeta: + prettier: + optional: true + + '@nestjs/swagger@11.2.1': + resolution: {integrity: sha512-1MS7xf0pzc1mofG53xrrtrurnziafPUHkqzRm4YUVPA/egeiMaSerQBD/feiAeQ2BnX0WiLsTX4HQFO0icvOjQ==} + peerDependencies: + '@fastify/static': ^8.0.0 + '@nestjs/common': ^11.0.1 + '@nestjs/core': ^11.0.1 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 || ^0.2.0 + peerDependenciesMeta: + '@fastify/static': + optional: true + class-transformer: + optional: true + class-validator: + optional: true + + '@nestjs/terminus@11.1.1': + resolution: {integrity: sha512-Ssql79H+EQY/Wg108eJqN4NiNsO/tLrj+qbzOWSQUf2JE4vJQ2RG3WTqUOrYjfjWmVHD3+Ys0+azed7LSMKScw==} + peerDependencies: + '@grpc/grpc-js': '*' + '@grpc/proto-loader': '*' + '@mikro-orm/core': '*' + '@mikro-orm/nestjs': '*' + '@nestjs/axios': ^2.0.0 || ^3.0.0 || ^4.0.0 + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/core': ^10.0.0 || ^11.0.0 + '@nestjs/microservices': ^10.0.0 || ^11.0.0 + '@nestjs/mongoose': ^11.0.0 + '@nestjs/sequelize': ^10.0.0 || ^11.0.0 + '@nestjs/typeorm': ^10.0.0 || ^11.0.0 + '@prisma/client': '*' + mongoose: '*' + reflect-metadata: 0.1.x || 0.2.x + rxjs: 7.x + sequelize: '*' + typeorm: '*' + peerDependenciesMeta: + '@grpc/grpc-js': + optional: true + '@grpc/proto-loader': + optional: true + '@mikro-orm/core': + optional: true + '@mikro-orm/nestjs': + optional: true + '@nestjs/axios': + optional: true + '@nestjs/microservices': + optional: true + '@nestjs/mongoose': + optional: true + '@nestjs/sequelize': + optional: true + '@nestjs/typeorm': + optional: true + '@prisma/client': + optional: true + mongoose: + optional: true + sequelize: + optional: true + typeorm: + optional: true + + '@nestjs/testing@11.2.3': + resolution: {integrity: sha512-7ANDWlkm8Xw4CYIhCNZhtBzANsQUKqjteA2yx/6sjqGyWhekeBKz8wgCJykm0vo+ltrg6U34dZlm2NgiRcNHPQ==} + peerDependencies: + '@nestjs/common': ^11.0.0 + '@nestjs/core': ^11.0.0 + '@nestjs/microservices': ^11.0.0 + '@nestjs/platform-express': ^11.0.0 + peerDependenciesMeta: + '@nestjs/microservices': + optional: true + '@nestjs/platform-express': + optional: true + + '@nestjs/throttler@6.5.0': + resolution: {integrity: sha512-9j0ZRfH0QE1qyrj9JjIRDz5gQLPqq9yVC2nHsrosDVAfI5HHw08/aUAWx9DZLSdQf4HDkmhTTEGLrRFHENvchQ==} + peerDependencies: + '@nestjs/common': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + '@nestjs/core': ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 + reflect-metadata: ^0.1.13 || ^0.2.0 + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@paralleldrive/cuid2@2.3.1': + resolution: {integrity: sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==} + + '@parcel/watcher-android-arm64@2.6.0': + resolution: {integrity: sha512-trgpLSCKRC/huFjXX/Smh+0sWe4+YtKfktIToiMl59ghz7z+qkH6kMvNnUbLyRs9N11t8l4svSCs1+5B3rOAhA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.6.0': + resolution: {integrity: sha512-Y3QV0gl7Q1zbfueunkWIERICbEojQFCgpyG7YqOGNFLsckXyI1xu9mAIUpKY9QBYzBtSkN8dBPwd3yiAO9ovMw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.6.0': + resolution: {integrity: sha512-Ohv6OpzhUfKYD7Beb8kDvG0jbIxORCYY1JRdZnaBtnjjkJxgD7ZVL0nw2sCYd0yTMKTvz3nnTnOF3cDifK+kvw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.6.0': + resolution: {integrity: sha512-5HmXvDgs8VK+74jF9y9/2FE3/OnlcKmc56tjmSrEuZjpSZOGL+fvAu+HKJBdPs9uwoP2hE6TlSUpXZ/C5jUFmQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.6.0': + resolution: {integrity: sha512-Ps/hui3A+vMbjdqlqAowK2ZL8+BO8dBjxeWXj6npTBs3jx4wWmbPpaLuqwrQrSqIVMCnpWo238bJ1U37GhQOYg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.6.0': + resolution: {integrity: sha512-9c6AUHgHoG+IY88MRIHupztQiQnrbqHYQjkM2btA+Bf/wQnQMuiD0Wfk1EVv3TlNT3x41uU71rn6E4xh/+zvkw==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + resolution: {integrity: sha512-yHRqS2owEXe6Hic9z6Mh1ECsCd+ODVOGvZDyciqRd21+v+o+DnXMOrw50DSpIG2sb8GPEaPPmfeCAWKPJdq46g==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.6.0': + resolution: {integrity: sha512-WhB2e/V7rqdHHWZusBSPuy5Ei8S6lSz6FE5TKKQz5h3a0O+C+mhY7vxU9b/stqvMb8beLnPY82ZrFTLKs+SrKA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.6.0': + resolution: {integrity: sha512-ulGE6x6Oz6iAwg75T8YQSoguBWasniIbX+QWpaYPcCnDOpdWX3k+4xbEYPZVLxOuoJI+svJJPD3sEj8G7lrQ3A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.6.0': + resolution: {integrity: sha512-tkBYKt7YQrjIJWYDnto2YgO8MRkjlMTSNoRHzsXinBqbLdeOM3L32wPZJvIZxqaLMfSlS/4sUjH/6STVP/XDLw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.6.0': + resolution: {integrity: sha512-gIZAP23jaHjGWasY/TY6yL7NHFClf0Ga7FN+iINvk+KN94rhm94lYZhFsbYFNcA04/onvGD9kKmiJLJB2HbNwQ==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-x64@2.6.0': + resolution: {integrity: sha512-cA+/pXV2YkfxlIcXOQ5fSWqAzzPyD78/x5qbK/I0vUkrlYHA8TIz+MXjAbGouguKVSI4bOmkTSJ1/poVSsgt+A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.6.0': + resolution: {integrity: sha512-7FNeNl8NCE7aINx7WXiKQrPYZWC/hvrTsmk6zmxbI7LTXE7hVek/n8AfVgpe2y82zl3w0HvCHN0bVKMBoJcC0w==} + engines: {node: '>= 10.0.0'} + + '@phc/format@1.0.0': + resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==} + engines: {node: '>=10'} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@pkgr/core@0.3.6': + resolution: {integrity: sha512-SEeaJLb3qBNF/OaXnaR1NmmBbFYk1zC0ZH/52fATcRPLFg/p791YrcyFFy44Bo9sLaGuSuLp5Q6axbb/O+v/RA==} + engines: {node: ^14.18.0 || >=16.0.0} + + '@prisma/client@6.19.0': + resolution: {integrity: sha512-QXFT+N/bva/QI2qoXmjBzL7D6aliPffIwP+81AdTGq0FXDoLxLkWivGMawG8iM5B9BKfxLIXxfWWAF6wbuJU6g==} + engines: {node: '>=18.18'} + peerDependencies: + prisma: '*' + typescript: '>=5.1.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@6.19.0': + resolution: {integrity: sha512-zwCayme+NzI/WfrvFEtkFhhOaZb/hI+X8TTjzjJ252VbPxAl2hWHK5NMczmnG9sXck2lsXrxIZuK524E25UNmg==} + + '@prisma/debug@6.19.0': + resolution: {integrity: sha512-8hAdGG7JmxrzFcTzXZajlQCidX0XNkMJkpqtfbLV54wC6LSSX6Vni25W/G+nAANwLnZ2TmwkfIuWetA7jJxJFA==} + + '@prisma/engines-version@6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773': + resolution: {integrity: sha512-gV7uOBQfAFlWDvPJdQxMT1aSRur3a0EkU/6cfbAC5isV67tKDWUrPauyaHNpB+wN1ebM4A9jn/f4gH+3iHSYSQ==} + + '@prisma/engines@6.19.0': + resolution: {integrity: sha512-pMRJ+1S6NVdXoB8QJAPIGpKZevFjxhKt0paCkRDTZiczKb7F4yTgRP8M4JdVkpQwmaD4EoJf6qA+p61godDokw==} + + '@prisma/fetch-engine@6.19.0': + resolution: {integrity: sha512-OOx2Lda0DGrZ1rodADT06ZGqHzr7HY7LNMaFE2Vp8dp146uJld58sRuasdX0OiwpHgl8SqDTUKHNUyzEq7pDdQ==} + + '@prisma/get-platform@6.19.0': + resolution: {integrity: sha512-ym85WDO2yDhC3fIXHWYpG3kVMBA49cL1XD2GCsCF8xbwoy2OkDQY44gEbAt2X46IQ4Apq9H6g0Ex1iFfPqEkHA==} + + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + + '@sinclair/typebox@0.34.52': + resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==} + + '@sinonjs/commons@3.0.1': + resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} + + '@sinonjs/fake-timers@15.4.0': + resolution: {integrity: sha512-DsG+8/LscQIQg68J6Ef3dv10u6nVyetYn923s3/sus5eaGfTo1of5WMZSLf0UJc9KDuKPilPH0UDJCjvNbDNCA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@tsconfig/node10@1.0.13': + resolution: {integrity: sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cookiejar@2.1.5': + resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + + '@types/eslint-scope@3.7.7': + resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} + + '@types/eslint@9.6.1': + resolution: {integrity: sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==} + + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + + '@types/express-serve-static-core@5.1.3': + resolution: {integrity: sha512-dPfW8NFiOF4wOHc7+N/QSxlY9cfSsenewGbAz8C8U/MULPd/YZ27LvJUIlzaXie7e6Ove9YunJGgC9tbHD2cKw==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/istanbul-lib-coverage@2.0.6': + resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} + + '@types/istanbul-lib-report@3.0.3': + resolution: {integrity: sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==} + + '@types/istanbul-reports@3.0.4': + resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + + '@types/jest@30.0.0': + resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + + '@types/jsonwebtoken@9.0.7': + resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + + '@types/methods@1.1.4': + resolution: {integrity: sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/passport-jwt@4.0.1': + resolution: {integrity: sha512-Y0Ykz6nWP4jpxgEUYq8NoVZeCQPo1ZndJLfapI249g1jHChvRfZRO/LS3tqu26YgAS/laI1qx98sYGz0IalRXQ==} + + '@types/passport-strategy@0.2.38': + resolution: {integrity: sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA==} + + '@types/passport@1.0.17': + resolution: {integrity: sha512-aciLyx+wDwT2t2/kJGJR2AEeBz0nJU4WuRX04Wu9Dqc5lSUtwu0WERPHYsLhF9PtseiAMPBGNUOtFjxZ56prsg==} + + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + + '@types/stack-utils@2.0.3': + resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + + '@types/superagent@8.1.11': + resolution: {integrity: sha512-KA7srSW/HENDtOw9DOqaFLgWuMqN9WgjEw62lh9dpvRaZDkhdOkazASd7X7i2eMUYLHa1U37ZttnePsH5zTDHw==} + + '@types/supertest@7.2.1': + resolution: {integrity: sha512-4CbBvoYVLHL7+yhbYrZET0vsvuyXTC05aRe7dNQkwMzm56auceoy6Yu3K50uZmwfHna1os3CMSgM/3QVkUtPTw==} + + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + + '@types/yargs-parser@21.0.3': + resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} + + '@types/yargs@17.0.35': + resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} + + '@typescript-eslint/eslint-plugin@8.68.0': + resolution: {integrity: sha512-WASHDpCm6qO5jj9g1a+8NiW5+GCkAyLReR56/4VruYmNgfUmqpxOfZ2Yfb8xGfJPWv5Qi6LSD8sXdces3vbp/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.68.0 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.68.0': + resolution: {integrity: sha512-fHq2VC1kpyYfvEcbiMjOpySY4WS7voEp89yAThrHRX5sm9j2lzYppCb2umFMEed4fWcyeLjHxrz0mpjNBaBxMQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.68.0': + resolution: {integrity: sha512-5GQtWZCXFcFYux955pvoS02WLc49pXNlvIxocKjS0clvwo3in1RdlzVKyiqQH9vE5AKWFLTaUgeQkOrTS+0Qxw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.68.0': + resolution: {integrity: sha512-T5eXpcaJNg8bhjHJ8Rjp68Vq/QBteYtTKY8TZqVNPaUbuz0f6jI9t6aDkylwvalpAB9XTTFeFOjrjXAZ3YvmVA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.68.0': + resolution: {integrity: sha512-F7zrGQfiJHojPwi8vhxZQC1tWtJzvL74cK/nqri2lk8YUXvYaYwl263xOJ69jDWPUk1hmcdoayFwk9lX09npVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.68.0': + resolution: {integrity: sha512-X77zqoY1EjeWGs/0JNxeaMfp5C5lIz4Tw8y66F1Ne8Faq6g424sBNYM6xBAqElfGZPLpWS+CZAp0DXyKDzWiHg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.68.0': + resolution: {integrity: sha512-9RnpsGJjrAllCMefGVVsImJM24YurhC0Q1h4UbvivtvOqXmR/vEJge2OoE++z9m6hyg8T1Q8t5SNT6tHSbrxcg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.68.0': + resolution: {integrity: sha512-OKKsD0tYmoNiU5PW2zehO1yO56jYOm1ShYlxon/Z0SJNidAkdVg86eg9ruRuoXf8xfnuWZGbwDsStkoXbZtIIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.68.0': + resolution: {integrity: sha512-PB5gJMMOg0Q5P1tsgWtEAqQacJXq0qEqRHDX/YJ4FaTMLfZPpHB3gjl2EJuiZyPABxmj4ZQYiY9m1bdAJ5y7tQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.68.0': + resolution: {integrity: sha512-YR65gGdGvTUAWLldC3xLOvOzamdGzB4A5/N8rehEaHs3Zvoe39BhgY+u0SPch1OvrVTfLcc55wsSgK2NcnTS/A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.4.0': + resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.12.2': + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.12.2': + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} + cpu: [loong64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} + cpu: [arm64] + os: [openharmony] + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} + cpu: [x64] + os: [win32] + + '@webassemblyjs/ast@1.14.1': + resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} + + '@webassemblyjs/floating-point-hex-parser@1.13.2': + resolution: {integrity: sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==} + + '@webassemblyjs/helper-api-error@1.13.2': + resolution: {integrity: sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==} + + '@webassemblyjs/helper-buffer@1.14.1': + resolution: {integrity: sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==} + + '@webassemblyjs/helper-numbers@1.13.2': + resolution: {integrity: sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==} + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': + resolution: {integrity: sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==} + + '@webassemblyjs/helper-wasm-section@1.14.1': + resolution: {integrity: sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==} + + '@webassemblyjs/ieee754@1.13.2': + resolution: {integrity: sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==} + + '@webassemblyjs/leb128@1.13.2': + resolution: {integrity: sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==} + + '@webassemblyjs/utf8@1.13.2': + resolution: {integrity: sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==} + + '@webassemblyjs/wasm-edit@1.14.1': + resolution: {integrity: sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==} + + '@webassemblyjs/wasm-gen@1.14.1': + resolution: {integrity: sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==} + + '@webassemblyjs/wasm-opt@1.14.1': + resolution: {integrity: sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==} + + '@webassemblyjs/wasm-parser@1.14.1': + resolution: {integrity: sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==} + + '@webassemblyjs/wast-printer@1.14.1': + resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} + + '@xtuc/ieee754@1.2.0': + resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} + + '@xtuc/long@4.2.2': + resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-import-phases@1.0.4: + resolution: {integrity: sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==} + engines: {node: '>=10.13.0'} + peerDependencies: + acorn: ^8.14.0 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.5: + resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} + engines: {node: '>=0.4.0'} + + acorn@8.18.0: + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-formats@2.1.1: + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-keywords@3.5.2: + resolution: {integrity: sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==} + peerDependencies: + ajv: ^6.9.1 + + ajv-keywords@5.1.0: + resolution: {integrity: sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==} + peerDependencies: + ajv: ^8.8.2 + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.18.0: + resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.3.0: + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + append-field@1.0.0: + resolution: {integrity: sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argon2@0.45.1: + resolution: {integrity: sha512-skm+/WCjkGqCQxF7FG1LuZXM5yvbFjgbfiCGsud2oLgaDhh6b6dbH0b1EkghbM+xx4Bj8Ape+KKgixoIlWZicQ==} + engines: {node: '>=16.17.0'} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + array-timsort@1.0.3: + resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + babel-jest@30.5.0: + resolution: {integrity: sha512-PrhPHlKC+MsLnuNzgIH/y1dkz1f6cSfKWaQeaG8WxLMuG44dYWQ8E9uRrsBbAGCU/3+BEFYPN4d6G3Zc5Y+waA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-0 + + babel-plugin-istanbul@8.0.0: + resolution: {integrity: sha512-18wCskrN3DgbuBmp1gr7LBGT8xdz5xhQQqFvFhVxbkl8VBCrMKQ2YtqBWtUal1Zrc1HTuX0011+Brjw78TCFkg==} + engines: {node: '>=18'} + + babel-plugin-jest-hoist@30.5.0: + resolution: {integrity: sha512-gtGo1B+u14jrZQv6TdSWIWkTqclboo7Qn+dFAGUOIuXLFuJKAdg3U5MIWzZnW4vfUb4dX9skmAMiby86e/SF4A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + babel-preset-current-node-syntax@1.2.0: + resolution: {integrity: sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==} + peerDependencies: + '@babel/core': ^7.0.0 || ^8.0.0-0 + + babel-preset-jest@30.5.0: + resolution: {integrity: sha512-ZGPn5ClP4lBDpuOK8W1yQIOy359HmbnZv3suucAlIe+SEE9yDsxV4S2PjSbH2Vc97U+WmzYD7vw3kr8NsQ/i6w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@babel/core': ^7.11.0 || ^8.0.0-beta.1 || ^8.0.0 + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseline-browser-mapping@2.11.20: + resolution: {integrity: sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==} + engines: {node: '>=6.0.0'} + hasBin: true + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bs-logger@0.2.6: + resolution: {integrity: sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==} + engines: {node: '>= 6'} + + bser@2.1.1: + resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + busboy@1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + c12@3.1.0: + resolution: {integrity: sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + char-regex@1.0.2: + resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} + engines: {node: '>=10'} + + chardet@2.2.0: + resolution: {integrity: sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==} + + check-disk-space@3.4.0: + resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==} + engines: {node: '>=16'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chrome-trace-event@1.0.4: + resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} + engines: {node: '>=6.0'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.2: + resolution: {integrity: sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==} + + cjs-module-lexer@2.2.1: + resolution: {integrity: sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==} + + class-transformer@0.5.1: + resolution: {integrity: sha512-SQa1Ws6hUbfC98vKGxZH3KFY0Y1lm5Zm0SY8XX9zbK7FJCyVEac3ATW0RIpwzW+oOfmHE5PMPufDG9hCfoEOMw==} + + class-validator@0.14.1: + resolution: {integrity: sha512-2VEG9JICxIqTpoK1eMzZqaV+u/EiwEJkMGzTrZf6sU/fwsnOITVgYJ8yojSy6CaXtO9V0Cc6ZQZ8h8m4UBuLwQ==} + + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + + cli-table3@0.6.5: + resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} + engines: {node: 10.* || >= 12.*} + + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone@1.0.4: + resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} + engines: {node: '>=0.8'} + + cluster-key-slot@1.1.1: + resolution: {integrity: sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw==} + engines: {node: '>=0.10.0'} + + co@4.6.0: + resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} + engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + + collect-v8-coverage@1.0.3: + resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + comment-json@5.0.0: + resolution: {integrity: sha512-uiqLcOiVDJtBP8WGkZHEP+FZIhTzP1dxvn59EfoYUi9gqupjrBWVQkO2atDrbnKPwLeotFYDsuNb26uBMqB+hw==} + engines: {node: '>= 6'} + + component-emitter@1.3.1: + resolution: {integrity: sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + concat-stream@2.0.0: + resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} + engines: {'0': node >= 6.0} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookiejar@2.1.4: + resolution: {integrity: sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cosmiconfig@8.3.6: + resolution: {integrity: sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-env@10.1.0: + resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==} + engines: {node: '>=20'} + hasBin: true + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + dedent@1.7.2: + resolution: {integrity: sha512-WzMx3mW98SN+zn3hgemf4OzdmyNhhhKz5Ay0pUfQiMQ3e1g+xmTJWp/pKdwKVXhdSkAEGIIzqeuWrL3mV/AXbA==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + defaults@1.0.4: + resolution: {integrity: sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-newline@3.1.0: + resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==} + engines: {node: '>=8'} + + dezalgo@1.0.4: + resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==} + + diff@4.0.4: + resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} + engines: {node: '>=0.3.1'} + + dotenv-expand@12.0.1: + resolution: {integrity: sha512-LaKRbou8gt0RNID/9RoI+J2rvXsBRPMV7p+ElHlPhcSARbCPDYcYG2s1TIzAfWv4YSgyY5taidWzzs31lNV3yQ==} + engines: {node: '>=12'} + + dotenv@16.4.7: + resolution: {integrity: sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + effect@3.18.4: + resolution: {integrity: sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==} + + electron-to-chromium@1.5.416: + resolution: {integrity: sha512-K6bvB2BjnNrugtIih6ewlbBI9DXa976jIdiIlRLHhBoEI9a4JaQjjHyF+A1IQI543aQYR4LnmOrT/K5fZj0aPA==} + + emittery@0.13.1: + resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} + engines: {node: '>=12'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@2.0.0: + resolution: {integrity: sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==} + engines: {node: '>=8'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.8: + resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.6: + resolution: {integrity: sha512-ifetmTcxWfz+4qRW3pH/ujdTq2jQIj59AxJMIN26K5avYgU8dxycUETQonWiW+wPrYXA0j3Try0l1CnwVQtDqQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-scope@5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.5: + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + + exit-x@0.2.2: + resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} + engines: {node: '>= 0.8.0'} + + expect@30.5.0: + resolution: {integrity: sha512-8fiMWcEjPU7B9nErC4FtFcCzf2tC6I75Qf7m8wzBAWC2taZmcno3yAFEjIQL34SwoGZNgPf63UDiJLyh4SMPaw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + exsolve@1.1.1: + resolution: {integrity: sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@3.1.6: + resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} + + fb-watchman@2.0.2: + resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + fork-ts-checker-webpack-plugin@9.1.0: + resolution: {integrity: sha512-mpafl89VFPJmhnJ1ssH+8wmM2b50n+Rew5x42NeI2U78aRWgtkEtGmctp7iT16UjquJTjorEmIfESj3DxdW84Q==} + engines: {node: '>=14.21.3'} + peerDependencies: + typescript: '>3.6.0' + webpack: ^5.11.0 + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + formidable@3.5.4: + resolution: {integrity: sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==} + engines: {node: '>=14.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-monkey@1.1.0: + resolution: {integrity: sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-package-type@0.1.0: + resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==} + engines: {node: '>=8.0.0'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob-to-regexp@0.4.1: + resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==} + + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@17.11.0: + resolution: {integrity: sha512-Z2I8hM+PbJDXQDq3Icgpzv+mPdwr68iZUU9d5WW4FuXfDUQfkZaZuvjMv42/5crNyw154+9+VWXbYrUgDXbxNw==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + handlebars@4.7.9: + resolution: {integrity: sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==} + engines: {node: '>=0.4.7'} + hasBin: true + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + helmet@8.3.0: + resolution: {integrity: sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==} + engines: {node: '>=18.0.0'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-local@3.2.0: + resolution: {integrity: sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==} + engines: {node: '>=8'} + hasBin: true + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ioredis@6.0.0: + resolution: {integrity: sha512-f+Dtubxfpf6KYFq7WVXJoOLn0bk4TJrMrN9SzeE+jrWrCWj7XX3fA6vkryafhADX+GMymRxgDJDOI33COkJc0w==} + engines: {node: '>=20.0.0'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-generator-fn@2.1.0: + resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} + engines: {node: '>=6'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-instrument@6.0.3: + resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} + engines: {node: '>=10'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + jest-changed-files@30.5.0: + resolution: {integrity: sha512-dq1x8JiEnHkJDxxOrF6UJDivBRAQMwAa5tzr+VX3um0SfyMFseUPQUbe51wLwggfoa5h/EmOtSpdJxxkzSlNRQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-circus@30.5.0: + resolution: {integrity: sha512-T3v7uM4wwCu+RQicjsAWCgoL3CiyuX3THSKwv2uMb9N2bMUgb+HfwDWEUma85vOGbvQNQ/YfoCXF1OCZot0ZEw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-cli@30.5.0: + resolution: {integrity: sha512-QHZMiy32x2K+NzJ1AuuoCAVc1Y5co0VXib3R7kD9MWcWFflzsD1eJN0wR+mkNlM+ts7C+Bjz0oOoFE5IiHylCg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jest-config@30.5.0: + resolution: {integrity: sha512-gYQl2FqYgiVpyuB7DutBIbJRWaq5VcHdzOXJJ51HNa8J5JrsZehIlzNFW0/9s5uvvcbzM5/LTUizYSbsFErv+w==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-diff@30.5.0: + resolution: {integrity: sha512-QjCfDMwdPFvLxTQmS4/Dswx3PUCiqmSXVLGljMC3SU7YG1qHVoR6b86IH/O2G9k9OMyKXz2vS2Q60VnAozNDwA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-docblock@30.5.0: + resolution: {integrity: sha512-NwDqcxtoZi33RhuW+zJS/RVA3rmheQ8BnwpYZuc/Eruaz6seQb7+aoCeDZu/3X7W2XmD8DbSo9Pn72DbKsBFYw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-each@30.5.0: + resolution: {integrity: sha512-NiMFNhRygJEFqYNt8pnkxppUF6CR486GEpt9rSU6lPBf7KeccaOL0zbjxG8fgJgD//6e7zYdssnlt6j+1kI31A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-environment-node@30.5.0: + resolution: {integrity: sha512-bTc79ywKLz0ogbT3JIYuEhgWV4Ffd/cJe06co4v8CyRtlmju8x5gokMlGFR8ARWhmk5SnbDRxmf50XWnlZVhDg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-haste-map@30.5.0: + resolution: {integrity: sha512-0FStogBslBVOEqTOJr4oXMtFitmrWp9WscG6Gbns88i0YAuMXijCT2G5VMfg/HCR4QAnL+OF2C2ednag+HlDuA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-leak-detector@30.5.0: + resolution: {integrity: sha512-Mq11ceAkNR250Iv45RoOwuG9fb4kYbJ02qoyL7A0nCI8FV5+aG/THmcEUx5uR2dNSa4KOtz+xBKrJ8cZPhPpuQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-matcher-utils@30.5.0: + resolution: {integrity: sha512-EfaYMC9f9ds7fahB/LYFTgd1Z2RS9Vpm2e46gazij0onkpoQG7Daq+MLm8/gQVqWwRVjL/RNDggbFx9MsrJEmQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-message-util@30.5.0: + resolution: {integrity: sha512-dBYMhplGfspKaCnVk9TUy1cZnknWubpuPNEputjz0YJk1G/92R45rn45BvbPMPMtC5LVcIdxJGPOaOSQTiuzJw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-mock@30.5.0: + resolution: {integrity: sha512-bP5MHZpkYrV7xpV+yvhl36DPcXoEmTR57Un5EACcdVpMY7mpkDefCBq+V4mhcjE/3rwUajT6OTrcJTN7EwN1BA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-regex-util@30.5.0: + resolution: {integrity: sha512-Mg0WK7A6xRHLSA1udJ8y9f3lM0uUhFTBnLKzwPmqB9AylvpleJ6BLemR8K9dK27DY+cesDryoA7yLZCAHsPG1A==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve-dependencies@30.5.0: + resolution: {integrity: sha512-TnSBAp3wGOnqXBmLT3OXtJkugw6Vj2KU0IPde2EJmbGns/QMoNlkauJ7jZ8KYaxONSpx0o4pUvi+u1Q/LSk3Pg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-resolve@30.5.0: + resolution: {integrity: sha512-NFvQWJ4G7e2kN5712iG+12Xr325NRFGAIhovrwLfgq5Oqd4bFHITw4CzcFkZGp1GTCqemC4v1l8/yZzedKZkjw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@30.5.0: + resolution: {integrity: sha512-Q6Yt+1LvXvEstvru6sQLT7OQYC77VNl6dK0KEvBkeOHgThmXDqNp3Ox9TglDWFHU85pemqTNdKwxfnmO3vgrdA==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runtime@30.5.0: + resolution: {integrity: sha512-VTRz0sRIw2EISeHigx1O+CMuwoG4+RKJjF8dp8okzFaDNQEcv5Mw0h5QW8VJXgb2CRF4gZIjSEBb2jdzXPagFQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-snapshot@30.5.0: + resolution: {integrity: sha512-pZWETdcqmKve9MDTE/AX6RaeAbRhzKhhAXGozAW8Pg2FfOSdUrQ/7D+VdwE3fLQsqjpITXJVQvYVZoMEzrUl0Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-util@30.5.0: + resolution: {integrity: sha512-lzU4aGUWaS+2X/B0CmgheDasfnsVlRfZh/rNQxB9b9s8cSYUq5BcqdQA95ld+KqJXBUVVt1sqnMQ2T3OxIalmg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-validate@30.5.0: + resolution: {integrity: sha512-N/hsPYKgBSzBeVZ2RHCs3yvBbTZNPX7Be8q33zhyo/yeFneBL1swzly31LYU4LJ3zJM9e8TxSM8IYLo2SDJZYQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-watcher@30.5.0: + resolution: {integrity: sha512-ujjnEoL4Uu+Swu3WRwYenWMB9JMEiD2T3OypnveMJ64S/1r/5G8eC4OQ1wEOgYWQqMi+mT+buzccflCHr/XJ9Q==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-worker@27.5.1: + resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} + engines: {node: '>= 10.13.0'} + + jest-worker@30.5.0: + resolution: {integrity: sha512-7kFk/607EoynNHLJa20daivkElM+c9PrCLduYy6AlMkYrXbh5TmVtW1BLXE05Y8baAFsJHMoC3xs2QRRTotwLw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest@30.5.0: + resolution: {integrity: sha512-HeFeOUEKh5gjnp1rjuSCse8Dhj0Y3KA8lsZ3azr4Wnq1nCxwMBu35MDc9mp8iblZxmeAz6wV4P241tyUB7J2Ew==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + node-notifier: ^8.0.1 || ^9.0.0 || ^10.0.0 + peerDependenciesMeta: + node-notifier: + optional: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.15.2: + resolution: {integrity: sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==} + hasBin: true + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@3.2.3: + resolution: {integrity: sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + libphonenumber-js@1.13.12: + resolution: {integrity: sha512-uLVeV1c9OTk6qkdqnj+mpMD+ZdnZ0szVyWu58HwMmpwkHA1gCEkyjd3veZQXDnuw9KEwSRjcc9B1pS9XKIN1fA==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-esm@1.0.3: + resolution: {integrity: sha512-v5xlu8eHD1+6r8EHTg6hfmO97LN8ugKtiXcy5e6oN72iD2r6u0RPfLl6fxM+7Wnh2ZRq15o0russMst44WauPA==} + engines: {node: '>=13.2.0'} + + loader-runner@4.3.2: + resolution: {integrity: sha512-DFEqQ3ihfS9blba08cLfYf1NRAIEm+dDjic073DRDc3/JspI/8wYmtDsHwd3+4hwvdxSK7PGaElfTmm0awWJ4w==} + engines: {node: '>=6.11.5'} + + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.memoize@4.1.2: + resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + memfs@3.5.3: + resolution: {integrity: sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==} + engines: {node: '>= 4.0.0'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + multer@2.2.0: + resolution: {integrity: sha512-6rdyFg2kLrMh9Jee7/BMPuV9lEAd7lLW2YUpF9/YxR7njyoUwwQ0ZPh3TaIY50Sw6vlyD2HW3wGOkTS4P79xrQ==} + engines: {node: '>= 10.16.0'} + + mute-stream@2.0.0: + resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} + engines: {node: ^18.17.0 || >=20.5.0} + + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} + + neo-async@2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + + node-abort-controller@3.1.1: + resolution: {integrity: sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-addon-api@8.9.2: + resolution: {integrity: sha512-VijLXbi3UACN69I0JVXJsX4tjACjNoQDgv2gTF6sx2wWEi8tkSg2eX8p5gSIFi8z2+DL3oHmY6OyKce38SDolg==} + engines: {node: ^18 || ^20 || >= 21} + + node-emoji@1.11.0: + resolution: {integrity: sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A==} + + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + engines: {node: '>=18'} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + + nypm@0.6.9: + resolution: {integrity: sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==} + engines: {node: '>=18'} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + ohash@2.0.12: + resolution: {integrity: sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + passport-jwt@4.0.1: + resolution: {integrity: sha512-UCKMDYhNuGOBE9/9Ycuoyh7vP6jpeTp/+sfMJl7nLff/t6dps+iaeE0hhNkKN8/HZHcJ7lCdOyDxHdDoxoSvdQ==} + + passport-strategy@1.0.0: + resolution: {integrity: sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==} + engines: {node: '>= 0.4.0'} + + passport@0.7.0: + resolution: {integrity: sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==} + engines: {node: '>= 0.4.0'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pause@0.0.1: + resolution: {integrity: sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-dir@4.2.0: + resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} + engines: {node: '>=8'} + + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.1: + resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==} + engines: {node: '>=6.0.0'} + + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@30.5.0: + resolution: {integrity: sha512-mzNzBErpHwM0zpmWS7ExOv62yhQhvd546nUuFqVR0dmnJB59tfrw9sjDF0DJknwsr59OXP0buwJ7PaKguczHSg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + prisma@6.19.0: + resolution: {integrity: sha512-F3eX7K+tWpkbhl3l4+VkFtrwJlLXbAM+f9jolgoUZbFcm1DgHZ4cq9AgVEgUym2au5Ad/TDLN8lg83D+M10ycw==} + engines: {node: '>=18.18'} + hasBin: true + peerDependencies: + typescript: '>=5.1.0' + peerDependenciesMeta: + typescript: + optional: true + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + pure-rand@7.0.1: + resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + react-is@19.2.8: + resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + reflect-metadata@0.2.2: + resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-cwd@3.0.0: + resolution: {integrity: sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==} + engines: {node: '>=8'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + schema-utils@3.3.0: + resolution: {integrity: sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==} + engines: {node: '>= 10.13.0'} + + schema-utils@4.3.3: + resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} + engines: {node: '>= 10.13.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + slash@3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + source-map@0.7.4: + resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} + engines: {node: '>= 8'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + stack-utils@2.0.6: + resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} + engines: {node: '>=10'} + + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + streamsearch@1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + + string-length@4.0.2: + resolution: {integrity: sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==} + engines: {node: '>=10'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-bom@4.0.0: + resolution: {integrity: sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==} + engines: {node: '>=8'} + + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strtok3@10.3.5: + resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} + engines: {node: '>=18'} + + superagent@10.3.0: + resolution: {integrity: sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==} + engines: {node: '>=14.18.0'} + + supertest@7.2.2: + resolution: {integrity: sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==} + engines: {node: '>=14.18.0'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + swagger-ui-dist@5.29.4: + resolution: {integrity: sha512-gJFDz/gyLOCQtWwAgqs6Rk78z9ONnqTnlW11gimG9nLap8drKa3AJBKpzIQMIjl5PD2Ix+Tn+mc/tfoT2tgsng==} + + symbol-observable@4.0.0: + resolution: {integrity: sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==} + engines: {node: '>=0.10'} + + synckit@0.11.13: + resolution: {integrity: sha512-eNRKgb3z66Yp3D2CixVujOUvXLFUTij/zVnV8KRyvFdQwpz7I5DS8UfRkTeLzb64u+dkzDSdelE24izu+zSSUg==} + engines: {node: ^14.18.0 || >=16.0.0} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + terser-webpack-plugin@5.6.1: + resolution: {integrity: sha512-201R5j+sJpK8nFWwKVyNfZot8FaJbLZDq5evriVzbV1wDtSXDjRUDRfJzHpAaxFDMEhsZL1QkeqM61wgsS3KaQ==} + engines: {node: '>= 10.13.0'} + peerDependencies: + '@minify-html/node': '*' + '@swc/core': '*' + '@swc/css': '*' + '@swc/html': '*' + clean-css: '*' + cssnano: '*' + csso: '*' + esbuild: '*' + html-minifier-terser: '*' + lightningcss: '*' + postcss: '*' + uglify-js: '*' + webpack: ^5.1.0 + peerDependenciesMeta: + '@minify-html/node': + optional: true + '@swc/core': + optional: true + '@swc/css': + optional: true + '@swc/html': + optional: true + clean-css: + optional: true + cssnano: + optional: true + csso: + optional: true + esbuild: + optional: true + html-minifier-terser: + optional: true + lightningcss: + optional: true + postcss: + optional: true + uglify-js: + optional: true + + terser@5.51.2: + resolution: {integrity: sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==} + engines: {node: '>=10'} + hasBin: true + + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + + tinyexec@1.3.0: + resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} + engines: {node: '>=18'} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.2: + resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} + engines: {node: '>=14.16'} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-jest@29.4.12: + resolution: {integrity: sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==} + engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/transform': ^29.0.0 || ^30.0.0 + '@jest/types': ^29.0.0 || ^30.0.0 + babel-jest: ^29.0.0 || ^30.0.0 + esbuild: '*' + jest: ^29.0.0 || ^30.0.0 + jest-util: ^29.0.0 || ^30.0.0 + typescript: '>=4.3 <7' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/transform': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + jest-util: + optional: true + + ts-loader@9.6.2: + resolution: {integrity: sha512-R4iuczmtgxvtuI556s+hTZ6/7Ee03VCAk/l/M8LY1OAsUgB7YydsCxkgq9D9pKRaD7GJqUi2u8fp9zZP/ufjKA==} + engines: {node: '>=12.0.0'} + peerDependencies: + loader-utils: '*' + typescript: '*' + webpack: ^4.0.0 || ^5.0.0 + peerDependenciesMeta: + loader-utils: + optional: true + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + tsconfig-paths-webpack-plugin@4.2.0: + resolution: {integrity: sha512-zbem3rfRS8BgeNK50Zz5SIQgXzLafiHjOwUAvk/38/o1jHn/V5QAgVUcz884or7WYcPaH3N2CIfUc2u0ul7UcA==} + engines: {node: '>=10.13.0'} + + tsconfig-paths@4.2.0: + resolution: {integrity: sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + + typescript-eslint@8.68.0: + resolution: {integrity: sha512-MHy0Y0ynqeEbx/S45+i/bBssdy3X6KNBfmJAP35GrgtNxu2TQ5K5xsFDhAnmsq1jvpdoZOPG1LGtJo0HWqYCrQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uglify-js@3.19.3: + resolution: {integrity: sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==} + engines: {node: '>=0.8.0'} + hasBin: true + + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + unrs-resolver@1.12.2: + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} + + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + v8-to-istanbul@9.3.0: + resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} + engines: {node: '>=10.12.0'} + + validator@13.15.35: + resolution: {integrity: sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==} + engines: {node: '>= 0.10'} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + watchpack@2.5.2: + resolution: {integrity: sha512-6i/00NBjP4yGPs+caKSyRfpTF/8Torsu0MOW3mMzIbhgISFder8i7xbqgHlLMwJrdiN8ndBV3UA1/AfzPSr+jg==} + engines: {node: '>=10.13.0'} + + wcwidth@1.0.1: + resolution: {integrity: sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==} + + webpack-node-externals@3.0.0: + resolution: {integrity: sha512-LnL6Z3GGDPht/AigwRh2dvL9PQPFQ8skEpVrWZXLWBYmqcaojHNN0onvHzie6rq7EWKrrBfPYqNEzTJgiwEQDQ==} + engines: {node: '>=6'} + + webpack-sources@3.5.1: + resolution: {integrity: sha512-jyuiGJdtvY434z5bUZrjz67v76/ePNvFZTp9Mdz29IlH4+GPsgyGjiv0fKI+M7BdkU6ADjulUcKAd3tUK3WlEw==} + engines: {node: '>=10.13.0'} + + webpack@5.106.2: + resolution: {integrity: sha512-wGN3qcrBQIFmQ/c0AiOAQBvrZ5lmY8vbbMv4Mxfgzqd/B6+9pXtLo73WuS1dSGXM5QYY3hZnIbvx+K1xxe6FyA==} + engines: {node: '>=10.13.0'} + hasBin: true + peerDependencies: + webpack-cli: '*' + peerDependenciesMeta: + webpack-cli: + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wordwrap@1.0.0: + resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + + zod@3.24.2: + resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} + +snapshots: + + '@angular-devkit/core@19.2.24(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/core@19.2.27(chokidar@4.0.3)': + dependencies: + ajv: 8.18.0 + ajv-formats: 3.0.1(ajv@8.18.0) + jsonc-parser: 3.3.1 + picomatch: 4.0.4 + rxjs: 7.8.1 + source-map: 0.7.4 + optionalDependencies: + chokidar: 4.0.3 + + '@angular-devkit/schematics-cli@19.2.27(@types/node@24.13.3)(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) + '@inquirer/prompts': 7.3.2(@types/node@24.13.3) + ansi-colors: 4.1.3 + symbol-observable: 4.0.0 + yargs-parser: 21.1.1 + transitivePeerDependencies: + - '@types/node' + - chokidar + + '@angular-devkit/schematics@19.2.24(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@angular-devkit/schematics@19.2.27(chokidar@4.0.3)': + dependencies: + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + jsonc-parser: 3.3.1 + magic-string: 0.30.17 + ora: 5.4.1 + rxjs: 7.8.1 + transitivePeerDependencies: + - chokidar + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.8 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.8 + '@babel/types': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.8 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 + + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-attributes@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + dependencies: + '@babel/core': 7.29.7 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/traverse@7.29.8': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/helper-globals': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/types': 7.29.8 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.8': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@bcoe/v8-coverage@0.2.3': {} + + '@borewit/text-codec@0.2.2': {} + + '@colors/colors@1.5.0': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@epic-web/invariant@1.0.0': {} + + '@eslint-community/eslint-utils@4.10.1(eslint@9.39.5(jiti@2.7.0))': + dependencies: + eslint: 9.39.5(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.6': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.3.2 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.5': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@inquirer/ansi@1.0.2': {} + + '@inquirer/checkbox@4.3.2(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.13.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.13.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/confirm@5.1.21(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.3) + '@inquirer/type': 3.0.10(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/core@10.3.2(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.13.3) + cli-width: 4.1.0 + mute-stream: 2.0.0 + signal-exit: 4.1.0 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/editor@4.2.23(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.3) + '@inquirer/external-editor': 1.0.3(@types/node@24.13.3) + '@inquirer/type': 3.0.10(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/expand@4.0.23(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.3) + '@inquirer/type': 3.0.10(@types/node@24.13.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/external-editor@1.0.3(@types/node@24.13.3)': + dependencies: + chardet: 2.2.0 + iconv-lite: 0.7.3 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@4.3.1(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.3) + '@inquirer/type': 3.0.10(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/number@3.0.23(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.3) + '@inquirer/type': 3.0.10(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/password@4.0.23(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.13.3) + '@inquirer/type': 3.0.10(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/prompts@7.10.1(@types/node@24.13.3)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.13.3) + '@inquirer/confirm': 5.1.21(@types/node@24.13.3) + '@inquirer/editor': 4.2.23(@types/node@24.13.3) + '@inquirer/expand': 4.0.23(@types/node@24.13.3) + '@inquirer/input': 4.3.1(@types/node@24.13.3) + '@inquirer/number': 3.0.23(@types/node@24.13.3) + '@inquirer/password': 4.0.23(@types/node@24.13.3) + '@inquirer/rawlist': 4.1.11(@types/node@24.13.3) + '@inquirer/search': 3.2.2(@types/node@24.13.3) + '@inquirer/select': 4.4.2(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/prompts@7.3.2(@types/node@24.13.3)': + dependencies: + '@inquirer/checkbox': 4.3.2(@types/node@24.13.3) + '@inquirer/confirm': 5.1.21(@types/node@24.13.3) + '@inquirer/editor': 4.2.23(@types/node@24.13.3) + '@inquirer/expand': 4.0.23(@types/node@24.13.3) + '@inquirer/input': 4.3.1(@types/node@24.13.3) + '@inquirer/number': 3.0.23(@types/node@24.13.3) + '@inquirer/password': 4.0.23(@types/node@24.13.3) + '@inquirer/rawlist': 4.1.11(@types/node@24.13.3) + '@inquirer/search': 3.2.2(@types/node@24.13.3) + '@inquirer/select': 4.4.2(@types/node@24.13.3) + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/rawlist@4.1.11(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.3) + '@inquirer/type': 3.0.10(@types/node@24.13.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/search@3.2.2(@types/node@24.13.3)': + dependencies: + '@inquirer/core': 10.3.2(@types/node@24.13.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.13.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/select@4.4.2(@types/node@24.13.3)': + dependencies: + '@inquirer/ansi': 1.0.2 + '@inquirer/core': 10.3.2(@types/node@24.13.3) + '@inquirer/figures': 1.0.15 + '@inquirer/type': 3.0.10(@types/node@24.13.3) + yoctocolors-cjs: 2.1.3 + optionalDependencies: + '@types/node': 24.13.3 + + '@inquirer/type@3.0.10(@types/node@24.13.3)': + optionalDependencies: + '@types/node': 24.13.3 + + '@ioredis/commands@2.0.0': {} + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/load-nyc-config@1.1.0': + dependencies: + camelcase: 5.3.1 + find-up: 4.1.0 + get-package-type: 0.1.0 + js-yaml: 3.15.2 + resolve-from: 5.0.0 + + '@istanbuljs/schema@0.1.6': {} + + '@jest/console@30.5.0': + dependencies: + '@jest/types': 30.5.0 + '@types/node': 24.13.3 + chalk: 4.1.2 + jest-message-util: 30.5.0 + jest-util: 30.5.0 + slash: 3.0.0 + + '@jest/core@30.5.0(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.5.0 + '@jest/pattern': 30.5.0 + '@jest/reporters': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 24.13.3 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.4.0 + exit-x: 0.2.2 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-changed-files: 30.5.0 + jest-config: 30.5.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)) + jest-haste-map: 30.5.0 + jest-message-util: 30.5.0 + jest-regex-util: 30.5.0 + jest-resolve: 30.5.0 + jest-resolve-dependencies: 30.5.0 + jest-runner: 30.5.0 + jest-runtime: 30.5.0 + jest-snapshot: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 + jest-watcher: 30.5.0 + pretty-format: 30.5.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + '@jest/diff-sequences@30.5.0': {} + + '@jest/environment@30.5.0': + dependencies: + '@jest/fake-timers': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 24.13.3 + jest-mock: 30.5.0 + + '@jest/expect-utils@30.5.0': + dependencies: + '@jest/get-type': 30.5.0 + + '@jest/expect@30.5.0': + dependencies: + expect: 30.5.0 + jest-snapshot: 30.5.0 + transitivePeerDependencies: + - supports-color + + '@jest/fake-timers@30.5.0': + dependencies: + '@jest/types': 30.5.0 + '@sinonjs/fake-timers': 15.4.0 + '@types/node': 24.13.3 + jest-message-util: 30.5.0 + jest-mock: 30.5.0 + jest-util: 30.5.0 + + '@jest/get-type@30.5.0': {} + + '@jest/globals@30.5.0': + dependencies: + '@jest/environment': 30.5.0 + '@jest/expect': 30.5.0 + '@jest/types': 30.5.0 + jest-mock: 30.5.0 + transitivePeerDependencies: + - supports-color + + '@jest/pattern@30.5.0': + dependencies: + '@types/node': 24.13.3 + jest-regex-util: 30.5.0 + + '@jest/reporters@30.5.0': + dependencies: + '@bcoe/v8-coverage': 0.2.3 + '@jest/console': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + '@jridgewell/trace-mapping': 0.3.31 + '@types/node': 24.13.3 + chalk: 4.1.2 + collect-v8-coverage: 1.0.3 + exit-x: 0.2.2 + glob: 13.0.6 + graceful-fs: 4.2.11 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-instrument: 6.0.3 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + jest-message-util: 30.5.0 + jest-util: 30.5.0 + jest-worker: 30.5.0 + slash: 3.0.0 + string-length: 4.0.2 + v8-to-istanbul: 9.3.0 + transitivePeerDependencies: + - supports-color + + '@jest/schemas@30.5.0': + dependencies: + '@sinclair/typebox': 0.34.52 + + '@jest/snapshot-utils@30.5.0': + dependencies: + '@jest/types': 30.5.0 + chalk: 4.1.2 + graceful-fs: 4.2.11 + natural-compare: 1.4.0 + + '@jest/source-map@30.5.0': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + callsites: 3.1.0 + convert-source-map: 2.0.0 + graceful-fs: 4.2.11 + + '@jest/test-result@30.5.0': + dependencies: + '@jest/console': 30.5.0 + '@jest/types': 30.5.0 + '@types/istanbul-lib-coverage': 2.0.6 + collect-v8-coverage: 1.0.3 + + '@jest/test-sequencer@30.5.0': + dependencies: + '@jest/test-result': 30.5.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.5.0 + slash: 3.0.0 + + '@jest/transform@30.5.0': + dependencies: + '@babel/core': 7.29.7 + '@jest/types': 30.5.0 + '@jridgewell/trace-mapping': 0.3.31 + babel-plugin-istanbul: 8.0.0 + chalk: 4.1.2 + convert-source-map: 2.0.0 + fast-json-stable-stringify: 2.1.0 + graceful-fs: 4.2.11 + jest-haste-map: 30.5.0 + jest-regex-util: 30.5.0 + jest-util: 30.5.0 + pirates: 4.0.7 + slash: 3.0.0 + write-file-atomic: 5.0.1 + transitivePeerDependencies: + - supports-color + + '@jest/types@30.5.0': + dependencies: + '@jest/pattern': 30.5.0 + '@jest/schemas': 30.5.0 + '@types/istanbul-lib-coverage': 2.0.6 + '@types/istanbul-reports': 3.0.4 + '@types/node': 24.13.3 + '@types/yargs': 17.0.35 + chalk: 4.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.11': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@lukeed/csprng@1.1.0': {} + + '@microsoft/tsdoc@0.15.1': {} + + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.3 + optional: true + + '@nestjs/cli@11.0.24(@types/node@24.13.3)(prettier@3.9.6)': + dependencies: + '@angular-devkit/core': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.27(chokidar@4.0.3) + '@angular-devkit/schematics-cli': 19.2.27(@types/node@24.13.3)(chokidar@4.0.3) + '@inquirer/prompts': 7.10.1(@types/node@24.13.3) + '@nestjs/schematics': 11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3) + ansis: 4.2.0 + chokidar: 4.0.3 + cli-table3: 0.6.5 + commander: 4.1.1 + fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.106.2) + glob: 13.0.6 + node-emoji: 1.11.0 + ora: 5.4.1 + tsconfig-paths: 4.2.0 + tsconfig-paths-webpack-plugin: 4.2.0 + typescript: 5.9.3 + webpack: 5.106.2 + webpack-node-externals: 3.0.0 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/css' + - '@swc/html' + - '@types/node' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - prettier + - uglify-js + - webpack-cli + + '@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + file-type: 21.3.4 + iterare: 1.2.1 + load-esm: 1.0.3 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/config@4.0.2(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + dotenv: 16.4.7 + dotenv-expand: 12.0.1 + lodash: 4.17.21 + rxjs: 7.8.2 + + '@nestjs/core@11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + fast-safe-stringify: 2.1.1 + iterare: 1.2.1 + path-to-regexp: 8.4.2 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + tslib: 2.8.1 + uid: 2.0.2 + optionalDependencies: + '@nestjs/platform-express': 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3) + + '@nestjs/jwt@11.0.0(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))': + dependencies: + '@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@types/jsonwebtoken': 9.0.7 + jsonwebtoken: 9.0.2 + + '@nestjs/mapped-types@2.1.0(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + + '@nestjs/passport@11.0.5(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(passport@0.7.0)': + dependencies: + '@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + passport: 0.7.0 + + '@nestjs/platform-express@11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)': + dependencies: + '@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + cors: 2.8.6 + express: 5.2.1 + multer: 2.2.0 + path-to-regexp: 8.4.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@nestjs/schematics@11.1.0(chokidar@4.0.3)(prettier@3.9.6)(typescript@5.9.3)': + dependencies: + '@angular-devkit/core': 19.2.24(chokidar@4.0.3) + '@angular-devkit/schematics': 19.2.24(chokidar@4.0.3) + comment-json: 5.0.0 + jsonc-parser: 3.3.1 + pluralize: 8.0.0 + typescript: 5.9.3 + optionalDependencies: + prettier: 3.9.6 + transitivePeerDependencies: + - chokidar + + '@nestjs/swagger@11.2.1(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)': + dependencies: + '@microsoft/tsdoc': 0.15.1 + '@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/mapped-types': 2.1.0(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2) + js-yaml: 4.1.0 + lodash: 4.17.21 + path-to-regexp: 8.3.0 + reflect-metadata: 0.2.2 + swagger-ui-dist: 5.29.4 + optionalDependencies: + class-transformer: 0.5.1 + class-validator: 0.14.1 + + '@nestjs/terminus@11.1.1(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)(@prisma/client@6.19.0(prisma@6.19.0(typescript@5.9.3))(typescript@5.9.3))(reflect-metadata@0.2.2)(rxjs@7.8.2)': + dependencies: + '@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + boxen: 5.1.2 + check-disk-space: 3.4.0 + reflect-metadata: 0.2.2 + rxjs: 7.8.2 + optionalDependencies: + '@prisma/client': 6.19.0(prisma@6.19.0(typescript@5.9.3))(typescript@5.9.3) + + '@nestjs/testing@11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)(@nestjs/platform-express@11.2.3)': + dependencies: + '@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-express': 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3) + + '@nestjs/throttler@6.5.0(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.2.3)(reflect-metadata@0.2.2)': + dependencies: + '@nestjs/common': 11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 11.2.3(@nestjs/common@11.2.3(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@11.2.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + reflect-metadata: 0.2.2 + + '@noble/hashes@1.8.0': {} + + '@paralleldrive/cuid2@2.3.1': + dependencies: + '@noble/hashes': 1.8.0 + + '@parcel/watcher-android-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-arm64@2.6.0': + optional: true + + '@parcel/watcher-darwin-x64@2.6.0': + optional: true + + '@parcel/watcher-freebsd-x64@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.6.0': + optional: true + + '@parcel/watcher-linux-x64-musl@2.6.0': + optional: true + + '@parcel/watcher-win32-arm64@2.6.0': + optional: true + + '@parcel/watcher-win32-x64@2.6.0': + optional: true + + '@parcel/watcher@2.6.0': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.7 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.6.0 + '@parcel/watcher-darwin-arm64': 2.6.0 + '@parcel/watcher-darwin-x64': 2.6.0 + '@parcel/watcher-freebsd-x64': 2.6.0 + '@parcel/watcher-linux-arm-glibc': 2.6.0 + '@parcel/watcher-linux-arm-musl': 2.6.0 + '@parcel/watcher-linux-arm64-glibc': 2.6.0 + '@parcel/watcher-linux-arm64-musl': 2.6.0 + '@parcel/watcher-linux-x64-glibc': 2.6.0 + '@parcel/watcher-linux-x64-musl': 2.6.0 + '@parcel/watcher-win32-arm64': 2.6.0 + '@parcel/watcher-win32-x64': 2.6.0 + + '@phc/format@1.0.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@pkgr/core@0.3.6': {} + + '@prisma/client@6.19.0(prisma@6.19.0(typescript@5.9.3))(typescript@5.9.3)': + optionalDependencies: + prisma: 6.19.0(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@6.19.0': + dependencies: + c12: 3.1.0 + deepmerge-ts: 7.1.5 + effect: 3.18.4 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@6.19.0': {} + + '@prisma/engines-version@6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773': {} + + '@prisma/engines@6.19.0': + dependencies: + '@prisma/debug': 6.19.0 + '@prisma/engines-version': 6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773 + '@prisma/fetch-engine': 6.19.0 + '@prisma/get-platform': 6.19.0 + + '@prisma/fetch-engine@6.19.0': + dependencies: + '@prisma/debug': 6.19.0 + '@prisma/engines-version': 6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773 + '@prisma/get-platform': 6.19.0 + + '@prisma/get-platform@6.19.0': + dependencies: + '@prisma/debug': 6.19.0 + + '@scarf/scarf@1.4.0': {} + + '@sinclair/typebox@0.34.52': {} + + '@sinonjs/commons@3.0.1': + dependencies: + type-detect: 4.0.8 + + '@sinonjs/fake-timers@15.4.0': + dependencies: + '@sinonjs/commons': 3.0.1 + + '@standard-schema/spec@1.1.0': {} + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.2 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@tsconfig/node10@1.0.13': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.28.0 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@types/babel__traverse@7.28.0': + dependencies: + '@babel/types': 7.29.8 + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 24.13.3 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 24.13.3 + + '@types/cookiejar@2.1.5': {} + + '@types/eslint-scope@3.7.7': + dependencies: + '@types/eslint': 9.6.1 + '@types/estree': 1.0.9 + + '@types/eslint@9.6.1': + dependencies: + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.9': {} + + '@types/express-serve-static-core@5.1.3': + dependencies: + '@types/node': 24.13.3 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.3 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + + '@types/istanbul-lib-coverage@2.0.6': {} + + '@types/istanbul-lib-report@3.0.3': + dependencies: + '@types/istanbul-lib-coverage': 2.0.6 + + '@types/istanbul-reports@3.0.4': + dependencies: + '@types/istanbul-lib-report': 3.0.3 + + '@types/jest@30.0.0': + dependencies: + expect: 30.5.0 + pretty-format: 30.5.0 + + '@types/json-schema@7.0.15': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 24.13.3 + + '@types/jsonwebtoken@9.0.7': + dependencies: + '@types/node': 24.13.3 + + '@types/methods@1.1.4': {} + + '@types/ms@2.1.0': {} + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/passport-jwt@4.0.1': + dependencies: + '@types/jsonwebtoken': 9.0.10 + '@types/passport-strategy': 0.2.38 + + '@types/passport-strategy@0.2.38': + dependencies: + '@types/express': 5.0.6 + '@types/passport': 1.0.17 + + '@types/passport@1.0.17': + dependencies: + '@types/express': 5.0.6 + + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@1.2.1': + dependencies: + '@types/node': 24.13.3 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 24.13.3 + + '@types/stack-utils@2.0.3': {} + + '@types/superagent@8.1.11': + dependencies: + '@types/cookiejar': 2.1.5 + '@types/methods': 1.1.4 + '@types/node': 24.13.3 + form-data: 4.0.6 + + '@types/supertest@7.2.1': + dependencies: + '@types/methods': 1.1.4 + '@types/superagent': 8.1.11 + + '@types/validator@13.15.10': {} + + '@types/yargs-parser@21.0.3': {} + + '@types/yargs@17.0.35': + dependencies: + '@types/yargs-parser': 21.0.3 + + '@typescript-eslint/eslint-plugin@8.68.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/type-utils': 8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/utils': 8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.68.0 + eslint: 9.39.5(jiti@2.7.0) + ignore: 7.0.6 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.68.0 + debug: 4.4.3 + eslint: 9.39.5(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.68.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@5.9.3) + '@typescript-eslint/types': 8.68.0 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.68.0': + dependencies: + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 + + '@typescript-eslint/tsconfig-utils@8.68.0(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.5(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.68.0': {} + + '@typescript-eslint/typescript-estree@8.68.0(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.68.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.68.0(typescript@5.9.3) + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/visitor-keys': 8.68.0 + debug: 4.4.3 + minimatch: 10.2.6 + semver: 7.8.5 + tinyglobby: 0.2.17 + ts-api-utils: 2.5.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.68.0 + '@typescript-eslint/types': 8.68.0 + '@typescript-eslint/typescript-estree': 8.68.0(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.68.0': + dependencies: + '@typescript-eslint/types': 8.68.0 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.4.0': {} + + '@unrs/resolver-binding-android-arm-eabi@1.12.2': + optional: true + + '@unrs/resolver-binding-android-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-loong64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.12.2': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.12.2': + optional: true + + '@unrs/resolver-binding-openharmony-arm64@1.12.2': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.12.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.12.2': + optional: true + + '@webassemblyjs/ast@1.14.1': + dependencies: + '@webassemblyjs/helper-numbers': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + + '@webassemblyjs/floating-point-hex-parser@1.13.2': {} + + '@webassemblyjs/helper-api-error@1.13.2': {} + + '@webassemblyjs/helper-buffer@1.14.1': {} + + '@webassemblyjs/helper-numbers@1.13.2': + dependencies: + '@webassemblyjs/floating-point-hex-parser': 1.13.2 + '@webassemblyjs/helper-api-error': 1.13.2 + '@xtuc/long': 4.2.2 + + '@webassemblyjs/helper-wasm-bytecode@1.13.2': {} + + '@webassemblyjs/helper-wasm-section@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/wasm-gen': 1.14.1 + + '@webassemblyjs/ieee754@1.13.2': + dependencies: + '@xtuc/ieee754': 1.2.0 + + '@webassemblyjs/leb128@1.13.2': + dependencies: + '@xtuc/long': 4.2.2 + + '@webassemblyjs/utf8@1.13.2': {} + + '@webassemblyjs/wasm-edit@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/helper-wasm-section': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-opt': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + '@webassemblyjs/wast-printer': 1.14.1 + + '@webassemblyjs/wasm-gen@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wasm-opt@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-buffer': 1.14.1 + '@webassemblyjs/wasm-gen': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + + '@webassemblyjs/wasm-parser@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/helper-api-error': 1.13.2 + '@webassemblyjs/helper-wasm-bytecode': 1.13.2 + '@webassemblyjs/ieee754': 1.13.2 + '@webassemblyjs/leb128': 1.13.2 + '@webassemblyjs/utf8': 1.13.2 + + '@webassemblyjs/wast-printer@1.14.1': + dependencies: + '@webassemblyjs/ast': 1.14.1 + '@xtuc/long': 4.2.2 + + '@xtuc/ieee754@1.2.0': {} + + '@xtuc/long@4.2.2': {} + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.1.0 + + acorn-import-phases@1.0.4(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn-jsx@5.3.2(acorn@8.18.0): + dependencies: + acorn: 8.18.0 + + acorn-walk@8.3.5: + dependencies: + acorn: 8.18.0 + + acorn@8.18.0: {} + + ajv-formats@2.1.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@3.0.1(ajv@8.18.0): + optionalDependencies: + ajv: 8.18.0 + + ajv-keywords@3.5.2(ajv@6.15.0): + dependencies: + ajv: 6.15.0 + + ajv-keywords@5.1.0(ajv@8.20.0): + dependencies: + ajv: 8.20.0 + fast-deep-equal: 3.1.3 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.18.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-colors@4.1.3: {} + + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + + ansi-regex@5.0.1: {} + + ansi-regex@6.3.0: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ansi-styles@6.2.3: {} + + ansis@4.2.0: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + append-field@1.0.0: {} + + arg@4.1.3: {} + + argon2@0.45.1: + dependencies: + '@phc/format': 1.0.0 + cross-env: 10.1.0 + node-addon-api: 8.9.2 + node-gyp-build: 4.8.4 + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + array-timsort@1.0.3: {} + + asap@2.0.6: {} + + asynckit@0.4.0: {} + + babel-jest@30.5.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@jest/transform': 30.5.0 + '@types/babel__core': 7.20.5 + babel-plugin-istanbul: 8.0.0 + babel-preset-jest: 30.5.0(@babel/core@7.29.7) + chalk: 4.1.2 + graceful-fs: 4.2.11 + slash: 3.0.0 + transitivePeerDependencies: + - supports-color + + babel-plugin-istanbul@8.0.0: + dependencies: + '@babel/helper-plugin-utils': 7.29.7 + '@istanbuljs/load-nyc-config': 1.1.0 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-instrument: 6.0.3 + test-exclude: 7.0.2 + transitivePeerDependencies: + - supports-color + + babel-plugin-jest-hoist@30.5.0: + dependencies: + '@types/babel__core': 7.20.5 + + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.7) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.7) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-import-attributes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.7) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.7) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.7) + + babel-preset-jest@30.5.0(@babel/core@7.29.7): + dependencies: + '@babel/core': 7.29.7 + babel-plugin-jest-hoist: 30.5.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseline-browser-mapping@2.11.20: {} + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.1.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.8: + dependencies: + baseline-browser-mapping: 2.11.20 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.416 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.8) + + bs-logger@0.2.6: + dependencies: + fast-json-stable-stringify: 2.1.0 + + bser@2.1.1: + dependencies: + node-int64: 0.4.0 + + buffer-equal-constant-time@1.0.1: {} + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + busboy@1.6.0: + dependencies: + streamsearch: 1.1.0 + + bytes@3.1.2: {} + + c12@3.1.0: + dependencies: + chokidar: 4.0.3 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 16.6.1 + exsolve: 1.1.1 + giget: 2.0.0 + jiti: 2.7.0 + ohash: 2.0.12 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.1 + rc9: 2.1.2 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + camelcase@5.3.1: {} + + camelcase@6.3.0: {} + + caniuse-lite@1.0.30001810: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + char-regex@1.0.2: {} + + chardet@2.2.0: {} + + check-disk-space@3.4.0: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chrome-trace-event@1.0.4: {} + + ci-info@4.4.0: {} + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.2: {} + + cjs-module-lexer@2.2.1: {} + + class-transformer@0.5.1: {} + + class-validator@0.14.1: + dependencies: + '@types/validator': 13.15.10 + libphonenumber-js: 1.13.12 + validator: 13.15.35 + + cli-boxes@2.2.1: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + + cli-table3@0.6.5: + dependencies: + string-width: 4.2.3 + optionalDependencies: + '@colors/colors': 1.5.0 + + cli-width@4.1.0: {} + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone@1.0.4: {} + + cluster-key-slot@1.1.1: {} + + co@4.6.0: {} + + collect-v8-coverage@1.0.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.20.3: {} + + commander@4.1.1: {} + + comment-json@5.0.0: + dependencies: + array-timsort: 1.0.3 + esprima: 4.0.1 + + component-emitter@1.3.1: {} + + concat-map@0.0.1: {} + + concat-stream@2.0.0: + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 3.6.2 + typedarray: 0.0.6 + + confbox@0.2.4: {} + + consola@3.4.2: {} + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.1.0: {} + + convert-source-map@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookiejar@2.1.4: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cosmiconfig@8.3.6(typescript@5.9.3): + dependencies: + import-fresh: 3.3.1 + js-yaml: 4.3.2 + parse-json: 5.2.0 + path-type: 4.0.0 + optionalDependencies: + typescript: 5.9.3 + + create-require@1.1.1: {} + + cross-env@10.1.0: + dependencies: + '@epic-web/invariant': 1.0.0 + cross-spawn: 7.0.6 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + dedent@1.7.2: {} + + deep-is@0.1.4: {} + + deepmerge-ts@7.1.5: {} + + deepmerge@4.3.1: {} + + defaults@1.0.4: + dependencies: + clone: 1.0.4 + + defu@6.1.7: {} + + delayed-stream@1.0.0: {} + + denque@2.1.0: {} + + depd@2.0.0: {} + + destr@2.0.5: {} + + detect-libc@2.1.2: {} + + detect-newline@3.1.0: {} + + dezalgo@1.0.4: + dependencies: + asap: 2.0.6 + wrappy: 1.0.2 + + diff@4.0.4: {} + + dotenv-expand@12.0.1: + dependencies: + dotenv: 16.4.7 + + dotenv@16.4.7: {} + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + effect@3.18.4: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + + electron-to-chromium@1.5.416: {} + + emittery@0.13.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + empathic@2.0.0: {} + + encodeurl@2.0.0: {} + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-module-lexer@2.3.2: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@2.0.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)): + dependencies: + eslint: 9.39.5(jiti@2.7.0) + + eslint-plugin-prettier@5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.5(jiti@2.7.0)))(eslint@9.39.5(jiti@2.7.0))(prettier@3.9.6): + dependencies: + eslint: 9.39.5(jiti@2.7.0) + prettier: 3.9.6 + prettier-linter-helpers: 1.0.1 + synckit: 0.11.13 + optionalDependencies: + '@types/eslint': 9.6.1 + eslint-config-prettier: 10.1.8(eslint@9.39.5(jiti@2.7.0)) + + eslint-scope@5.1.1: + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.5(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@9.39.5(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.6 + '@eslint/js': 9.39.5 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.18.0 + acorn-jsx: 5.3.2(acorn@8.18.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@4.3.0: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + etag@1.8.1: {} + + events@3.3.0: {} + + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + + exit-x@0.2.2: {} + + expect@30.5.0: + dependencies: + '@jest/expect-utils': 30.5.0 + '@jest/get-type': 30.5.0 + jest-matcher-utils: 30.5.0 + jest-message-util: 30.5.0 + jest-mock: 30.5.0 + jest-util: 30.5.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + exsolve@1.1.1: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.1.6: {} + + fb-watchman@2.0.2: + dependencies: + bser: 2.1.1 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + + flatted@3.4.4: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + fork-ts-checker-webpack-plugin@9.1.0(typescript@5.9.3)(webpack@5.106.2): + dependencies: + '@babel/code-frame': 7.29.7 + chalk: 4.1.2 + chokidar: 4.0.3 + cosmiconfig: 8.3.6(typescript@5.9.3) + deepmerge: 4.3.1 + fs-extra: 10.1.0 + memfs: 3.5.3 + minimatch: 3.1.5 + node-abort-controller: 3.1.1 + schema-utils: 3.3.0 + semver: 7.8.5 + tapable: 2.3.3 + typescript: 5.9.3 + webpack: 5.106.2 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + formidable@3.5.4: + dependencies: + '@paralleldrive/cuid2': 2.3.1 + dezalgo: 1.0.4 + once: 1.4.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-monkey@1.1.0: {} + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-package-type@0.1.0: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@6.0.1: {} + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.7 + node-fetch-native: 1.6.7 + nypm: 0.6.9 + pathe: 2.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob-to-regexp@0.4.1: {} + + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + + globals@14.0.0: {} + + globals@17.11.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + handlebars@4.7.9: + dependencies: + minimist: 1.2.8 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.19.3 + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + helmet@8.3.0: {} + + html-escaper@2.0.2: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + human-signals@2.1.0: {} + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + ignore@5.3.2: {} + + ignore@7.0.6: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-local@3.2.0: + dependencies: + pkg-dir: 4.2.0 + resolve-cwd: 3.0.0 + + imurmurhash@0.1.4: {} + + inherits@2.0.4: {} + + ioredis@6.0.0: + dependencies: + '@ioredis/commands': 2.0.0 + cluster-key-slot: 1.1.1 + debug: 4.4.3 + denque: 2.1.0 + redis-errors: 1.2.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ipaddr.js@1.9.1: {} + + is-arrayish@0.2.1: {} + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-generator-fn@2.1.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-interactive@1.0.0: {} + + is-promise@4.0.0: {} + + is-stream@2.0.1: {} + + is-unicode-supported@0.1.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-instrument@6.0.3: + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.8 + '@istanbuljs/schema': 0.1.6 + istanbul-lib-coverage: 3.2.2 + semver: 7.8.5 + transitivePeerDependencies: + - supports-color + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + iterare@1.2.1: {} + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + jest-changed-files@30.5.0: + dependencies: + execa: 5.1.1 + jest-util: 30.5.0 + p-limit: 3.1.0 + + jest-circus@30.5.0: + dependencies: + '@jest/environment': 30.5.0 + '@jest/expect': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 24.13.3 + chalk: 4.1.2 + co: 4.6.0 + dedent: 1.7.2 + is-generator-fn: 2.1.0 + jest-each: 30.5.0 + jest-matcher-utils: 30.5.0 + jest-message-util: 30.5.0 + jest-runtime: 30.5.0 + jest-snapshot: 30.5.0 + jest-util: 30.5.0 + p-limit: 3.1.0 + pretty-format: 30.5.0 + pure-rand: 7.0.1 + slash: 3.0.0 + stack-utils: 2.0.6 + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-cli@30.5.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.5.0(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)) + '@jest/test-result': 30.5.0 + '@jest/types': 30.5.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.5.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)) + jest-util: 30.5.0 + jest-validate: 30.5.0 + yargs: 17.7.3 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-config@30.5.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.29.7 + '@jest/get-type': 30.5.0 + '@jest/pattern': 30.5.0 + '@jest/test-sequencer': 30.5.0 + '@jest/types': 30.5.0 + babel-jest: 30.5.0(@babel/core@7.29.7) + chalk: 4.1.2 + ci-info: 4.4.0 + deepmerge: 4.3.1 + glob: 13.0.6 + graceful-fs: 4.2.11 + jest-circus: 30.5.0 + jest-docblock: 30.5.0 + jest-environment-node: 30.5.0 + jest-regex-util: 30.5.0 + jest-resolve: 30.5.0 + jest-runner: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 + parse-json: 5.2.0 + pretty-format: 30.5.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 24.13.3 + ts-node: 10.9.2(@types/node@24.13.3)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-diff@30.5.0: + dependencies: + '@jest/diff-sequences': 30.5.0 + '@jest/get-type': 30.5.0 + chalk: 4.1.2 + pretty-format: 30.5.0 + + jest-docblock@30.5.0: + dependencies: + detect-newline: 3.1.0 + + jest-each@30.5.0: + dependencies: + '@jest/get-type': 30.5.0 + '@jest/types': 30.5.0 + chalk: 4.1.2 + jest-util: 30.5.0 + pretty-format: 30.5.0 + + jest-environment-node@30.5.0: + dependencies: + '@jest/environment': 30.5.0 + '@jest/fake-timers': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 24.13.3 + jest-mock: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 + + jest-haste-map@30.5.0: + dependencies: + '@jest/types': 30.5.0 + '@parcel/watcher': 2.6.0 + '@types/node': 24.13.3 + anymatch: 3.1.3 + fb-watchman: 2.0.2 + fdir: 6.5.0(picomatch@4.0.7) + graceful-fs: 4.2.11 + jest-regex-util: 30.5.0 + jest-util: 30.5.0 + jest-worker: 30.5.0 + picomatch: 4.0.7 + + jest-leak-detector@30.5.0: + dependencies: + '@jest/get-type': 30.5.0 + pretty-format: 30.5.0 + + jest-matcher-utils@30.5.0: + dependencies: + '@jest/get-type': 30.5.0 + chalk: 4.1.2 + jest-diff: 30.5.0 + pretty-format: 30.5.0 + + jest-message-util@30.5.0: + dependencies: + '@babel/code-frame': 7.29.7 + '@jest/types': 30.5.0 + '@types/stack-utils': 2.0.3 + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-util: 30.5.0 + picomatch: 4.0.7 + pretty-format: 30.5.0 + slash: 3.0.0 + stack-utils: 2.0.6 + + jest-mock@30.5.0: + dependencies: + '@jest/expect-utils': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 24.13.3 + jest-util: 30.5.0 + + jest-regex-util@30.5.0: {} + + jest-resolve-dependencies@30.5.0: + dependencies: + jest-regex-util: 30.5.0 + jest-snapshot: 30.5.0 + transitivePeerDependencies: + - supports-color + + jest-resolve@30.5.0: + dependencies: + chalk: 4.1.2 + graceful-fs: 4.2.11 + jest-haste-map: 30.5.0 + jest-util: 30.5.0 + jest-validate: 30.5.0 + slash: 3.0.0 + unrs-resolver: 1.12.2 + + jest-runner@30.5.0: + dependencies: + '@jest/console': 30.5.0 + '@jest/environment': 30.5.0 + '@jest/source-map': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 24.13.3 + chalk: 4.1.2 + emittery: 0.13.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-docblock: 30.5.0 + jest-environment-node: 30.5.0 + jest-haste-map: 30.5.0 + jest-leak-detector: 30.5.0 + jest-message-util: 30.5.0 + jest-resolve: 30.5.0 + jest-runtime: 30.5.0 + jest-util: 30.5.0 + jest-watcher: 30.5.0 + jest-worker: 30.5.0 + p-limit: 3.1.0 + transitivePeerDependencies: + - supports-color + + jest-runtime@30.5.0: + dependencies: + '@jest/environment': 30.5.0 + '@jest/fake-timers': 30.5.0 + '@jest/globals': 30.5.0 + '@jest/source-map': 30.5.0 + '@jest/test-result': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 24.13.3 + chalk: 4.1.2 + cjs-module-lexer: 2.2.1 + collect-v8-coverage: 1.0.3 + es-module-lexer: 2.3.2 + glob: 13.0.6 + graceful-fs: 4.2.11 + jest-haste-map: 30.5.0 + jest-message-util: 30.5.0 + jest-mock: 30.5.0 + jest-regex-util: 30.5.0 + jest-resolve: 30.5.0 + jest-snapshot: 30.5.0 + jest-util: 30.5.0 + slash: 3.0.0 + strip-bom: 4.0.0 + transitivePeerDependencies: + - supports-color + + jest-snapshot@30.5.0: + dependencies: + '@babel/core': 7.29.7 + '@babel/generator': 7.29.8 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/types': 7.29.8 + '@jest/expect-utils': 30.5.0 + '@jest/get-type': 30.5.0 + '@jest/snapshot-utils': 30.5.0 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.7) + chalk: 4.1.2 + expect: 30.5.0 + graceful-fs: 4.2.11 + jest-diff: 30.5.0 + jest-matcher-utils: 30.5.0 + jest-message-util: 30.5.0 + jest-util: 30.5.0 + pretty-format: 30.5.0 + semver: 7.8.5 + synckit: 0.11.13 + transitivePeerDependencies: + - supports-color + + jest-util@30.5.0: + dependencies: + '@jest/types': 30.5.0 + '@types/node': 24.13.3 + chalk: 4.1.2 + ci-info: 4.4.0 + graceful-fs: 4.2.11 + picomatch: 4.0.7 + + jest-validate@30.5.0: + dependencies: + '@jest/get-type': 30.5.0 + '@jest/types': 30.5.0 + camelcase: 6.3.0 + chalk: 4.1.2 + leven: 3.1.0 + pretty-format: 30.5.0 + + jest-watcher@30.5.0: + dependencies: + '@jest/test-result': 30.5.0 + '@jest/types': 30.5.0 + '@types/node': 24.13.3 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.13.1 + jest-util: 30.5.0 + string-length: 4.0.2 + + jest-worker@27.5.1: + dependencies: + '@types/node': 24.13.3 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest-worker@30.5.0: + dependencies: + '@types/node': 24.13.3 + '@ungap/structured-clone': 1.4.0 + jest-util: 30.5.0 + merge-stream: 2.0.0 + supports-color: 8.1.1 + + jest@30.5.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.5.0(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)) + '@jest/types': 30.5.0 + import-local: 3.2.0 + jest-cli: 30.5.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.15.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsonc-parser@3.3.1: {} + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.3 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.3: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + leven@3.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + libphonenumber-js@1.13.12: {} + + lines-and-columns@1.2.4: {} + + load-esm@1.0.3: {} + + loader-runner@4.3.2: {} + + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.memoize@4.1.2: {} + + lodash.merge@4.6.2: {} + + lodash.once@4.1.1: {} + + lodash@4.17.21: {} + + lodash@4.18.1: {} + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@10.4.3: {} + + lru-cache@11.5.2: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + make-dir@4.0.0: + dependencies: + semver: 7.8.5 + + make-error@1.3.6: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.1: {} + + memfs@3.5.3: + dependencies: + fs-monkey: 1.1.0 + + merge-descriptors@2.0.0: {} + + merge-stream@2.0.0: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@2.6.0: {} + + mimic-fn@2.1.0: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + ms@2.1.3: {} + + multer@2.2.0: + dependencies: + append-field: 1.0.0 + busboy: 1.6.0 + concat-stream: 2.0.0 + type-is: 1.6.18 + + mute-stream@2.0.0: {} + + napi-postinstall@0.3.4: {} + + natural-compare@1.4.0: {} + + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 + + neo-async@2.6.2: {} + + node-abort-controller@3.1.1: {} + + node-addon-api@7.1.1: {} + + node-addon-api@8.9.2: {} + + node-emoji@1.11.0: + dependencies: + lodash: 4.18.1 + + node-fetch-native@1.6.7: {} + + node-gyp-build@4.8.4: {} + + node-int64@0.4.0: {} + + node-releases@2.0.54: {} + + normalize-path@3.0.0: {} + + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + + nypm@0.6.9: + dependencies: + citty: 0.2.2 + pathe: 2.0.3 + tinyexec: 1.3.0 + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + ohash@2.0.12: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@4.1.0: + dependencies: + p-limit: 2.3.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-try@2.2.0: {} + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.7 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parseurl@1.3.3: {} + + passport-jwt@4.0.1: + dependencies: + jsonwebtoken: 9.0.3 + passport-strategy: 1.0.0 + + passport-strategy@1.0.0: {} + + passport@0.7.0: + dependencies: + passport-strategy: 1.0.0 + pause: 0.0.1 + utils-merge: 1.0.1 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + + path-to-regexp@8.3.0: {} + + path-to-regexp@8.4.2: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + pause@0.0.1: {} + + perfect-debounce@1.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.4: {} + + picomatch@4.0.7: {} + + pirates@4.0.7: {} + + pkg-dir@4.2.0: + dependencies: + find-up: 4.1.0 + + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.1.1 + pathe: 2.0.3 + + pluralize@8.0.0: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.1: + dependencies: + fast-diff: 1.3.0 + + prettier@3.9.6: {} + + pretty-format@30.5.0: + dependencies: + '@jest/react-is-18': react-is@18.3.1 + '@jest/react-is-19': react-is@19.2.8 + '@jest/schemas': 30.5.0 + ansi-styles: 5.2.0 + + prisma@6.19.0(typescript@5.9.3): + dependencies: + '@prisma/config': 6.19.0 + '@prisma/engines': 6.19.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + punycode@2.3.1: {} + + pure-rand@6.1.0: {} + + pure-rand@7.0.1: {} + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + + rc9@2.1.2: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + react-is@18.3.1: {} + + react-is@19.2.8: {} + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + redis-errors@1.2.0: {} + + reflect-metadata@0.2.2: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resolve-cwd@3.0.0: + dependencies: + resolve-from: 5.0.0 + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + schema-utils@3.3.0: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 6.15.0 + ajv-keywords: 3.5.2(ajv@6.15.0) + + schema-utils@4.3.3: + dependencies: + '@types/json-schema': 7.0.15 + ajv: 8.20.0 + ajv-formats: 2.1.1(ajv@8.20.0) + ajv-keywords: 5.1.0(ajv@8.20.0) + + semver@6.3.1: {} + + semver@7.8.5: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + + slash@3.0.0: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + source-map@0.7.4: {} + + source-map@0.7.6: {} + + sprintf-js@1.0.3: {} + + stack-utils@2.0.6: + dependencies: + escape-string-regexp: 2.0.0 + + standard-as-callback@2.1.0: {} + + statuses@2.0.2: {} + + streamsearch@1.1.0: {} + + string-length@4.0.2: + dependencies: + char-regex: 1.0.2 + strip-ansi: 6.0.1 + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.3.0 + + strip-bom@3.0.0: {} + + strip-bom@4.0.0: {} + + strip-final-newline@2.0.0: {} + + strip-json-comments@3.1.1: {} + + strtok3@10.3.5: + dependencies: + '@tokenizer/token': 0.3.0 + + superagent@10.3.0: + dependencies: + component-emitter: 1.3.1 + cookiejar: 2.1.4 + debug: 4.4.3 + fast-safe-stringify: 2.1.1 + form-data: 4.0.6 + formidable: 3.5.4 + methods: 1.1.2 + mime: 2.6.0 + qs: 6.15.3 + transitivePeerDependencies: + - supports-color + + supertest@7.2.2: + dependencies: + cookie-signature: 1.2.2 + methods: 1.1.2 + superagent: 10.3.0 + transitivePeerDependencies: + - supports-color + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + swagger-ui-dist@5.29.4: + dependencies: + '@scarf/scarf': 1.4.0 + + symbol-observable@4.0.0: {} + + synckit@0.11.13: + dependencies: + '@pkgr/core': 0.3.6 + + tapable@2.3.3: {} + + terser-webpack-plugin@5.6.1(webpack@5.106.2): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + jest-worker: 27.5.1 + schema-utils: 4.3.3 + terser: 5.51.2 + webpack: 5.106.2 + + terser@5.51.2: + dependencies: + '@jridgewell/source-map': 0.3.11 + acorn: 8.18.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.6 + glob: 10.5.0 + minimatch: 10.2.6 + + tinyexec@1.3.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + toidentifier@1.0.1: {} + + token-types@6.1.2: + dependencies: + '@borewit/text-codec': 0.2.2 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + ts-api-utils@2.5.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-jest@29.4.12(@babel/core@7.29.7)(@jest/transform@30.5.0)(@jest/types@30.5.0)(babel-jest@30.5.0(@babel/core@7.29.7))(jest-util@30.5.0)(jest@30.5.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.9 + jest: 30.5.0(@types/node@24.13.3)(ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.8.5 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.29.7 + '@jest/transform': 30.5.0 + '@jest/types': 30.5.0 + babel-jest: 30.5.0(@babel/core@7.29.7) + jest-util: 30.5.0 + + ts-loader@9.6.2(typescript@5.9.3)(webpack@5.106.2): + dependencies: + chalk: 4.1.2 + picomatch: 4.0.7 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.106.2 + + ts-node@10.9.2(@types/node@24.13.3)(typescript@5.9.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.13 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.13.3 + acorn: 8.18.0 + acorn-walk: 8.3.5 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.4 + make-error: 1.3.6 + typescript: 5.9.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + tsconfig-paths-webpack-plugin@4.2.0: + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.24.5 + tapable: 2.3.3 + tsconfig-paths: 4.2.0 + + tsconfig-paths@4.2.0: + dependencies: + json5: 2.2.3 + minimist: 1.2.8 + strip-bom: 3.0.0 + + tslib@2.8.1: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.0.8: {} + + type-fest@0.20.2: {} + + type-fest@0.21.3: {} + + type-fest@4.41.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.1.0: + dependencies: + content-type: 2.1.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + + typedarray@0.0.6: {} + + typescript-eslint@8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.68.0(@typescript-eslint/parser@8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3))(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/parser': 8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.68.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.68.0(eslint@9.39.5(jiti@2.7.0))(typescript@5.9.3) + eslint: 9.39.5(jiti@2.7.0) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + typescript@5.9.3: {} + + uglify-js@3.19.3: + optional: true + + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + uint8array-extras@1.5.0: {} + + undici-types@7.18.2: {} + + universalify@2.0.1: {} + + unpipe@1.0.0: {} + + unrs-resolver@1.12.2: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.12.2 + '@unrs/resolver-binding-android-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-arm64': 1.12.2 + '@unrs/resolver-binding-darwin-x64': 1.12.2 + '@unrs/resolver-binding-freebsd-x64': 1.12.2 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.12.2 + '@unrs/resolver-binding-linux-arm64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-arm64-musl': 1.12.2 + '@unrs/resolver-binding-linux-loong64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-loong64-musl': 1.12.2 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-riscv64-musl': 1.12.2 + '@unrs/resolver-binding-linux-s390x-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-gnu': 1.12.2 + '@unrs/resolver-binding-linux-x64-musl': 1.12.2 + '@unrs/resolver-binding-openharmony-arm64': 1.12.2 + '@unrs/resolver-binding-wasm32-wasi': 1.12.2 + '@unrs/resolver-binding-win32-arm64-msvc': 1.12.2 + '@unrs/resolver-binding-win32-ia32-msvc': 1.12.2 + '@unrs/resolver-binding-win32-x64-msvc': 1.12.2 + + update-browserslist-db@1.3.2(browserslist@4.28.8): + dependencies: + browserslist: 4.28.8 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + util-deprecate@1.0.2: {} + + utils-merge@1.0.1: {} + + v8-compile-cache-lib@3.0.1: {} + + v8-to-istanbul@9.3.0: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@types/istanbul-lib-coverage': 2.0.6 + convert-source-map: 2.0.0 + + validator@13.15.35: {} + + vary@1.1.2: {} + + watchpack@2.5.2: + dependencies: + graceful-fs: 4.2.11 + + wcwidth@1.0.1: + dependencies: + defaults: 1.0.4 + + webpack-node-externals@3.0.0: {} + + webpack-sources@3.5.1: {} + + webpack@5.106.2: + dependencies: + '@types/eslint-scope': 3.7.7 + '@types/estree': 1.0.9 + '@types/json-schema': 7.0.15 + '@webassemblyjs/ast': 1.14.1 + '@webassemblyjs/wasm-edit': 1.14.1 + '@webassemblyjs/wasm-parser': 1.14.1 + acorn: 8.18.0 + acorn-import-phases: 1.0.4(acorn@8.18.0) + browserslist: 4.28.8 + chrome-trace-event: 1.0.4 + enhanced-resolve: 5.24.5 + es-module-lexer: 2.3.2 + eslint-scope: 5.1.1 + events: 3.3.0 + glob-to-regexp: 0.4.1 + graceful-fs: 4.2.11 + loader-runner: 4.3.2 + mime-db: 1.54.0 + neo-async: 2.6.2 + schema-utils: 4.3.3 + tapable: 2.3.3 + terser-webpack-plugin: 5.6.1(webpack@5.106.2) + watchpack: 2.5.2 + webpack-sources: 3.5.1 + transitivePeerDependencies: + - '@minify-html/node' + - '@swc/core' + - '@swc/css' + - '@swc/html' + - clean-css + - cssnano + - csso + - esbuild + - html-minifier-terser + - lightningcss + - postcss + - uglify-js + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + + word-wrap@1.2.5: {} + + wordwrap@1.0.0: {} + + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + + wrappy@1.0.2: {} + + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + + y18n@5.0.8: {} + + yallist@3.1.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} + + yoctocolors-cjs@2.1.3: {} + + zod@3.24.2: {} diff --git a/apps/api/prisma/backfill-contract-buckets.ts b/apps/api/prisma/backfill-contract-buckets.ts new file mode 100644 index 0000000..13bfc04 --- /dev/null +++ b/apps/api/prisma/backfill-contract-buckets.ts @@ -0,0 +1,121 @@ +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; +import { parseOaJson } from './oa-json'; + +const prisma = new PrismaClient(); + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +function compactName(name: string) { + return name.replace(/[\s()()【】\[\]《》·,,。.\-_/\\]/g, '').toLowerCase(); +} + +function namesOverlap(a: string, b: string) { + const na = compactName(a); + const nb = compactName(b); + if (!na || !nb) return false; + if (na === nb) return true; + const shorter = na.length <= nb.length ? na : nb; + const longer = na.length <= nb.length ? nb : na; + return shorter.length >= 8 && longer.includes(shorter); +} + +async function linkContracts() { + const bids = await prisma.bidCase.findMany({ + select: { id: true, bidNo: true, externalNo: true, name: true }, + }); + const contracts = await prisma.contract.findMany({ + where: { bidCaseId: null }, + select: { id: true, name: true, contractNo: true }, + }); + let linked = 0; + for (const c of contracts) { + const no = c.contractNo || ''; + const tb = no.match(/TB-\d+-\d+/); + const hits = bids.filter((b) => { + if (tb && (b.bidNo === tb[0] || b.bidNo.includes(tb[0]) || (b.externalNo && b.externalNo.includes(tb[0])))) { + return true; + } + if (no && (b.bidNo && no.includes(b.bidNo))) return true; + return namesOverlap(c.name, b.name); + }); + const unique = [...new Map(hits.map((h) => [h.id, h])).values()]; + if (unique.length !== 1) continue; + await prisma.contract.update({ where: { id: c.id }, data: { bidCaseId: unique[0].id } }); + linked += 1; + } + return { scanned: contracts.length, linked }; +} + +async function backfillSealApplicants() { + const dumps = ['/opt/import/prod/oa_fresh.json', '/opt/import/prod/oa_core.json', '/opt/import/prod/oa_clean.json']; + const dump = dumps.find((p) => fs.existsSync(p)); + if (!dump) return { updated: 0, skipped: 'no dump' }; + + const raw = parseOaJson>(dump); + const hrPath = '/opt/import/prod/oa_hr.json'; + const hr = fs.existsSync(hrPath) ? parseOaJson<{ users?: Record[] }>(hrPath) : { users: [] }; + + const users = await prisma.user.findMany(); + const byUsername = new Map(users.map((u) => [u.username, u])); + const byName = new Map(users.map((u) => [u.displayName, u])); + const admin = users.find((u) => u.displayName === '系统管理员'); + const oaUser = new Map((hr.users || []).map((u) => [String(u.id), str(u.userName || u.username)])); + const userFromOaId = (id: unknown) => { + const uname = oaUser.get(str(id)); + return uname ? byUsername.get(uname) : undefined; + }; + + const applies = (raw.oa_seal_apply || raw.oa_seal || []) as Record[]; + const rows = await prisma.sealRequest.findMany({ + include: { applicant: { select: { displayName: true } } }, + }); + let updated = 0; + for (const r of rows) { + if (r.applicant?.displayName && r.applicant.displayName !== '系统管理员') continue; + const reason = r.reason || ''; + const created = r.createdAt ? r.createdAt.getTime() : 0; + const hit = applies.find((a) => { + const content = str(a.content || a.apply_subject || a.reason); + if (content && reason && (content === reason || reason.includes(content) || content.includes(reason))) return true; + const t = str(a.create_time); + if (t && created) { + const d = new Date(t.includes('T') ? t : t.replace(' ', 'T')); + return Math.abs(d.getTime() - created) < 2000 && str(a.seal_type || '') === r.sealType; + } + return false; + }); + if (!hit) continue; + const who = + byName.get(str(hit.applicant_name) === '张靖妍' ? '张婧妍' : str(hit.applicant_name)) || + userFromOaId(hit.applicant_id) || + userFromOaId(hit.create_by) || + userFromOaId(hit.creator_id) || + userFromOaId(hit.apply_user); + if (!who || who.id === admin?.id) continue; + await prisma.sealRequest.update({ where: { id: r.id }, data: { applicantId: who.id } }); + updated += 1; + } + return { updated, scanned: rows.length }; +} + +async function main() { + const contracts = await linkContracts(); + const seals = await backfillSealApplicants(); + const [tender, incremental] = await Promise.all([ + prisma.contract.count({ where: { bidCaseId: { not: null } } }), + prisma.contract.count({ where: { bidCaseId: null } }), + ]); + console.log(JSON.stringify({ contracts, seals, tender, incremental }, null, 2)); +} + +main() + .then(async () => prisma.$disconnect()) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/backfill-project-ledger.ts b/apps/api/prisma/backfill-project-ledger.ts new file mode 100644 index 0000000..f1b3b81 --- /dev/null +++ b/apps/api/prisma/backfill-project-ledger.ts @@ -0,0 +1,127 @@ +import { Prisma, PrismaClient } from '@prisma/client'; +import { readFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import { normalizeProjectType } from '../src/project/project-workflow'; + +const prisma = new PrismaClient(); + +function str(v: unknown) { + return v == null ? '' : String(v).trim(); +} + +function num(v: unknown) { + const n = Number(v); + return Number.isFinite(n) ? n : 0; +} + +function compact(name: string) { + return String(name || '') + .replace(/[()()\[\]【】\s\-—_.,,、]/g, '') + .replace(/招标公告|采购项目|技术服务合同|合同/g, ''); +} + +function loadCore(): { project_list?: Record[]; reimbursement?: Record[] } { + const candidates = [ + join('/opt/import/prod/oa_core.json'), + join(process.cwd(), '../../import/prod/oa_core.json'), + join(process.cwd(), 'import/prod/oa_core.json'), + ]; + for (const p of candidates) { + if (existsSync(p)) return JSON.parse(readFileSync(p, 'utf8')); + } + return {}; +} + +async function main() { + const core = loadCore(); + const oldProjects = core.project_list || []; + const reimburses = core.reimbursement || []; + + const projects = await prisma.project.findMany({ + include: { contract: { include: { bidCase: { select: { projectType: true, name: true } } } }, expenses: { select: { id: true, projectId: true } } }, + }); + const expenses = await prisma.expenseClaim.findMany({ + where: { tenantId: '1' }, + select: { id: true, projectId: true, purpose: true, remark: true }, + }); + + let typed = 0; + let amounted = 0; + let linked = 0; + + for (const p of projects) { + const old = oldProjects.find((o) => { + const code = str(o.project_code || o.system_number); + const name = str(o.project_name); + return (code && (code === p.projectNo || p.projectNo.endsWith(code))) || + (name && (name === p.name || compact(name) === compact(p.name))); + }); + const nextType = + normalizeProjectType(p.projectType) || + normalizeProjectType(p.contract?.bidCase?.projectType) || + normalizeProjectType(str(old?.project_category || old?.project_type)); + const nextAmount = + num(p.amount) || + num(p.contract?.amount) || + num(old?.project_budget); + const data: Prisma.ProjectUpdateInput = {}; + if (nextType && nextType !== p.projectType) { + data.projectType = nextType; + typed += 1; + } + if (nextAmount > 0 && num(p.amount) <= 0) { + data.amount = nextAmount; + amounted += 1; + } + if (Object.keys(data).length) { + await prisma.project.update({ where: { id: p.id }, data }); + } + } + + const refreshed = await prisma.project.findMany({ select: { id: true, name: true, projectNo: true } }); + for (const e of expenses) { + if (e.projectId) continue; + const hit = refreshed.find((p) => { + const blob = `${e.purpose || ''} ${e.remark || ''}`; + return blob.includes(p.projectNo) || blob.includes(p.name) || (p.name.length >= 8 && blob.includes(p.name.slice(0, 12))); + }); + const old = reimburses.find((r) => str(r.project_name) && hit && compact(str(r.project_name)) === compact(hit.name)); + const byName = refreshed.find((p) => { + const r = reimburses.find((x) => str(x.id) && String(e.remark || '').includes(String(x.id))); + return r ? compact(str(r.project_name)) === compact(p.name) : false; + }); + const target = hit || byName; + if (!target) continue; + await prisma.expenseClaim.update({ where: { id: e.id }, data: { projectId: target.id, costType: 'PROJECT' } }); + linked += 1; + } + + // old reimbursements already imported with project names + for (const r of reimburses) { + const name = str(r.project_name); + if (!name) continue; + const project = refreshed.find((p) => p.name === name || compact(p.name) === compact(name)); + if (!project) continue; + const updated = await prisma.expenseClaim.updateMany({ + where: { + tenantId: '1', + projectId: null, + OR: [{ purpose: { contains: name.slice(0, 16) } }, { remark: { contains: name.slice(0, 16) } }], + }, + data: { projectId: project.id, costType: 'PROJECT' }, + }); + linked += updated.count; + } + + console.log(`project ledger backfill: typed=${typed} amounted=${amounted} expensesLinked=${linked}`); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/backfill-report-cc.ts b/apps/api/prisma/backfill-report-cc.ts new file mode 100644 index 0000000..9edd77e --- /dev/null +++ b/apps/api/prisma/backfill-report-cc.ts @@ -0,0 +1,79 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const KEYS = ['ccDaily', 'ccWeekly', 'ccMonthly'] as const; + +function rolesOf(emp: { users: { roles: { role: { code: string } }[] }[] }) { + return [...new Set(emp.users.flatMap((u) => u.roles.map((r) => r.role.code)))]; +} + +function isDirector(emp: { title?: string | null; jobRole?: string | null; users: { roles: { role: { code: string } }[] }[] }) { + if (/总监/.test(`${emp.title || ''}${emp.jobRole || ''}`)) return true; + return rolesOf(emp).some((r) => r.endsWith('_director') || r === 'biz_director' || r === 'owner'); +} + +function parseIds(raw?: string | null) { + if (!raw) return []; + return [...new Set(raw.split(/[,,、\s]+/).filter((s) => UUID_RE.test(s)))]; +} + +async function main() { + const depts = await prisma.department.findMany({ where: { tenantId: '1' }, select: { id: true, parentId: true } }); + const byId = new Map(depts.map((d) => [d.id, d])); + const people = await prisma.employee.findMany({ + where: { tenantId: '1' }, + include: { users: { select: { roles: { select: { role: { select: { code: true } } } } } } }, + }); + const byDept = new Map(); + for (const e of people) { + const list = byDept.get(e.departmentId) || []; + list.push(e); + byDept.set(e.departmentId, list); + } + + function scopeDeptIds(start: string, selfId: string) { + const scope: string[] = []; + const seen = new Set(); + let cur: string | null | undefined = start; + while (cur && !seen.has(cur)) { + seen.add(cur); + scope.push(cur); + const hasDir = (byDept.get(cur) || []).some((e) => e.id !== selfId && isDirector(e)); + if (hasDir) break; + cur = byId.get(cur)?.parentId; + } + return new Set(scope); + } + + let n = 0; + for (const emp of people) { + const scope = scopeDeptIds(emp.departmentId, emp.id); + const dirs = people.filter((d) => scope.has(d.departmentId) && d.id !== emp.id && d.id !== emp.managerId && isDirector(d)); + const dirIds = dirs.map((d) => d.id); + const allowed = new Set(dirIds); + const data: Partial> = {}; + for (const key of KEYS) { + const cur = parseIds(emp[key]); + let next = cur.filter((id) => allowed.has(id)); + if (!next.length && dirIds.length && cur.some((id) => id === emp.managerId || !allowed.has(id))) { + next = dirIds; + } + const joined = next.length ? next.join(',') : null; + if (joined !== (emp[key] || null)) data[key] = joined; + } + if (!Object.keys(data).length) continue; + await prisma.employee.update({ where: { id: emp.id }, data }); + n += 1; + console.log(`${emp.name}: ${JSON.stringify(data)}`); + } + console.log(`updated ${n} / ${people.length} employee cc fields`); +} + +main() + .then(async () => prisma.$disconnect()) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/backfill-tech-travel.ts b/apps/api/prisma/backfill-tech-travel.ts new file mode 100644 index 0000000..f6612a1 --- /dev/null +++ b/apps/api/prisma/backfill-tech-travel.ts @@ -0,0 +1,36 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + const techPeople = await prisma.employee.findMany({ + where: { department: { deptType: 'TECH' } }, + select: { name: true, department: { select: { deptType: true, name: true } } }, + }); + const names = techPeople.map((e) => e.name).filter((n) => n && n.length >= 2); + const rows = await prisma.expenseClaim.findMany({ + where: { tenantId: '1', costType: 'PROJECT', budgetCategory: 'TRAVEL' }, + include: { + applicant: { select: { employee: { select: { name: true, department: { select: { deptType: true } } } } } }, + lines: { select: { remark: true } }, + }, + }); + let n = 0; + for (const row of rows) { + const dept = row.applicant.employee?.department?.deptType || ''; + const text = [row.purpose, row.remark, ...row.lines.map((l) => l.remark)].filter(Boolean).join(' '); + const forTech = names.some((name) => text.includes(name)); + if (dept !== 'TECH' && !forTech) continue; + await prisma.expenseClaim.update({ where: { id: row.id }, data: { budgetCategory: 'TECH' } }); + n += 1; + } + console.log(`reclassified ${n} / ${rows.length} travel expenses into tech budget; tech staff ${names.length}`); +} + +main() + .then(async () => prisma.$disconnect()) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/backfill-won-ledger.ts b/apps/api/prisma/backfill-won-ledger.ts new file mode 100644 index 0000000..09a9fca --- /dev/null +++ b/apps/api/prisma/backfill-won-ledger.ts @@ -0,0 +1,88 @@ +import { PrismaClient } from '@prisma/client'; +import { ensureWonLedger } from '../src/contract/won-ledger'; +import { BIZ_TYPE } from '../src/bidding/bid-workflow'; + +const prisma = new PrismaClient(); + +async function main() { + const owner = await prisma.user.findFirst({ + where: { tenantId: '1', status: 'ACTIVE', roles: { some: { role: { code: { in: ['owner', 'admin'] } } } } }, + select: { id: true }, + }); + const won = await prisma.bidCase.findMany({ + where: { tenantId: '1', status: 'WON' }, + include: { party: true, legalEntity: true, expenses: { select: { amount: true } } }, + }); + let newContracts = 0; + let newProjects = 0; + let todos = 0; + for (const bid of won) { + const actorId = bid.createdById || owner?.id; + if (!actorId) throw new Error('没有可用的合同创建人'); + const beforeC = await prisma.contract.count({ + where: { bidCaseId: bid.id, status: { in: ['DRAFT', 'PENDING', 'APPROVED'] } }, + }); + const beforeP = await prisma.project.count({ + where: { contract: { bidCaseId: bid.id } }, + }); + const result = await prisma.$transaction((tx) => ensureWonLedger(tx, bid, actorId)); + if ( + (await prisma.contract.count({ + where: { bidCaseId: bid.id, status: { in: ['DRAFT', 'PENDING', 'APPROVED'] } }, + })) > beforeC + ) { + newContracts += 1; + } + if ((await prisma.project.count({ where: { contract: { bidCaseId: bid.id } } })) > beforeP) { + newProjects += 1; + } + + await prisma.todoTask.updateMany({ + where: { + status: 'OPEN', + bizType: BIZ_TYPE.CONVERT_CONTRACT, + bizId: bid.id, + }, + data: { bizId: result.contract.id, title: `完善合同草稿:${bid.name}` }, + }); + const openTodo = await prisma.todoTask.findFirst({ + where: { + status: 'OPEN', + bizType: BIZ_TYPE.CONVERT_CONTRACT, + bizId: result.contract.id, + }, + }); + if (!openTodo && result.contract.status === 'DRAFT') { + const assignees = await prisma.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + roles: { some: { role: { code: { in: ['biz_director', 'biz_staff', 'owner'] } } } }, + }, + }); + if (assignees.length) { + await prisma.todoTask.createMany({ + data: assignees.map((u) => ({ + tenantId: '1', + title: `完善合同草稿:${bid.name}`, + bizType: BIZ_TYPE.CONVERT_CONTRACT, + bizId: result.contract.id, + assigneeId: u.id, + })), + }); + todos += assignees.length; + } + } + } + console.log( + `won bids ${won.length}; new contracts ${newContracts}; new projects ${newProjects}; todos ${todos}`, + ); +} + +main() + .then(async () => prisma.$disconnect()) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/ensure-prod-admin.ts b/apps/api/prisma/ensure-prod-admin.ts new file mode 100644 index 0000000..98c9ae1 --- /dev/null +++ b/apps/api/prisma/ensure-prod-admin.ts @@ -0,0 +1,65 @@ +import { PrismaClient } from '@prisma/client'; +import * as argon2 from 'argon2'; + +const prisma = new PrismaClient(); + +async function main() { + const username = '18115604840'; + const password = '123456'; + const role = await prisma.role.findFirst({ where: { code: 'admin' } }); + if (!role) throw new Error('缺少 admin 角色,请先执行 prisma seed'); + const root = + (await prisma.department.findFirst({ where: { code: 'ROOT' } })) || + (await prisma.department.findFirst({ orderBy: { sortNo: 'asc' } })); + if (!root) throw new Error('缺少部门'); + let emp = await prisma.employee.findFirst({ where: { mobile: username } }); + if (!emp) { + emp = await prisma.employee.create({ + data: { + name: '系统管理员', + employeeNo: 'E-ADMIN', + departmentId: root.id, + title: '管理员', + restSchedule: 'DOUBLE', + employmentStatus: 'REGULAR', + mobile: username, + hiredAt: new Date(), + }, + }); + } else if (!emp.mobile) { + emp = await prisma.employee.update({ where: { id: emp.id }, data: { mobile: username } }); + } + const hash = await argon2.hash(password); + let user = await prisma.user.findFirst({ where: { username } }); + if (!user) { + user = await prisma.user.create({ + data: { + username, + passwordHash: hash, + displayName: emp.name, + employeeId: emp.id, + mustChangePassword: false, + status: 'ACTIVE', + }, + }); + } else { + user = await prisma.user.update({ + where: { id: user.id }, + data: { employeeId: emp.id, status: 'ACTIVE' }, + }); + } + await prisma.userRole.upsert({ + where: { userId_roleId: { userId: user.id, roleId: role.id } }, + create: { userId: user.id, roleId: role.id }, + update: {}, + }); + console.log('prod admin ok', username); +} + +main() + .then(() => prisma.$disconnect()) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/fix-approval-fields.ts b/apps/api/prisma/fix-approval-fields.ts new file mode 100644 index 0000000..2a52644 --- /dev/null +++ b/apps/api/prisma/fix-approval-fields.ts @@ -0,0 +1,141 @@ +import { PrismaClient } from '@prisma/client'; +import { readFileSync } from 'fs'; + +const prisma = new PrismaClient(); + +type OaExpense = { + name: string; + amount: number; + project: string; + date: string; + purpose: string; + remark: string | null; + expenseCategory: string; +}; + +type OaLoan = { name: string; amount: number; purpose: string; project: string }; + +const NAME_ALIAS: Record = { + 张靖妍: '张婧妍', + 张婧妍: '张婧妍', +}; + +function normName(v?: string | null) { + const s = (v || '').trim(); + return NAME_ALIAS[s] || s; +} + +function amtEq(a: unknown, b: unknown) { + return Math.abs(Number(a) - Number(b)) < 0.021; +} + +function sameProject(oa: string, locals: string[]) { + if (!oa) return locals.length === 0; + return locals.some((p) => p === oa || p.includes(oa) || oa.includes(p)); +} + +async function main() { + const oaExpenses = JSON.parse(readFileSync('/opt/apps/api/prisma/_oa_reimb_map.json', 'utf8')) as OaExpense[]; + const oaLoans = JSON.parse(readFileSync('/opt/apps/api/prisma/_oa_loan_map.json', 'utf8')) as OaLoan[]; + + const expenses = await prisma.expenseClaim.findMany({ + include: { + applicant: { select: { displayName: true, employee: { select: { name: true } } } }, + project: { select: { name: true } }, + bidCase: { select: { name: true } }, + }, + orderBy: { claimNo: 'asc' }, + }); + + const used = new Set(); + let patchedExpenses = 0; + const unmatchedExpenses: string[] = []; + + for (const row of expenses) { + const names = [normName(row.applicant?.displayName), normName(row.applicant?.employee?.name)].filter(Boolean); + const locals = [row.project?.name, row.bidCase?.name].filter((v): v is string => Boolean(v)); + const date = String(row.submittedAt || '').slice(0, 10); + const idxs = oaExpenses + .map((o, i) => ({ o, i })) + .filter(({ o, i }) => !used.has(i) && amtEq(o.amount, row.amount) && names.includes(normName(o.name))); + const byProj = idxs.filter(({ o }) => sameProject(o.project, locals)); + const byDate = idxs.filter(({ o }) => o.date === date); + const hit = (byProj.length === 1 ? byProj[0] : null) || (byDate.length === 1 ? byDate[0] : null) || (idxs.length === 1 ? idxs[0] : byProj[0] || byDate[0] || null); + if (!hit) { + unmatchedExpenses.push(`${row.claimNo} ${names[0] || '?'} ${row.amount}`); + continue; + } + used.add(hit.i); + await prisma.expenseClaim.update({ + where: { id: row.id }, + data: { + purpose: hit.o.purpose || null, + remark: hit.o.remark, + expenseCategory: hit.o.expenseCategory, + }, + }); + if (hit.o.purpose) { + await prisma.approvalTask.updateMany({ + where: { bizId: row.id, bizType: 'EXPENSE_APPROVAL' }, + data: { title: hit.o.purpose }, + }); + } + patchedExpenses += 1; + } + + const loans = await prisma.loanRecord.findMany({ + include: { + applicant: { select: { displayName: true, employee: { select: { name: true } } } }, + }, + orderBy: { loanNo: 'asc' }, + }); + const usedLoan = new Set(); + let patchedLoans = 0; + const unmatchedLoans: string[] = []; + for (const row of loans) { + const names = [normName(row.applicant?.displayName), normName(row.applicant?.employee?.name)].filter(Boolean); + const hit = oaLoans + .map((o, i) => ({ o, i })) + .find(({ o, i }) => !usedLoan.has(i) && amtEq(o.amount, row.amount) && names.includes(normName(o.name)) && (!row.purpose || row.purpose === o.purpose || o.purpose.includes(row.purpose) || row.purpose.includes(o.purpose))); + const fallback = oaLoans + .map((o, i) => ({ o, i })) + .find(({ o, i }) => !usedLoan.has(i) && amtEq(o.amount, row.amount) && names.includes(normName(o.name))); + const pick = hit || fallback; + if (!pick) { + unmatchedLoans.push(`${row.loanNo} ${names[0] || '?'}`); + await prisma.loanRecord.update({ where: { id: row.id }, data: { remark: null } }); + if (row.purpose) { + await prisma.approvalTask.updateMany({ + where: { bizId: row.id, bizType: 'LOAN_APPROVAL' }, + data: { title: row.purpose }, + }); + } + continue; + } + usedLoan.add(pick.i); + await prisma.loanRecord.update({ + where: { id: row.id }, + data: { purpose: pick.o.purpose, remark: null }, + }); + await prisma.approvalTask.updateMany({ + where: { bizId: row.id, bizType: 'LOAN_APPROVAL' }, + data: { title: pick.o.purpose }, + }); + patchedLoans += 1; + } + + console.log( + JSON.stringify( + { patchedExpenses, unmatchedExpenses, patchedLoans, unmatchedLoans }, + null, + 2, + ), + ); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/fix-bid-from-oa.ts b/apps/api/prisma/fix-bid-from-oa.ts new file mode 100644 index 0000000..d35d16f --- /dev/null +++ b/apps/api/prisma/fix-bid-from-oa.ts @@ -0,0 +1,125 @@ +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; + +const prisma = new PrismaClient(); +const DUMP = '/opt/import/prod/oa_clean.json'; + +type Row = Record; + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +function when(v: unknown) { + const s = str(v); + if (!s || s.startsWith('0000')) return null; + const d = new Date(s.includes('T') ? s : s.replace(' ', 'T')); + return Number.isNaN(d.getTime()) ? null : d; +} + +function entityKey(raw: unknown) { + const s = str(raw); + if (!s) return ''; + if (s.includes('鲁兵')) return '山东鲁兵'; + if (s.includes('隆创')) return '隆创'; + if (s.includes('风影')) return '江苏风影随行科技有限公司'; + return s; +} + +const ENTITIES: { key: string; name: string; shortName: string }[] = [ + { key: '江苏风影随行科技有限公司', name: '江苏风影随行科技有限公司', shortName: '江苏风影' }, + { key: '山东鲁兵', name: '山东鲁兵', shortName: '山东鲁兵' }, + { key: '隆创', name: '隆创', shortName: '隆创' }, +]; + +async function main() { + const raw = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Record; + const tenders = raw.oa_tender || []; + const entityIds = new Map(); + for (const e of ENTITIES) { + const existing = await prisma.legalEntity.findFirst({ + where: { tenantId: '1', OR: [{ name: e.name }, { shortName: e.shortName }] }, + }); + if (existing) { + await prisma.legalEntity.update({ where: { id: existing.id }, data: { name: e.name, shortName: e.shortName } }); + entityIds.set(e.key, existing.id); + } else { + const created = await prisma.legalEntity.create({ + data: { name: e.name, shortName: e.shortName, isDefault: e.shortName === '江苏风影' }, + }); + entityIds.set(e.key, created.id); + } + } + await prisma.legalEntity.updateMany({ + where: { name: { not: '江苏风影随行科技有限公司' } }, + data: { isDefault: false }, + }); + await prisma.legalEntity.updateMany({ + where: { name: '江苏风影随行科技有限公司' }, + data: { isDefault: true }, + }); + + const users = await prisma.user.findMany(); + const userByName = new Map(users.map((u) => [u.displayName, u])); + const bids = await prisma.bidCase.findMany(); + const usedNos = new Set(); + const matched: { rowId: string; t: Row }[] = []; + for (const t of tenders) { + const name = str(t.project_name); + const sys = str(t.system_number); + const official = str(t.project_number); + const row = bids.find((b) => b.name === name) || bids.find((b) => b.bidNo === sys || b.externalNo === official); + if (!row) continue; + matched.push({ rowId: row.id, t }); + } + for (const m of matched) { + await prisma.bidCase.update({ + where: { id: m.rowId }, + data: { bidNo: `TMP-${m.rowId.replace(/-/g, '').slice(0, 20)}` }, + }); + } + + for (const { rowId, t } of matched) { + const row = bids.find((b) => b.id === rowId)!; + const official = str(t.project_number); + const key = entityKey(t.participating_unit); + let bidNo = official || row.externalNo || row.bidNo; + if (usedNos.has(bidNo)) bidNo = `${bidNo}-${row.id.slice(0, 4)}`; + usedNos.add(bidNo); + const preparer = str(t.bid_prepare_person); + const creator = userByName.get(preparer === '张靖妍' ? '张婧妍' : preparer); + await prisma.bidCase.update({ + where: { id: row.id }, + data: { + bidNo, + externalNo: official || row.externalNo, + legalEntityId: key ? entityIds.get(key) || null : null, + openAt: when(t.bid_open_date) || row.openAt, + applyEndAt: when(t.file_apply_deadline) || row.applyEndAt, + applyStartAt: when(t.create_time) || row.applyStartAt, + createdById: creator?.id || row.createdById, + createdAt: when(t.create_time) || row.createdAt, + }, + }); + } + const patched = matched.length; + + const loan = await prisma.loanRecord.findFirst({ where: { purpose: { contains: '重庆投标' } } }); + const chongqing = await prisma.bidCase.findFirst({ where: { name: { contains: '门诊特病' } } }); + if (loan && chongqing) { + await prisma.loanRecord.update({ + where: { id: loan.id }, + data: { bidCaseId: chongqing.id, costType: 'BID' }, + }); + } + + console.log(JSON.stringify({ patchedBids: patched, entities: ENTITIES.map((e) => e.shortName), linkedChongqingLoan: Boolean(loan && chongqing) })); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/fix-expense-applicants.ts b/apps/api/prisma/fix-expense-applicants.ts new file mode 100644 index 0000000..263b38b --- /dev/null +++ b/apps/api/prisma/fix-expense-applicants.ts @@ -0,0 +1,125 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function ensureUser(name: string, username: string, deptName: string, employeeNo: string, passwordHash: string) { + const existing = await prisma.user.findFirst({ where: { OR: [{ username }, { displayName: name }] } }); + if (existing) return existing.id; + const dept = + (await prisma.department.findFirst({ where: { name: deptName } })) || + (await prisma.department.findFirst({ where: { name: { contains: '研发' } } })) || + (await prisma.department.findFirst({ where: { parentId: { not: null } } })); + if (!dept) throw new Error(`没有部门可挂 ${name}`); + const empRole = await prisma.role.findFirst({ where: { code: 'employee' } }); + if (!empRole) throw new Error('没有 employee 角色'); + let emp = await prisma.employee.findFirst({ where: { OR: [{ name }, { employeeNo }] } }); + if (!emp) { + emp = await prisma.employee.create({ + data: { + name, + employeeNo, + departmentId: dept.id, + title: '员工', + employmentStatus: 'LEFT', + remark: `OA 报销有此人(原部门 ${deptName}),人事花名册未导入,补建后挂单据,账号停用。`, + }, + }); + } + const user = await prisma.user.create({ + data: { + username, + displayName: name, + passwordHash, + status: 'DISABLED', + employeeId: emp.id, + }, + }); + await prisma.userRole.create({ data: { userId: user.id, roleId: empRole.id } }); + console.log('created leftover', name, user.id, 'dept', dept.name); + return user.id; +} + +async function main() { + const admin = await prisma.user.findFirst({ where: { username: 'admin' } }); + if (!admin) throw new Error('no admin'); + await ensureUser('李乐菡', 'lilehan', '项目一部', 'E0115', admin.passwordHash); + + const users = await prisma.user.findMany(); + const byName = new Map(users.map((u) => [u.displayName, u.id])); + const ALIAS: Record = { 张靖妍: '张婧妍' }; + for (const [from, to] of Object.entries(ALIAS)) { + const uid = byName.get(to); + if (uid) byName.set(from, uid); + } + const expenses = await prisma.expenseClaim.findMany({ select: { id: true, remark: true, applicantId: true, status: true } }); + let n = 0; + for (const e of expenses) { + const name = (e.remark || '').split(' / ')[0]?.trim(); + if (!name || name === '系统管理员') continue; + const uid = byName.get(name); + if (!uid || uid === e.applicantId) continue; + await prisma.expenseClaim.update({ where: { id: e.id }, data: { applicantId: uid } }); + n += 1; + } + const loans = await prisma.loanRecord.findMany({ select: { id: true, remark: true, applicantId: true } }); + let ln = 0; + for (const e of loans) { + const name = (e.remark || '').split(' / ')[0]?.trim(); + if (!name || name === '系统管理员') continue; + const uid = byName.get(name); + if (!uid || uid === e.applicantId) continue; + await prisma.loanRecord.update({ where: { id: e.id }, data: { applicantId: uid } }); + ln += 1; + } + + const approvers = await prisma.user.findMany({ + where: { status: 'ACTIVE', roles: { some: { role: { code: { in: ['admin', 'finance', 'owner'] } } } } }, + }); + const pendingExp = await prisma.expenseClaim.findMany({ where: { status: 'PENDING' } }); + const pendingLoan = await prisma.loanRecord.findMany({ where: { status: 'PENDING' } }); + let tasks = 0; + for (const row of pendingExp) { + for (const u of approvers) { + const hit = await prisma.approvalTask.findFirst({ + where: { bizType: 'EXPENSE_APPROVAL', bizId: row.id, assigneeId: u.id, status: 'PENDING' }, + }); + if (hit) continue; + await prisma.approvalTask.create({ + data: { + title: `报销确认:${row.claimNo}`, + bizType: 'EXPENSE_APPROVAL', + bizId: row.id, + assigneeId: u.id, + }, + }); + tasks += 1; + } + } + for (const row of pendingLoan) { + for (const u of approvers) { + const hit = await prisma.approvalTask.findFirst({ + where: { bizType: 'LOAN_APPROVAL', bizId: row.id, assigneeId: u.id, status: 'PENDING' }, + }); + if (hit) continue; + await prisma.approvalTask.create({ + data: { + title: `借款确认:${row.loanNo}`, + bizType: 'LOAN_APPROVAL', + bizId: row.id, + assigneeId: u.id, + }, + }); + tasks += 1; + } + } + + const left = await prisma.expenseClaim.count({ where: { applicantId: admin.id } }); + console.log(JSON.stringify({ rematchedExpenses: n, rematchedLoans: ln, approvalTasks: tasks, adminStillHolds: left })); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/fix-expense-lines.ts b/apps/api/prisma/fix-expense-lines.ts new file mode 100644 index 0000000..45bf397 --- /dev/null +++ b/apps/api/prisma/fix-expense-lines.ts @@ -0,0 +1,107 @@ +import { Prisma, PrismaClient } from '@prisma/client'; +import { mapExpenseLineKind, parseOaJson } from './oa-json'; + +const prisma = new PrismaClient(); +const FLOW = '/opt/import/prod/oa_flow_finance.json'; + +type Row = Record; + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +function num(v: unknown) { + if (v == null || v === '') return 0; + const n = Number(String(v).replace(/,/g, '')); + return Number.isFinite(n) ? n : 0; +} + +function when(v: unknown) { + const s = str(v); + if (!s || s.startsWith('0000')) return null; + const d = new Date(s.includes('T') ? s : s.replace(' ', 'T')); + return Number.isNaN(d.getTime()) ? null : d; +} + +function clip(v: unknown, n = 200) { + const s = str(v); + return s.length > n ? s.slice(0, n) : s; +} + +async function main() { + const flow = parseOaJson>(FLOW); + const itemsByReimb = new Map(); + for (const item of flow.reimbursement_item || []) { + const key = str(item.reimbursement_id); + if (!key) continue; + const list = itemsByReimb.get(key) || []; + list.push(item); + itemsByReimb.set(key, list); + } + + let claims = 0; + let lines = 0; + let skipped = 0; + for (const r of flow.reimbursement || []) { + const claimNo = str(r.reimburse_no); + if (!claimNo) continue; + const row = await prisma.expenseClaim.findFirst({ where: { tenantId: '1', claimNo } }); + if (!row) { + skipped += 1; + continue; + } + const incoming = itemsByReimb.get(str(r.id)) || []; + await prisma.$transaction(async (tx) => { + await tx.expenseLine.deleteMany({ where: { claimId: row.id } }); + for (const [i, line] of incoming.entries()) { + await tx.expenseLine.create({ + data: { + claimId: row.id, + kind: mapExpenseLineKind(str(line.item_type), str(line.remark)), + amount: new Prisma.Decimal(num(line.amount)), + invoiceNo: clip(line.invoice_no, 80) || undefined, + occurAt: when(line.expense_date) || undefined, + remark: clip(line.remark || line.item_type, 200) || undefined, + sortNo: num(line.sort_order) || i, + }, + }); + lines += 1; + } + }); + claims += 1; + } + + const sample = await prisma.expenseClaim.findFirst({ + where: { claimNo: 'BX-20260828-002' }, + include: { lines: { orderBy: { sortNo: 'asc' } } }, + }); + const sum = sample?.lines.reduce((s, l) => s + Number(l.amount), 0); + console.log( + JSON.stringify( + { + claims, + lines, + skipped, + sample: sample + ? { + claimNo: sample.claimNo, + header: Number(sample.amount), + lineCount: sample.lines.length, + lineSum: sum, + kinds: sample.lines.map((l) => `${l.kind}:${l.amount}:${l.remark}`), + } + : null, + }, + null, + 2, + ), + ); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/fix-oa-posts.ts b/apps/api/prisma/fix-oa-posts.ts new file mode 100644 index 0000000..5280f73 --- /dev/null +++ b/apps/api/prisma/fix-oa-posts.ts @@ -0,0 +1,141 @@ +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; + +const prisma = new PrismaClient(); +const DUMP = '/opt/import/prod/oa_hr.json'; + +const ROLES: { code: string; name: string; dataScope: string }[] = [ + { code: 'rd_director', name: '研发总监', dataScope: 'DEPT' }, + { code: '3d_director', name: '三维总监', dataScope: 'DEPT' }, + { code: 'video_director', name: '摄制总监', dataScope: 'DEPT' }, + { code: 'material_director', name: '物资总监', dataScope: 'DEPT' }, + { code: 'pm', name: '项目经理', dataScope: 'DEPT' }, + { code: 'rd_staff', name: '研发技术岗', dataScope: 'SELF' }, + { code: '3d_staff', name: '三维技术岗', dataScope: 'SELF' }, + { code: 'video_staff', name: '摄制技术岗', dataScope: 'SELF' }, + { code: 'material_staff', name: '物资技术岗', dataScope: 'SELF' }, + { code: 'tech', name: '普通岗位', dataScope: 'SELF' }, +]; + +const POST_ROLE: Record = { + sysadmin: 'admin', + boss: 'owner', + business_director: 'biz_director', + business_staff: 'biz_staff', + rd_director: 'rd_director', + '3d_director': '3d_director', + video_director: 'video_director', + material_director: 'material_director', + pm: 'pm', + tech: 'tech', + rd_staff: 'rd_staff', + '3d_staff': '3d_staff', + video_staff: 'video_staff', + material_staff: 'material_staff', +}; + +function pickRole(userName: string, postCodes: string[]) { + if (userName === 'xibei') return 'employee'; + if (userName === 'administrator' || userName === 'admin') return 'admin'; + const rank: Record = { + employee: 0, + tech: 1, + rd_staff: 1, + '3d_staff': 1, + video_staff: 1, + material_staff: 1, + pm: 2, + biz_staff: 3, + rd_director: 4, + '3d_director': 4, + video_director: 4, + material_director: 4, + tech_director: 4, + biz_director: 5, + owner: 6, + admin: 7, + }; + let best: string | null = null; + let bestRank = -1; + for (const c of postCodes) { + const role = POST_ROLE[c]; + if (!role) continue; + const r = rank[role] ?? 1; + if (r > bestRank) { + best = role; + bestRank = r; + } + } + return best || 'employee'; +} + +async function main() { + for (const r of ROLES) { + const existing = await prisma.role.findFirst({ where: { code: r.code } }); + if (existing) { + await prisma.role.update({ where: { id: existing.id }, data: { name: r.name, dataScope: r.dataScope } }); + } else { + await prisma.role.create({ data: { name: r.name, code: r.code, dataScope: r.dataScope } }); + } + } + await prisma.role.updateMany({ where: { code: 'biz_staff' }, data: { name: '商务专员' } }); + await prisma.role.updateMany({ where: { code: 'biz_director' }, data: { name: '商务总监' } }); + await prisma.role.updateMany({ where: { code: 'owner' }, data: { name: '企业负责人' } }); + + const dump = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as { + posts: { id: number; code: string; name: string }[]; + users: { id: number; userName: string; nickName: string }[]; + userPosts: { postId: number; userId: number }[]; + }; + const postById = new Map(dump.posts.map((p) => [p.id, p])); + const postsByUser = new Map(); + for (const up of dump.userPosts) { + const post = postById.get(up.postId); + if (!post) continue; + const list = postsByUser.get(up.userId) || []; + list.push(post); + postsByUser.set(up.userId, list); + } + + const roles = await prisma.role.findMany(); + const roleByCode = new Map(roles.map((r) => [r.code, r.id])); + let n = 0; + for (const u of dump.users) { + if (u.userName === 'administrator') continue; + const user = await prisma.user.findFirst({ where: { username: u.userName } }); + if (!user) continue; + const posts = postsByUser.get(u.id) || []; + const roleCode = pickRole(u.userName, posts.map((p) => p.code)); + const roleId = roleByCode.get(roleCode); + if (!roleId) continue; + await prisma.userRole.deleteMany({ where: { userId: user.id } }); + await prisma.userRole.create({ data: { userId: user.id, roleId } }); + const title = posts.find((p) => POST_ROLE[p.code] === roleCode)?.name || posts[0]?.name; + if (title && user.employeeId) { + await prisma.employee.update({ where: { id: user.employeeId }, data: { title } }); + } + n += 1; + } + + const delQual = await prisma.qualification.deleteMany({ where: { code: 'QUAL_P6_OLD' } }); + + const cancelled = await prisma.approvalTask.updateMany({ + where: { bizType: 'EXPENSE_APPROVAL', status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + + console.log( + JSON.stringify({ + reassignedUsers: n, + deletedTestQuals: delQual.count, + cancelledExpenseApprovals: cancelled.count, + }), + ); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/grant-admin-equivalents.ts b/apps/api/prisma/grant-admin-equivalents.ts new file mode 100644 index 0000000..8f4e75b --- /dev/null +++ b/apps/api/prisma/grant-admin-equivalents.ts @@ -0,0 +1,40 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + const admin = await prisma.role.findFirst({ where: { code: 'admin' } }); + if (!admin) throw new Error('缺少 admin 角色'); + await prisma.role.updateMany({ where: { code: 'rd_director' }, data: { dataScope: 'ALL' } }); + + const users = await prisma.user.findMany({ + where: { + status: 'ACTIVE', + OR: [ + { displayName: { contains: '潘兴辉' } }, + { employee: { name: { contains: '潘兴辉' } } }, + { roles: { some: { role: { code: 'rd_director' } } } }, + ], + }, + select: { id: true, displayName: true, username: true }, + }); + if (!users.length) throw new Error('没有找到潘兴辉或研发总监账号'); + + const result = await prisma.userRole.createMany({ + data: users.map((u) => ({ userId: u.id, roleId: admin.id })), + skipDuplicates: true, + }); + console.log( + `granted admin to: ${users.map((u) => u.displayName).join('、')} (new links ${result.count})`, + ); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/grant-sun-chunqing.ts b/apps/api/prisma/grant-sun-chunqing.ts new file mode 100644 index 0000000..13ac52a --- /dev/null +++ b/apps/api/prisma/grant-sun-chunqing.ts @@ -0,0 +1,41 @@ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +async function main() { + const user = await prisma.user.findFirst({ + where: { + status: 'ACTIVE', + OR: [{ displayName: { contains: '孙春庆' } }, { employee: { name: { contains: '孙春庆' } } }], + }, + include: { roles: true }, + }); + if (!user) throw new Error('没有找到孙春庆'); + + const role = + (await prisma.role.findFirst({ where: { code: 'bid_screener' } })) || + (await prisma.role.create({ + data: { name: '项目筛选发起', code: 'bid_screener', dataScope: 'SELF' }, + })); + + const perms = await prisma.permission.findMany({ + where: { code: { in: ['office', 'office:overview', 'bid', 'bid:screening'] } }, + }); + await prisma.rolePermission.createMany({ + data: perms.map((p) => ({ roleId: role.id, permissionId: p.id })), + skipDuplicates: true, + }); + await prisma.userRole.createMany({ + data: [{ userId: user.id, roleId: role.id }], + skipDuplicates: true, + }); + console.log(`granted bid screening to ${user.displayName} (${user.username})`); +} + +main() + .then(async () => prisma.$disconnect()) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/import-hr.ts b/apps/api/prisma/import-hr.ts new file mode 100644 index 0000000..5e63d20 --- /dev/null +++ b/apps/api/prisma/import-hr.ts @@ -0,0 +1,587 @@ +// @ts-nocheck +import { PrismaClient } from '@prisma/client'; +import * as argon2 from 'argon2'; +import * as fs from 'fs'; +import * as path from 'path'; + +const prisma = new PrismaClient(); +const DUMP = path.resolve('/opt/import/prod/oa_hr.json'); +const DEFAULT_PASSWORD = '123456'; + +const DEMO_USERS = [ + 'owner', + 'bizdir', + 'bizstaff', + 'dirsoft', + 'dir3d', + 'dirfilm', + 'dirmaterial', + 'finance', + 'hr', + 'employee', + 'softpm', + 'batchdemo1', +]; + +const DEPT_META: Record = { + 1: { deptType: 'ADMIN' }, + 2: { deptType: 'ADMIN' }, + 4: { deptType: 'BUSINESS' }, + 7: { deptType: 'ADMIN' }, + 8: { deptType: 'ADMIN' }, + 9: { deptType: 'TECH', categoryCode: 'CAT_SOFTWARE' }, + 10: { deptType: 'TECH', categoryCode: 'CAT_SOFTWARE' }, + 19: { deptType: 'TECH', categoryCode: 'CAT_FILM' }, + 23: { deptType: 'TECH', categoryCode: 'CAT_3D' }, + 210: { deptType: 'TECH', categoryCode: 'CAT_SOFTWARE' }, + 211: { deptType: 'TECH', categoryCode: 'CAT_3D' }, + 212: { deptType: 'TECH', categoryCode: 'CAT_FILM' }, + 213: { deptType: 'TECH', categoryCode: 'CAT_MATERIAL' }, + 214: { deptType: 'TECH', categoryCode: 'CAT_SOFTWARE' }, +}; + +const OLD_DEPT_TO_OA: Record = { + ROOT: 'OA_1', + BIZ: 'OA_4', + TECH_SOFTWARE: 'OA_9', + TECH_3D: 'OA_211', + TECH_FILM: 'OA_212', + TECH_MATERIAL: 'OA_213', + FIN: 'OA_2', + HR: 'OA_2', +}; + +const DEPT_MANAGER_OA: Record = { + 1: 101, + 2: 101, + 4: 107, + 7: 101, + 8: 101, + 9: 131, + 10: 131, + 19: 102, + 23: 105, + 210: 131, + 211: 105, + 212: 102, + 213: 104, + 214: 131, +}; + +const POST_ROLE: Record = { + sysadmin: 'admin', + boss: 'owner', + business_director: 'biz_director', + business_staff: 'biz_staff', + rd_director: 'rd_director', + '3d_director': '3d_director', + video_director: 'video_director', + material_director: 'material_director', + pm: 'pm', + tech: 'tech', + rd_staff: 'rd_staff', + '3d_staff': '3d_staff', + video_staff: 'video_staff', + material_staff: 'material_staff', +}; + +const ROLE_RANK: Record = { + employee: 0, + tech: 1, + rd_staff: 1, + '3d_staff': 1, + video_staff: 1, + material_staff: 1, + pm: 2, + biz_staff: 3, + tech_director: 4, + rd_director: 4, + '3d_director': 4, + video_director: 4, + material_director: 4, + biz_director: 5, + owner: 6, + admin: 7, +}; + +type Dump = { + depts: { id: number; name: string; parentId: number; orderNum: number; delFlag: string }[]; + posts: { id: number; code: string; name: string }[]; + users: { + id: number; + sex: string; + email: string; + phone: string; + deptId: number; + remark: string | null; + status: string; + nickName: string; + userName: string; + entryDate: string | null; + leaveDate: string | null; + employmentStatus: string; + }[]; + userPosts: { postId: number; userId: number }[]; +}; + +function empNo(oaId: number) { + return `E${String(oaId).padStart(4, '0')}`; +} + +function gender(sex: string) { + if (sex === '0') return 'MALE'; + if (sex === '1') return 'FEMALE'; + return null; +} + +function empStatus(code: string) { + if (code === '1') return 'INTERN'; + if (code === '2') return 'PROBATION'; + if (code === '4') return 'LEFT'; + return 'REGULAR'; +} + +function when(v: string | null) { + if (!v || v.startsWith('0000')) return null; + const d = new Date(v.includes('T') ? v : `${v}T00:00:00`); + return Number.isNaN(d.getTime()) ? null : d; +} + +function addMonths(d: Date, n: number) { + const x = new Date(d); + x.setMonth(x.getMonth() + n); + return x; +} + +function pickRole(userName: string, postCodes: string[]) { + if (userName === 'xibei') return 'employee'; + let best: string | null = null; + let bestRank = -1; + for (const c of postCodes) { + const role = POST_ROLE[c]; + if (!role) continue; + const r = ROLE_RANK[role] ?? 1; + if (r > bestRank) { + best = role; + bestRank = r; + } + } + return best || 'employee'; +} + +async function remumber( + rows: T[], + prefix: string, + update: (id: string, no: string) => Promise, +) { + for (const r of rows) await update(r.id, `TMP-${r.id.replace(/-/g, '').slice(0, 20)}`); + const groups = new Map(); + const sorted = [...rows].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime() || a.id.localeCompare(b.id)); + for (const r of sorted) { + const y = r.createdAt.getFullYear(); + const list = groups.get(y) || []; + list.push(r); + groups.set(y, list); + } + for (const [year, list] of [...groups.entries()].sort((a, b) => a[0] - b[0])) { + let i = 1; + for (const r of list) { + await update(r.id, `${prefix}-${year}-${String(i).padStart(5, '0')}`); + i += 1; + } + } +} + +async function remapDeptFks(codeToId: Map) { + const old = await prisma.department.findMany({ where: { tenantId: '1' } }); + const oldByCode = new Map(old.map((d) => [d.code, d.id])); + const mapping = new Map(); + for (const d of old) { + const targetCode = OLD_DEPT_TO_OA[d.code] || (d.code.startsWith('OA_') ? d.code : 'OA_2'); + const nid = codeToId.get(targetCode); + if (nid && nid !== d.id) mapping.set(d.id, nid); + } + for (const [from, to] of mapping) { + await prisma.projectTask.updateMany({ where: { departmentId: from }, data: { departmentId: to } }); + await prisma.employmentEvent.updateMany({ where: { fromDeptId: from }, data: { fromDeptId: to } }); + await prisma.employmentEvent.updateMany({ where: { toDeptId: from }, data: { toDeptId: to } }); + } + return { oldByCode, mapping }; +} + +async function reassignDemoUsers(demoIds: string[], adminId: string) { + if (!demoIds.length) return; + await prisma.bidReviewer.deleteMany({ where: { userId: { in: demoIds } } }); + await prisma.bidAssignee.deleteMany({ where: { userId: { in: demoIds } } }); + await prisma.contractReviewer.deleteMany({ where: { userId: { in: demoIds } } }); + await prisma.todoTask.deleteMany({ where: { assigneeId: { in: demoIds } } }); + await prisma.approvalTask.deleteMany({ where: { assigneeId: { in: demoIds } } }); + await prisma.workReport.deleteMany({ where: { OR: [{ authorId: { in: demoIds } }, { toUserId: { in: demoIds } }, { ccUserId: { in: demoIds } }] } }); + await prisma.refreshToken.deleteMany({ where: { userId: { in: demoIds } } }); + await prisma.userRole.deleteMany({ where: { userId: { in: demoIds } } }); + + const patch = async (model: string, field: string) => { + const delegate = (prisma as unknown as Record Promise }>)[model]; + if (!delegate?.updateMany) return; + try { + await delegate.updateMany({ + where: { [field]: { in: demoIds } }, + data: { [field]: adminId }, + }); + } catch (e) { + console.warn(`skip ${model}.${field}`, (e as Error).message); + } + }; + + await patch('bidCase', 'createdById'); + await patch('bidActionLog', 'actorId'); + await patch('bidDocVersion', 'submittedById'); + await patch('contract', 'createdById'); + await patch('contractActionLog', 'actorId'); + await patch('expenseClaim', 'applicantId'); + await patch('expenseClaim', 'decidedById'); + await patch('loanRecord', 'applicantId'); + await patch('loanRecord', 'decidedById'); + await patch('sealRequest', 'applicantId'); + await patch('sealRequest', 'decidedById'); + await patch('timesheet', 'userId'); + await patch('projectTask', 'assigneeId'); + await patch('projectAcceptance', 'actorId'); + await patch('employmentEvent', 'actorId'); + await patch('attendanceMonth', 'importedById'); + await patch('payrollRun', 'createdById'); + await patch('purchaseRequest', 'applicantId'); + await patch('purchaseRequest', 'decidedById'); + await patch('paymentRecord', 'createdById'); + await patch('projectChange', 'actorId'); + await patch('assetOccupancy', 'holderId'); + await patch('invoiceRecord', 'createdById'); + await patch('operationLog', 'userId'); + await patch('credentialBorrow', 'borrowerId'); + + await prisma.user.deleteMany({ where: { id: { in: demoIds } } }); +} + +async function main() { + const dump = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Dump; + const hash = await argon2.hash(DEFAULT_PASSWORD); + const posts = new Map(dump.posts.map((p) => [p.id, p])); + const postsByUser = new Map(); + const postNamesByUser = new Map(); + for (const up of dump.userPosts) { + const p = posts.get(up.postId); + if (!p) continue; + postsByUser.set(up.userId, [...(postsByUser.get(up.userId) || []), p.code]); + postNamesByUser.set(up.userId, [...(postNamesByUser.get(up.userId) || []), p.name]); + } + + await prisma.role.updateMany({ where: { code: { in: ['admin', 'owner', 'finance', 'hr'] } }, data: { dataScope: 'ALL' } }); + await prisma.role.updateMany({ where: { code: { in: ['biz_director', 'tech_director'] } }, data: { dataScope: 'DEPT' } }); + await prisma.role.updateMany({ where: { code: { in: ['biz_staff', 'employee'] } }, data: { dataScope: 'SELF' } }); + + const numberRules = [ + { code: 'BID', name: '投标编号', prefix: 'BID', padLength: 5 }, + { code: 'HT', name: '合同编号', prefix: 'HT', padLength: 5 }, + { code: 'PRJ', name: '项目编号', prefix: 'PRJ', padLength: 5 }, + { code: 'BX', name: '报销编号', prefix: 'BX', padLength: 5 }, + { code: 'JK', name: '借款编号', prefix: 'JK', padLength: 5 }, + { code: 'BZ', name: '保证金编号', prefix: 'BZ', padLength: 5 }, + ]; + for (const r of numberRules) { + await prisma.numberRule.upsert({ + where: { tenantId_code: { tenantId: '1', code: r.code } }, + update: { prefix: r.prefix, padLength: r.padLength, name: r.name }, + create: { ...r, tenantId: '1' }, + }); + } + + const roles = await prisma.role.findMany(); + const roleByCode = new Map(roles.map((r) => [r.code, r.id])); + + const codeToId = new Map(); + const sortedDepts = [...dump.depts].sort((a, b) => a.parentId - b.parentId || a.orderNum - b.orderNum); + for (const d of sortedDepts) { + const code = `OA_${d.id}`; + const meta = DEPT_META[d.id] || { deptType: 'ADMIN' }; + const parentCode = d.parentId ? `OA_${d.parentId}` : null; + const parentId = parentCode ? codeToId.get(parentCode) || null : null; + const existing = await prisma.department.findFirst({ where: { tenantId: '1', code } }); + const row = existing + ? await prisma.department.update({ + where: { id: existing.id }, + data: { + name: d.name, + deptType: meta.deptType, + categoryCode: meta.categoryCode || null, + sortNo: d.orderNum, + parentId, + }, + }) + : await prisma.department.create({ + data: { + name: d.name, + code, + deptType: meta.deptType, + categoryCode: meta.categoryCode || null, + sortNo: d.orderNum, + parentId, + }, + }); + codeToId.set(code, row.id); + } + + await remapDeptFks(codeToId); + + const officeId = codeToId.get('OA_2')!; + const empByOa = new Map(); + const empByName = new Map(); + + const adminUser = await prisma.user.findFirst({ where: { username: 'admin' } }); + if (!adminUser) throw new Error('本地必须先有 admin 账号'); + + const adminEmpId = + adminUser.employeeId || + ( + await prisma.employee.create({ + data: { + name: '系统管理员', + employeeNo: 'E0000', + departmentId: officeId, + title: '系统管理员', + employmentStatus: 'REGULAR', + }, + }) + ).id; + + const adminOa = dump.users.find((u) => u.userName === 'administrator'); + await prisma.employee.update({ + where: { id: adminEmpId }, + data: { + name: adminOa?.nickName || '系统管理员', + employeeNo: 'E0000', + departmentId: officeId, + title: '系统管理员', + employmentStatus: 'REGULAR', + hiredAt: when(adminOa?.entryDate || '2026-06-04'), + gender: gender(adminOa?.sex || '2'), + email: adminOa?.email || null, + mobile: adminOa?.phone || null, + remark: adminOa?.remark || '超级管理员', + managerId: null, + }, + }); + empByOa.set(1, adminEmpId); + empByName.set('系统管理员', adminEmpId); + + for (const u of dump.users) { + if (u.userName === 'administrator') continue; + const deptId = codeToId.get(`OA_${u.deptId}`) || officeId; + const status = empStatus(u.employmentStatus); + const hiredAt = when(u.entryDate); + const titles = postNamesByUser.get(u.id) || []; + const title = titles[0] || u.remark || '员工'; + const no = empNo(u.id); + const data = { + name: u.nickName, + departmentId: deptId, + title, + restSchedule: DEPT_META[u.deptId]?.categoryCode === 'CAT_SOFTWARE' ? 'DOUBLE' : 'SINGLE', + employmentStatus: status, + hiredAt, + internStartAt: status === 'INTERN' ? hiredAt : null, + probationEndAt: status === 'PROBATION' && hiredAt ? addMonths(hiredAt, 3) : null, + leftAt: when(u.leaveDate), + gender: gender(u.sex), + email: u.email || null, + mobile: u.phone || null, + remark: u.remark, + }; + const existing = await prisma.employee.findFirst({ where: { tenantId: '1', employeeNo: no } }); + const row = existing + ? await prisma.employee.update({ where: { id: existing.id }, data }) + : await prisma.employee.create({ data: { ...data, employeeNo: no } }); + empByOa.set(u.id, row.id); + empByName.set(u.nickName, row.id); + } + + for (const u of dump.users) { + if (u.userName === 'administrator') continue; + const mgrOa = DEPT_MANAGER_OA[u.deptId]; + if (!mgrOa || mgrOa === u.id) continue; + const managerId = empByOa.get(mgrOa); + const id = empByOa.get(u.id); + if (managerId && id) await prisma.employee.update({ where: { id }, data: { managerId } }); + } + + await prisma.user.update({ + where: { id: adminUser.id }, + data: { + passwordHash: hash, + displayName: '系统管理员', + employeeId: adminEmpId, + status: 'ACTIVE', + }, + }); + + for (const u of dump.users) { + if (u.userName === 'administrator') continue; + const employeeId = empByOa.get(u.id)!; + const roleCode = pickRole(u.userName, postsByUser.get(u.id) || []); + const roleId = roleByCode.get(roleCode); + if (!roleId) throw new Error(`缺少角色 ${roleCode}`); + const disabled = u.status === '1' || u.employmentStatus === '4'; + const existing = await prisma.user.findFirst({ where: { tenantId: '1', username: u.userName } }); + const row = existing + ? await prisma.user.update({ + where: { id: existing.id }, + data: { + passwordHash: hash, + displayName: u.nickName, + employeeId, + status: disabled ? 'DISABLED' : 'ACTIVE', + }, + }) + : await prisma.user.create({ + data: { + username: u.userName, + passwordHash: hash, + displayName: u.nickName, + employeeId, + status: disabled ? 'DISABLED' : 'ACTIVE', + }, + }); + await prisma.userRole.deleteMany({ where: { userId: row.id } }); + await prisma.userRole.create({ data: { userId: row.id, roleId } }); + } + + const users = await prisma.user.findMany({ include: { employee: true } }); + const userByName = new Map(users.map((u) => [u.displayName, u.id])); + userByName.set('系统管理员', adminUser.id); + + const rematchByText = async ( + model: 'expenseClaim' | 'loanRecord' | 'sealRequest', + field: 'remark' | 'reason', + ) => { + const rows = await prisma[model].findMany({ select: { id: true, [field]: true } as never }); + let n = 0; + for (const r of rows as { id: string; remark?: string; reason?: string }[]) { + const name = ((r.remark || r.reason || '') as string).split(' / ')[0]?.trim(); + if (!name) continue; + const uid = userByName.get(name); + if (!uid) continue; + await prisma[model].update({ where: { id: r.id }, data: { applicantId: uid } }); + n += 1; + } + console.log(`matched ${model}`, n); + }; + await rematchByText('expenseClaim', 'remark'); + await rematchByText('loanRecord', 'remark'); + await rematchByText('sealRequest', 'reason'); + + const keepUsernames = new Set(['admin', ...dump.users.filter((u) => u.userName !== 'administrator').map((u) => u.userName)]); + const demo = await prisma.user.findMany({ + where: { OR: [{ username: { in: DEMO_USERS } }, { username: { notIn: [...keepUsernames] } }] }, + }); + const demoIds = demo.filter((u) => u.username !== 'admin').map((u) => u.id); + console.log( + 'delete demo users', + demo.filter((u) => u.username !== 'admin').map((u) => u.username), + ); + await reassignDemoUsers(demoIds, adminUser.id); + + const keepEmp = new Set([adminEmpId, ...empByOa.values()]); + const leftoverEmp = await prisma.employee.findMany({ where: { id: { notIn: [...keepEmp] } } }); + const leftoverIds = leftoverEmp.map((e) => e.id); + if (leftoverIds.length) { + await prisma.payrollItem.deleteMany({ where: { employeeId: { in: leftoverIds } } }); + await prisma.attendanceMonth.deleteMany({ where: { employeeId: { in: leftoverIds } } }); + await prisma.employmentEvent.deleteMany({ where: { employeeId: { in: leftoverIds } } }); + await prisma.user.updateMany({ where: { employeeId: { in: leftoverIds } }, data: { employeeId: null } }); + await prisma.employee.updateMany({ where: { managerId: { in: leftoverIds } }, data: { managerId: null } }); + await prisma.employee.deleteMany({ where: { id: { in: leftoverIds } } }); + } + console.log( + 'deleted demo employees', + leftoverEmp.map((e) => e.name), + ); + + const keepDept = new Set(codeToId.values()); + const leftoverDept = await prisma.department.findMany({ where: { id: { notIn: [...keepDept] } } }); + if (leftoverDept.length) { + const leftoverDeptIds = leftoverDept.map((d) => d.id); + await prisma.employee.updateMany({ + where: { departmentId: { in: leftoverDeptIds } }, + data: { departmentId: officeId }, + }); + await prisma.projectTask.updateMany({ + where: { departmentId: { in: leftoverDeptIds } }, + data: { departmentId: officeId }, + }); + await prisma.department.updateMany({ where: { id: { in: leftoverDeptIds } }, data: { parentId: null } }); + await prisma.department.deleteMany({ where: { id: { in: leftoverDeptIds } } }); + } + console.log( + 'deleted demo depts', + leftoverDept.map((d) => `${d.code}:${d.name}`), + ); + + const demoBid = await prisma.bidCase.findMany({ where: { name: '某市展厅数字化升级' } }); + for (const b of demoBid) { + const used = + (await prisma.expenseClaim.count({ where: { bidCaseId: b.id } })) + + (await prisma.contract.count({ where: { bidCaseId: b.id } })); + if (used) continue; + await prisma.bidReviewer.deleteMany({ where: { bidCaseId: b.id } }); + await prisma.bidAssignee.deleteMany({ where: { bidCaseId: b.id } }); + await prisma.bidActionLog.deleteMany({ where: { bidCaseId: b.id } }); + await prisma.bidDocVersion.deleteMany({ where: { bidCaseId: b.id } }); + await prisma.bondRecord.deleteMany({ where: { bidCaseId: b.id } }); + await prisma.sealRequest.deleteMany({ where: { bidCaseId: b.id } }); + await prisma.bidCase.delete({ where: { id: b.id } }); + console.log('deleted demo bid', b.bidNo); + } + + await remumber( + await prisma.bidCase.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), + 'BID', + (id, bidNo) => prisma.bidCase.update({ where: { id }, data: { bidNo } }), + ); + await remumber( + await prisma.contract.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), + 'HT', + (id, contractNo) => prisma.contract.update({ where: { id }, data: { contractNo } }), + ); + await remumber( + await prisma.project.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), + 'PRJ', + (id, projectNo) => prisma.project.update({ where: { id }, data: { projectNo } }), + ); + await remumber( + await prisma.expenseClaim.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), + 'BX', + (id, claimNo) => prisma.expenseClaim.update({ where: { id }, data: { claimNo } }), + ); + await remumber( + await prisma.loanRecord.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), + 'JK', + (id, loanNo) => prisma.loanRecord.update({ where: { id }, data: { loanNo } }), + ); + await remumber( + await prisma.bondRecord.findMany({ select: { id: true, createdAt: true }, orderBy: { createdAt: 'asc' } }), + 'BZ', + (id, bondNo) => prisma.bondRecord.update({ where: { id }, data: { bondNo } }), + ); + + const empCount = await prisma.employee.count(); + const userCount = await prisma.user.count(); + const deptCount = await prisma.department.count(); + const sample = await prisma.expenseClaim.findMany({ take: 3, include: { applicant: true }, orderBy: { createdAt: 'desc' } }); + console.log({ empCount, userCount, deptCount, sampleNos: sample.map((s) => `${s.claimNo}:${s.applicant.displayName}`) }); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/import-notices-from-oa.ts b/apps/api/prisma/import-notices-from-oa.ts new file mode 100644 index 0000000..aea2fe3 --- /dev/null +++ b/apps/api/prisma/import-notices-from-oa.ts @@ -0,0 +1,306 @@ +import { createReadStream, existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { createGunzip } from 'zlib'; +import { basename, join } from 'path'; +import { createHash, randomUUID } from 'crypto'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); +const DUMP = process.env.OA_DUMP || '/opt/backup/cloud-20260830/oa.sql.gz'; +const UPLOAD_ROOT = process.env.OA_UPLOAD || '/opt/backup/cloud-20260830/oa-uploadPath'; +const SKIP_TITLE = /已更新至|发现新版本|推荐更新|桌面端|移动端|^App /; + +function stableId(key: string) { + const hex = createHash('sha1').update(key).digest('hex'); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`; +} + +function parseMysqlTuples(valuesSql: string): unknown[][] { + const rows: unknown[][] = []; + let i = 0; + const s = valuesSql; + while (i < s.length) { + while (i < s.length && ' \n\r\t,'.includes(s[i])) i += 1; + if (i >= s.length) break; + if (s[i] !== '(') throw new Error(`expected ( at ${i}`); + i += 1; + const row: unknown[] = []; + while (true) { + while (i < s.length && ' \n\r\t'.includes(s[i])) i += 1; + if (s.startsWith('_binary ', i)) i += 8; + if (s.startsWith('NULL', i) && (i + 4 === s.length || ',) \n\r\t'.includes(s[i + 4]))) { + row.push(null); + i += 4; + } else if (s[i] === "'" || s[i] === '"') { + const q = s[i]; + i += 1; + let out = ''; + while (i < s.length) { + const ch = s[i]; + if (ch === '\\') { + const n = s[i + 1]; + out += n === 'n' ? '\n' : n === 'r' ? '\r' : n === 't' ? '\t' : n === '0' ? '\0' : n; + i += 2; + continue; + } + if (ch === q) { + if (s[i + 1] === q) { + out += q; + i += 2; + continue; + } + i += 1; + break; + } + out += ch; + i += 1; + } + row.push(out); + } else { + const start = i; + while (i < s.length && s[i] !== ',' && s[i] !== ')') i += 1; + const raw = s.slice(start, i).trim(); + row.push(raw === '' ? null : /^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : raw); + } + while (i < s.length && ' \n\r\t'.includes(s[i])) i += 1; + if (s[i] === ',') { + i += 1; + continue; + } + if (s[i] === ')') { + i += 1; + rows.push(row); + break; + } + throw new Error(`bad token at ${i}: ${s.slice(i, i + 20)}`); + } + } + return rows; +} + +async function readDump(): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + createReadStream(DUMP) + .pipe(createGunzip()) + .on('data', (c: Buffer) => chunks.push(c)) + .on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) + .on('error', reject); + }); +} + +function insertsFor(dump: string, table: string) { + const rows: unknown[][] = []; + const needle = `INSERT INTO \`${table}\` VALUES `; + let from = 0; + while (true) { + const start = dump.indexOf(needle, from); + if (start < 0) break; + let i = start + needle.length; + const s = dump; + while (i < s.length) { + while (i < s.length && ' \n\r\t'.includes(s[i])) i += 1; + if (s[i] === ';') { + i += 1; + break; + } + if (s[i] === ',') { + i += 1; + continue; + } + if (s[i] !== '(') break; + const sliceStart = i; + let depth = 0; + let inStr = false; + let q = ''; + while (i < s.length) { + const ch = s[i]; + if (inStr) { + if (ch === '\\') { + i += 2; + continue; + } + if (ch === q) { + inStr = false; + } + i += 1; + continue; + } + if (ch === "'" || ch === '"') { + inStr = true; + q = ch; + i += 1; + continue; + } + if (ch === '(') depth += 1; + if (ch === ')') { + depth -= 1; + if (depth === 0) { + i += 1; + break; + } + } + i += 1; + } + rows.push(...parseMysqlTuples(s.slice(sliceStart, i))); + } + from = i; + } + return rows; +} + +function mimeOf(name: string) { + const lower = name.toLowerCase(); + if (lower.endsWith('.pdf')) return 'application/pdf'; + if (lower.endsWith('.docx')) return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + if (lower.endsWith('.doc')) return 'application/msword'; + if (lower.endsWith('.xlsx')) return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + if (lower.endsWith('.xls')) return 'application/vnd.ms-excel'; + if (lower.endsWith('.png')) return 'image/png'; + if (lower.endsWith('.jpg') || lower.endsWith('.jpeg')) return 'image/jpeg'; + return 'application/octet-stream'; +} + +function resolveUpload(rel: string) { + const cleaned = rel.replace(/^\/profile\//, '').replace(/^\//, ''); + const candidates = [join(UPLOAD_ROOT, cleaned), join(UPLOAD_ROOT, 'upload', cleaned.replace(/^upload\//, ''))]; + return candidates.find((p) => existsSync(p)); +} + +async function attachFile(noticeId: string, rel: string, authorId: string) { + if (!rel?.trim()) return; + const src = resolveUpload(rel); + if (!src) { + console.warn('missing attachment', rel); + return; + } + const fileName = basename(src); + const buf = readFileSync(src); + const id = stableId(`oa-file:${rel}`); + const ext = (fileName.split('.').pop() || 'bin').replace(/[^a-zA-Z0-9]/g, '').slice(0, 8); + const storageKey = `NOTICE/${noticeId}/${id}.${ext || 'bin'}`; + const full = join(process.cwd(), 'uploads', storageKey); + mkdirSync(join(full, '..'), { recursive: true }); + writeFileSync(full, buf); + await prisma.fileAsset.upsert({ + where: { id }, + update: { fileName, size: buf.length, mimeType: mimeOf(fileName), storageKey, bizId: noticeId }, + create: { + id, + tenantId: '1', + bizType: 'NOTICE', + bizId: noticeId, + fileName, + mimeType: mimeOf(fileName), + size: buf.length, + storageKey, + uploadedById: authorId, + }, + }); +} + +async function upsertNotice(opts: { + key: string; + title: string; + content: string; + kind: 'ANNOUNCEMENT' | 'POLICY'; + createdAt?: Date | null; + authorId: string; + attachment?: string | null; +}) { + const title = opts.title.trim(); + if (!title) return; + const id = stableId(opts.key); + const content = opts.content || ''; + await prisma.notice.upsert({ + where: { id }, + update: { title, content, kind: opts.kind, status: 'PUBLISHED' }, + create: { + id, + tenantId: '1', + title, + content, + kind: opts.kind, + status: 'PUBLISHED', + createdById: opts.authorId, + createdAt: opts.createdAt || undefined, + }, + }); + if (opts.attachment) await attachFile(id, opts.attachment, opts.authorId); +} + +function asDate(v: unknown) { + if (!v) return null; + const d = new Date(String(v)); + return Number.isNaN(d.getTime()) ? null : d; +} + +async function main() { + const admin = await prisma.user.findFirst({ where: { username: '18115604840' } }); + if (!admin) throw new Error('缺少管理员账号'); + await prisma.notice.deleteMany({ where: { title: '未命名', status: 'DRAFT' } }); + + const dump = await readDump(); + let n = 0; + + for (const row of insertsFor(dump, 'oa_announcement')) { + const title = String(row[1] || ''); + const status = String(row[4] || ''); + if (status && status !== '正常') continue; + await upsertNotice({ + key: `oa-announcement:${row[0]}`, + title, + content: String(row[15] || ''), + kind: 'ANNOUNCEMENT', + createdAt: asDate(row[17]) || asDate(row[11]), + authorId: admin.id, + attachment: row[14] ? String(row[14]) : null, + }); + n += 1; + } + + for (const row of insertsFor(dump, 'oa_regulation')) { + const title = String(row[1] || ''); + const status = String(row[16] || ''); + if (status && status !== '正常') continue; + await upsertNotice({ + key: `oa-regulation:${row[0]}`, + title, + content: String(row[19] || ''), + kind: 'POLICY', + createdAt: asDate(row[15]) || asDate(row[21]), + authorId: admin.id, + attachment: row[18] ? String(row[18]) : null, + }); + n += 1; + } + + for (const row of insertsFor(dump, 'sys_notice')) { + const type = String(row[2] || ''); + const title = String(row[1] || ''); + const status = String(row[4] || '0'); + if (type !== '2') continue; + if (status === '1') continue; + if (SKIP_TITLE.test(title)) continue; + await upsertNotice({ + key: `sys-notice:${row[0]}`, + title, + content: String(row[3] || ''), + kind: 'ANNOUNCEMENT', + createdAt: asDate(row[7]), + authorId: admin.id, + }); + n += 1; + } + + const total = await prisma.notice.count(); + const files = await prisma.fileAsset.count({ where: { bizType: 'NOTICE' } }); + console.log(`imported ${n} source rows; notices=${total} files=${files}`); +} + +main() + .then(async () => prisma.$disconnect()) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/import-prod-complete.ts b/apps/api/prisma/import-prod-complete.ts new file mode 100644 index 0000000..6a9ac9f --- /dev/null +++ b/apps/api/prisma/import-prod-complete.ts @@ -0,0 +1,371 @@ +// @ts-nocheck +import { PrismaClient, Prisma } from '@prisma/client'; +import { createHash, randomUUID } from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; + +const prisma = new PrismaClient(); +const DUMP = '/opt/import/prod/oa_core.json'; +const HR = '/opt/import/prod/oa_hr.json'; +const OA_FILES = '/opt/import/prod/oa-files'; +const UPLOAD = path.join(process.cwd(), 'uploads'); + +type Row = Record; + +const MIME: Record = { + pdf: 'application/pdf', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + webp: 'image/webp', + doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + zip: 'application/zip', + rar: 'application/x-rar-compressed', +}; + +const NAME_ALIAS: Record = { + 张靖妍: '张婧妍', +}; + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +function num(v: unknown) { + if (v == null || v === '') return 0; + const n = Number(v); + return Number.isFinite(n) ? n : 0; +} + +function when(v: unknown) { + const s = str(v); + if (!s || s.startsWith('0000')) return null; + const d = new Date(s.includes('T') ? s : s.replace(' ', 'T')); + return Number.isNaN(d.getTime()) ? null : d; +} + +function clip(v: unknown, n = 2000) { + const s = str(v); + return s.length > n ? s.slice(0, n) : s || undefined; +} + +function mapProjectType(v: unknown, projectName?: unknown) { + const name = str(projectName); + if (name.includes('三维')) return '三维'; + if (/(视频.{0,12}(制作|摄制|拍摄)|摄制|拍摄|摄影|宣传片|专题片|纪录片|微电影|影视制作)/.test(name)) return '摄制'; + const s = str(v); + if (s.includes('三维')) return '三维'; + if (s.includes('摄制') || s.includes('视频')) return '摄制'; + if (s.includes('物资')) return '物资'; + if (s.includes('集成')) return '集成'; + return '软件'; +} + +function mapQual(raw: unknown) { + const s = str(raw); + if (!s) return '无需资质'; + try { + const arr = JSON.parse(s) as { item?: string }[]; + const item = str(arr?.[0]?.item); + if (!item || item === '无') return '无需资质'; + if (item.includes('军')) return '军二'; + if (item.includes('涉')) return '涉乙'; + return item.slice(0, 20); + } catch { + if (s.includes('军')) return '军二'; + if (s.includes('涉')) return '涉乙'; + return '其他'; + } +} + +function profilePaths(v: unknown) { + const out: string[] = []; + if (!v) return out; + for (const chunk of str(v).split(/[,;]/)) { + const piece = chunk.trim(); + const first = piece.split('|')[0].trim(); + const idx = first.indexOf('/profile/upload/'); + const p = idx >= 0 ? first.slice(idx) : first; + if (p.startsWith('/profile/upload/')) out.push(p); + } + return out; +} + +function relOf(p: string) { + return p.replace(/^\/profile\/upload\//, ''); +} + +function displayName(p: string) { + const pipe = str(p).split('|')[1]; + if (pipe && !pipe.includes('/')) return path.basename(pipe); + return path.basename(relOf(p.split('|')[0])); +} + +function mimeOf(name: string) { + const ext = name.split('.').pop()?.toLowerCase() || 'bin'; + return MIME[ext] || 'application/octet-stream'; +} + +function prettyName(name: string) { + try { + return decodeURIComponent(name); + } catch { + return name; + } +} + +async function attach( + bizType: string, + bizId: string, + urls: unknown, + uploadedById: string, + seen: Set, +) { + let n = 0; + let firstNas: string | undefined; + for (const raw of profilePaths(urls)) { + const rel = relOf(raw); + const src = path.join(OA_FILES, rel); + if (!fs.existsSync(src)) continue; + const buf = fs.readFileSync(src); + const hash = createHash('sha1').update(buf).digest('hex').slice(0, 16); + const key = `${bizId}:${hash}`; + if (seen.has(key)) continue; + seen.add(key); + const id = randomUUID(); + const ext = (path.extname(rel).replace('.', '') || 'bin').replace(/[^a-zA-Z0-9]/g, '').slice(0, 8); + const storageKey = `OA/${bizType}/${bizId}/${id}.${ext || 'bin'}`; + const dest = path.join(UPLOAD, storageKey); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(src, dest); + const name = prettyName(displayName(raw) || path.basename(rel)); + await prisma.fileAsset.create({ + data: { + id, + tenantId: '1', + bizType, + bizId, + fileName: name.slice(0, 180), + mimeType: mimeOf(name), + size: buf.length, + storageKey, + uploadedById, + }, + }); + n += 1; + firstNas ||= raw; + } + return { n, firstNas }; +} + +async function main() { + const raw = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Record; + const hr = JSON.parse(fs.readFileSync(HR, 'utf8')) as { users: { id: number; userName: string; nickName: string }[] }; + const admin = await prisma.user.findFirst({ where: { username: 'admin' } }); + if (!admin) throw new Error('没有 admin'); + fs.mkdirSync(UPLOAD, { recursive: true }); + + const users = await prisma.user.findMany(); + const byUsername = new Map(users.map((u) => [u.username, u])); + const byName = new Map(users.map((u) => [u.displayName, u])); + for (const [a, b] of Object.entries(NAME_ALIAS)) { + const u = byName.get(b); + if (u) byName.set(a, u); + } + const oaUser = new Map(hr.users.map((u) => [String(u.id), u.userName])); + const userFromOaId = (id: unknown) => { + const uname = oaUser.get(str(id)); + return uname ? byUsername.get(uname) : undefined; + }; + const userFromName = (name: unknown) => { + const n = NAME_ALIAS[str(name)] || str(name); + return byName.get(n); + }; + + await prisma.fileAsset.deleteMany({ where: { storageKey: { startsWith: 'OA/' } } }); + const seen = new Set(); + const stats = { bids: 0, bidFiles: 0, contracts: 0, contractFiles: 0, expenses: 0, expenseFiles: 0, projects: 0, purchases: 0 }; + + const bids = await prisma.bidCase.findMany(); + const bidByName = new Map(bids.map((b) => [b.name, b])); + const tenders = (raw.oa_tender || []).filter((t) => /^TB-/.test(str(t.system_number)) && str(t.project_name)); + + for (const t of tenders) { + const local = bidByName.get(str(t.project_name)); + if (!local) { + console.warn('no local bid', t.project_name); + continue; + } + const creator = userFromOaId(t.create_by) || userFromName(t.bid_prepare_person) || admin; + const drafter = userFromName(t.bid_prepare_person); + const bidder = userFromOaId(t.bid_delivery_person_id) || userFromName(t.bid_delivery_person); + const announce = await attach('BID_ANNOUNCE', local.id, t.announcement_file, creator.id, seen); + const extra = await attach('BID_FILE', local.id, t.attachment, creator.id, seen); + const formal = await attach('BID_FORMAL', local.id, t.formal_bid_file, creator.id, seen); + const proof = await attach('BID_PROOF', local.id, t.submit_proof_file, creator.id, seen); + const nas = announce.firstNas || extra.firstNas || formal.firstNas || proof.firstNas; + await prisma.bidCase.update({ + where: { id: local.id }, + data: { + externalNo: clip(t.project_number, 120) || local.externalNo, + projectType: mapProjectType(t.project_type, t.project_name), + source: clip(t.project_source, 50) || local.source, + qualificationNeed: mapQual(t.bid_doc_requirement), + tenderMethod: clip(t.tender_method, 50) || local.tenderMethod, + applyStartAt: when(t.create_time) || local.applyStartAt, + applyEndAt: when(t.file_apply_deadline) || local.applyEndAt, + applyMethod: announce.firstNas || extra.firstNas ? '网上申领' : '其他', + openAt: when(t.bid_open_date) || local.openAt, + openMethod: str(t.tender_address) ? '线下开标' : '其他', + openPlace: clip(t.tender_address, 200) || local.openPlace, + projectNature: clip(t.project_nature, 50) || local.projectNature, + securityLevel: str(t.security_level) || local.securityLevel, + contactName: clip(t.bid_contact_person, 64) || local.contactName, + contactPhone: clip(t.bid_contact_phone, 32) || local.contactPhone, + bondMethod: clip(t.deposit_pay_method, 20) || local.bondMethod, + bondStatus: clip(t.deposit_status, 20) || local.bondStatus, + bondPaidAt: when(t.deposit_pay_date) || local.bondPaidAt, + delivererName: clip(t.bid_delivery_person, 64) || local.delivererName, + priceCap: t.price_limit == null || t.price_limit === '' ? local.priceCap : String(t.price_limit), + quoteAmount: t.bid_quotation == null || t.bid_quotation === '' ? local.quoteAmount : String(t.bid_quotation), + summary: clip(t.project_content || t.project_intro, 4000) || local.summary, + nasPath: nas || local.nasPath, + remark: clip(t.remark, 1000) || local.remark, + createdById: creator.id, + }, + }); + if (drafter) { + await prisma.bidAssignee.upsert({ + where: { bidCaseId_userId_role: { bidCaseId: local.id, userId: drafter.id, role: 'DRAFTER' } }, + update: {}, + create: { bidCaseId: local.id, userId: drafter.id, role: 'DRAFTER' }, + }); + } + if (bidder) { + await prisma.bidAssignee.upsert({ + where: { bidCaseId_userId_role: { bidCaseId: local.id, userId: bidder.id, role: 'BIDDER' } }, + update: {}, + create: { bidCaseId: local.id, userId: bidder.id, role: 'BIDDER' }, + }); + } + if (formal.firstNas) { + const exists = await prisma.bidDocVersion.findFirst({ where: { bidCaseId: local.id, versionNo: 1 } }); + if (!exists) { + await prisma.bidDocVersion.create({ + data: { + bidCaseId: local.id, + versionNo: 1, + nasPath: formal.firstNas, + note: 'OA 正式标书', + submittedById: (drafter || creator).id, + }, + }); + } else { + await prisma.bidDocVersion.update({ + where: { id: exists.id }, + data: { nasPath: formal.firstNas }, + }); + } + } + stats.bids += 1; + stats.bidFiles += announce.n + extra.n + formal.n + proof.n; + } + + const contracts = await prisma.contract.findMany(); + const contractByName = new Map(contracts.map((c) => [c.name, c])); + for (const c of raw.oa_contract || []) { + const local = contractByName.get(str(c.contract_name)); + if (!local) { + console.warn('no local contract', c.contract_name); + continue; + } + const creator = userFromOaId(c.create_by) || userFromOaId(c.creator_id) || admin; + const original = await attach('CONTRACT', local.id, c.contract_original_url, creator.id, seen); + const accept = await attach('CONTRACT_ACCEPT', local.id, c.acceptance_url, creator.id, seen); + const award = await attach('CONTRACT_AWARD', local.id, c.award_notice_url, creator.id, seen); + await prisma.contract.update({ + where: { id: local.id }, + data: { + nasPath: original.firstNas || accept.firstNas || award.firstNas || local.nasPath, + signedAt: when(c.sign_date) || local.signedAt, + createdById: creator.id, + partyA: local.partyA || str(c.party_a_name) || undefined, + partyB: local.partyB || str(c.party_b_name) || undefined, + remark: + local.remark || + [str(c.payment_method) && `付款:${c.payment_method}`, str(c.remark)] + .filter(Boolean) + .join(' / ') || + undefined, + }, + }); + stats.contracts += 1; + stats.contractFiles += original.n + accept.n + award.n; + } + + const expenses = await prisma.expenseClaim.findMany(); + for (const r of raw.reimbursement || []) { + const amount = num(r.total_amount); + const name = str(r.employee_name); + const subject = str(r.subject); + let hits = expenses.filter((e) => Math.abs(Number(e.amount) - amount) < 0.009 && (e.remark || '').includes(name)); + if (subject) { + const tighter = hits.filter((e) => (e.remark || '').includes(subject)); + if (tighter.length) hits = tighter; + } + const stamp = when(r.created_time); + if (stamp && hits.length > 1) { + const day = stamp.toISOString().slice(0, 10); + const byDay = hits.filter((e) => (e.submittedAt || e.createdAt).toISOString().slice(0, 10) === day); + if (byDay.length) hits = byDay; + } + const local = hits[0]; + if (!local) continue; + const who = userFromName(name) || users.find((u) => u.id === local.applicantId) || admin; + const items = (raw.reimbursement_item || []).filter((i) => String(i.reimbursement_id) === String(r.id)); + let n = 0; + for (const item of items) { + n += (await attach('EXPENSE', local.id, item.url, who.id, seen)).n; + } + n += (await attach('EXPENSE', local.id, r.attachment_url, who.id, seen)).n; + if (n) { + stats.expenses += 1; + stats.expenseFiles += n; + } + } + + const projects = await prisma.project.findMany(); + const projectByName = new Map(projects.map((p) => [p.name, p])); + for (const p of raw.project_list || []) { + const local = projectByName.get(str(p.project_name)); + if (!local) continue; + const n = await attach('PROJECT', local.id, p.bzj_attachment, admin.id, seen); + stats.projects += n.n ? 1 : 0; + } + + const purchases = await prisma.purchaseRequest.findMany(); + for (const r of raw.oa_purchasing || []) { + const titleBits = [str(r.contract_no), str(r.contract_name)].filter(Boolean); + const local = purchases.find((p) => titleBits.some((b) => p.title.includes(b))); + if (!local) continue; + const who = userFromOaId(r.create_by) || userFromOaId(r.creator_id) || admin; + const a = await attach('PURCHASE', local.id, r.contract_original_url, who.id, seen); + const b = await attach('PURCHASE_ACCEPT', local.id, r.acceptance_url, who.id, seen); + if (a.n + b.n) stats.purchases += 1; + } + + const fileCount = await prisma.fileAsset.count(); + console.log(JSON.stringify({ ...stats, fileAssetTotal: fileCount }, null, 2)); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/import-prod.ts b/apps/api/prisma/import-prod.ts new file mode 100644 index 0000000..1ed55c3 --- /dev/null +++ b/apps/api/prisma/import-prod.ts @@ -0,0 +1,475 @@ +import { PrismaClient, Prisma } from '@prisma/client'; +import * as fs from 'fs'; +import * as path from 'path'; + +const prisma = new PrismaClient(); +const DUMP = path.resolve('/opt/import/prod/oa_clean.json'); + +type Row = Record; + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +function num(v: unknown) { + if (v == null || v === '') return 0; + const n = Number(v); + return Number.isFinite(n) ? n : 0; +} + +function when(v: unknown) { + const s = str(v); + if (!s || s.startsWith('0000')) return null; + const d = new Date(s.includes('T') ? s : s.replace(' ', 'T')); + return Number.isNaN(d.getTime()) ? null : d; +} + +function clip(v: unknown, n = 200) { + const s = str(v); + return s.length > n ? s.slice(0, n) : s || undefined; +} + +function realPartyName(raw: unknown) { + let name = str(raw).replace(/\s+/g, ' '); + if (!name) return ''; + const alias: Record = { + '73047': '中国人民解放军73047部队保障部', + '中国人民解放军 32272 部队': '中国人民解放军32272部队', + }; + name = alias[name] || name; + if (/^未指定/.test(name)) return ''; + if (/^\d+$/.test(name)) return ''; + if (name.startsWith('/') || name.endsWith('.pdf')) return ''; + if (name.length < 2) return ''; + return name; +} + +function mapProjectType(v: unknown, projectName?: unknown) { + const name = str(projectName); + if (name.includes('三维')) return '三维'; + if (/(视频.{0,12}(制作|摄制|拍摄)|摄制|拍摄|摄影|宣传片|专题片|纪录片|微电影|影视制作)/.test(name)) return '摄制'; + const s = str(v); + if (s.includes('三维')) return '三维'; + if (s.includes('摄制') || s.includes('视频')) return '摄制'; + if (s.includes('物资')) return '物资'; + if (s.includes('集成')) return '集成'; + return '软件'; +} + +function mapBidStatus(row: Row) { + const win = str(row.winning_status); + const st = str(row.project_status); + if (st === '中标' || (st === '' && win.includes('中标'))) return 'WON'; + if (st === '未中' || (st === '' && win.includes('未中'))) return 'LOST'; + if (st === '流标') return 'FAILED'; + if (st === '弃标' || win.includes('弃标')) return 'TERMINATED'; + return 'PENDING'; +} + +function mapContractStatus(v: unknown) { + const s = str(v); + if (s === '已通过') return 'APPROVED'; + if (s === '审批中') return 'PENDING'; + return 'DRAFT'; +} + +function mapProjectStatus(v: unknown) { + const s = str(v); + if (s === '验收通过') return 'ACCEPTED'; + if (s === '已完成') return 'DONE'; + if (s === '待验收') return 'INTERNAL_ACCEPTED'; + if (s === '未开始') return 'NOT_STARTED'; + return 'ACTIVE'; +} + +function mapExpenseStatus(v: unknown) { + const s = str(v); + if (s === '2') return 'APPROVED'; + if (s === '3') return 'REJECTED'; + if (s === '1') return 'PENDING'; + return 'DRAFT'; +} + +function mapLoanStatus(row: Row) { + const st = str(row.approval_status); + const offset = num(row.offset_amount); + if (st === '2') { + if (str(row.offset_status) === '2' || offset > 0) return offset >= num(row.amount) ? 'OFFSET' : 'OPEN'; + return 'OPEN'; + } + if (st === '1') return 'PENDING'; + if (st === '3') return 'REJECTED'; + return 'DRAFT'; +} + +function mapSealStatus(v: unknown) { + const s = str(v); + if (s === '2') return 'APPROVED'; + if (s === '3') return 'REJECTED'; + return 'PENDING'; +} + +function mapItemCategory(itemType: string) { + if (itemType.includes('交通') || itemType.includes('差旅')) return 'travel'; + if (itemType.includes('餐')) return 'meal'; + if (itemType.includes('住宿')) return 'stay'; + if (itemType.includes('办公')) return 'office'; + if (itemType.includes('招待')) return 'entertain'; + return 'other'; +} + +function isBidExpense(itemType: string) { + return itemType.includes('投标') || itemType.includes('制标'); +} + +async function purgeBusiness() { + await prisma.smsSendLog.deleteMany(); + await prisma.operationLog.deleteMany(); + await prisma.paymentRecord.deleteMany(); + await prisma.projectChange.deleteMany(); + await prisma.todoTask.deleteMany(); + await prisma.approvalTask.deleteMany(); + await prisma.workReport.deleteMany(); + await prisma.fileAsset.deleteMany(); + await prisma.expenseClaim.deleteMany(); + await prisma.loanRecord.deleteMany(); + await prisma.bondRecord.deleteMany(); + await prisma.invoiceRecord.deleteMany(); + await prisma.timesheet.deleteMany(); + await prisma.projectAcceptance.deleteMany(); + await prisma.projectBudget.deleteMany(); + await prisma.projectTask.deleteMany(); + await prisma.purchaseRequest.deleteMany(); + await prisma.assetOccupancy.deleteMany(); + await prisma.contractReviewer.deleteMany(); + await prisma.contractActionLog.deleteMany(); + await prisma.paymentMilestone.deleteMany(); + await prisma.sealRequest.deleteMany(); + await prisma.credentialBorrow.deleteMany(); + await prisma.project.deleteMany(); + await prisma.contract.deleteMany(); + await prisma.bidDocVersion.deleteMany(); + await prisma.bidAssignee.deleteMany(); + await prisma.bidActionLog.deleteMany(); + await prisma.bidReviewer.deleteMany(); + await prisma.bidCase.deleteMany(); + await prisma.partyContact.deleteMany(); + await prisma.businessParty.deleteMany(); +} + +async function main() { + const raw = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Record; + const admin = await prisma.user.findFirst({ where: { username: 'admin' } }); + if (!admin) throw new Error('本地没有 admin'); + + await purgeBusiness(); + + await prisma.legalEntity.updateMany({ data: { isDefault: false } }); + let entity = await prisma.legalEntity.findFirst({ + where: { tenantId: '1', name: '江苏风影随行科技有限公司' }, + }); + if (!entity) { + entity = await prisma.legalEntity.create({ + data: { name: '江苏风影随行科技有限公司', shortName: '江苏风影', isDefault: true }, + }); + } else { + entity = await prisma.legalEntity.update({ where: { id: entity.id }, data: { isDefault: true } }); + } + + const tenders = (raw.oa_tender || []).filter((r) => /^TB-\d/.test(str(r.system_number)) && str(r.project_name)); + const partyByName = new Map(); + const partyNames = new Set(); + for (const t of tenders) { + const n = realPartyName(t.tender_dan_wei); + if (n) partyNames.add(n); + } + for (const c of raw.oa_contract || []) { + const n = realPartyName(c.party_a_name); + if (n && n !== '江苏风影随行科技有限公司') partyNames.add(n); + } + for (const name of partyNames) { + const row = await prisma.businessParty.create({ + data: { name, remark: 'OA 导入', level: 'B' }, + }); + partyByName.set(name, row.id); + } + for (const t of tenders) { + const n = realPartyName(t.tender_dan_wei); + const person = str(t.bid_contact_person); + const phone = str(t.bid_contact_phone); + if (!n || (!person && !phone)) continue; + const partyId = partyByName.get(n); + if (!partyId) continue; + const exists = await prisma.partyContact.findFirst({ where: { partyId, name: person || '招标联系人' } }); + if (!exists) { + await prisma.partyContact.create({ + data: { partyId, name: person || '招标联系人', phone: phone || undefined }, + }); + } + } + + const bidByProd = new Map(); + const bidByNo = new Map(); + const bidByName = new Map(); + for (const t of tenders) { + const bidNo = str(t.system_number); + const partyName = realPartyName(t.tender_dan_wei); + const resultBits = [ + t.winning_unit ? `中标单位:${t.winning_unit}` : '', + num(t.winning_amount) ? `中标金额:${num(t.winning_amount)}` : '', + str(t.opening_remark), + str(t.bid_summary), + ].filter(Boolean); + const row = await prisma.bidCase.create({ + data: { + bidNo, + name: clip(t.project_name, 200)!, + externalNo: clip(t.project_number, 120) || undefined, + projectType: mapProjectType(t.project_type, t.project_name), + source: clip(t.project_source, 50) || undefined, + legalEntityId: entity.id, + partyId: partyName ? partyByName.get(partyName) : undefined, + tenderMethod: clip(t.tender_method, 50) || undefined, + applyEndAt: when(t.file_apply_deadline) || undefined, + openAt: when(t.bid_open_date) || undefined, + openPlace: clip(t.tender_address, 200) || undefined, + securityLevel: str(t.security_level) || 'INTERNAL', + projectNature: clip(t.project_nature, 50) || undefined, + contactName: clip(t.bid_contact_person, 64) || undefined, + contactPhone: clip(t.bid_contact_phone, 32) || undefined, + bondMethod: clip(t.deposit_pay_method, 20) || undefined, + bondStatus: clip(t.deposit_status, 20) || undefined, + bondPaidAt: when(t.deposit_pay_date) || undefined, + delivererName: clip(t.bid_delivery_person, 64) || undefined, + priceCap: t.price_limit == null ? undefined : String(t.price_limit), + quoteAmount: t.bid_quotation == null ? undefined : String(t.bid_quotation), + summary: clip(t.project_intro, 2000) || undefined, + remark: clip(t.remark, 1000) || undefined, + resultNote: resultBits.join(';') || undefined, + openedResultAt: when(t.winning_date) || when(t.bid_open_date) || undefined, + status: mapBidStatus(t), + createdById: admin.id, + createdAt: when(t.create_time) || undefined, + }, + }); + bidByProd.set(String(t.id), row.id); + bidByNo.set(bidNo, row.id); + bidByName.set(str(t.project_name), row.id); + const deposit = num(t.bid_deposit); + if (deposit > 0) { + const paid = Boolean(when(t.deposit_pay_date)); + const returned = Boolean(when(t.deposit_refund_date)); + await prisma.bondRecord.create({ + data: { + bondNo: `BZJ-${bidNo}`, + bidCaseId: row.id, + bondType: 'TENDER', + amount: new Prisma.Decimal(deposit), + paidAt: when(t.deposit_pay_date) || undefined, + returnedAt: when(t.deposit_refund_date) || undefined, + status: returned ? 'RETURNED' : paid ? 'PAID' : 'UNPAID', + remark: 'OA 投标保证金', + }, + }); + } + } + + const contractByProd = new Map(); + const contractByNo = new Map(); + const contractByName = new Map(); + for (const c of raw.oa_contract || []) { + const contractNo = str(c.contract_no); + if (!contractNo) continue; + const tb = contractNo.match(/TB-\d+-\d+/); + const bidCaseId = tb ? bidByNo.get(tb[0]) : undefined; + if (str(c.approval_status) === 'pending_submit' && !c.sign_date && !num(c.amount)) { + continue; + } + const partyA = realPartyName(c.party_a_name); + const partyB = str(c.party_b_name); + if (partyA && !partyByName.has(partyA) && partyA !== '江苏风影随行科技有限公司') { + const p = await prisma.businessParty.create({ data: { name: partyA, remark: 'OA 合同甲方', level: 'B' } }); + partyByName.set(partyA, p.id); + } + const remark = [ + str(c.payment_method) ? `付款:${c.payment_method}` : '', + str(c.contract_lb) ? `类别:${c.contract_lb}` : '', + str(c.remark), + ] + .filter(Boolean) + .join(' / '); + const row = await prisma.contract.create({ + data: { + contractNo, + name: clip(c.contract_name, 200)!, + bidCaseId, + legalEntityId: entity.id, + amount: new Prisma.Decimal(num(c.amount)), + status: mapContractStatus(c.approval_status), + partyA: partyA || undefined, + partyB: partyB || undefined, + remark: remark || undefined, + signedAt: when(c.sign_date) || undefined, + createdById: admin.id, + createdAt: when(c.create_time) || undefined, + }, + }); + contractByProd.set(String(c.id), row.id); + contractByNo.set(contractNo, row.id); + contractByName.set(str(c.contract_name), row.id); + } + + const projectByProd = new Map(); + const projectByName = new Map(); + for (const p of raw.project_list || []) { + const projectNo = str(p.system_number) || str(p.project_code); + if (!projectNo || !str(p.project_name)) continue; + const contractId = + (p.contract_id != null ? contractByProd.get(String(p.contract_id)) : undefined) || + (str(p.contract_name) ? contractByName.get(str(p.contract_name)) : undefined); + const row = await prisma.project.create({ + data: { + projectNo, + name: clip(p.project_name, 200)!, + contractId, + status: mapProjectStatus(p.project_status), + createdAt: when(p.create_time) || undefined, + }, + }); + projectByProd.set(String(p.project_id), row.id); + projectByName.set(str(p.project_name), row.id); + const budget = num(p.project_budget); + if (budget > 0) { + await prisma.projectBudget.create({ + data: { projectId: row.id, category: 'PURCHASE', amount: new Prisma.Decimal(budget) }, + }); + } + const bzj = num(p.project_bzj); + if (bzj > 0) { + await prisma.bondRecord.create({ + data: { + bondNo: `BZJ-${projectNo}`, + projectId: row.id, + contractId, + bondType: 'WARRANTY', + amount: new Prisma.Decimal(bzj), + dueBackAt: when(p.bzj_date) || undefined, + status: 'UNPAID', + remark: str(p.bzj_type) ? `OA 项目质保金 ${p.bzj_type}` : 'OA 项目质保金', + }, + }); + } + } + + const itemsByReimb = new Map(); + for (const item of raw.reimbursement_item || []) { + const key = String(item.reimbursement_id); + if (!itemsByReimb.has(key)) itemsByReimb.set(key, []); + itemsByReimb.get(key)!.push(item); + } + + for (const r of raw.reimbursement || []) { + const claimNo = str(r.reimburse_no); + if (!claimNo) continue; + const lines = itemsByReimb.get(String(r.id)) || []; + const main = [...lines].sort((a, b) => num(b.amount) - num(a.amount))[0]; + const itemType = str(main?.item_type); + const cat = mapItemCategory(itemType); + const projectId = r.project_id != null ? projectByProd.get(String(r.project_id)) : projectByName.get(str(r.project_name)); + const bidCaseId = isBidExpense(itemType) + ? bidByName.get(str(r.project_name)) + : !projectId + ? bidByName.get(str(r.project_name)) + : undefined; + const costType = isBidExpense(itemType) || bidCaseId ? 'BID' : projectId ? 'PROJECT' : 'DEPT'; + await prisma.expenseClaim.create({ + data: { + claimNo, + applicantId: admin.id, + costType, + budgetCategory: costType === 'PROJECT' ? (cat === 'office' ? 'PURCHASE' : 'TRAVEL') : undefined, + projectId: costType === 'PROJECT' ? projectId : undefined, + bidCaseId: costType === 'BID' ? bidCaseId : undefined, + expenseCategory: cat, + purpose: clip(r.subject || r.project_name, 200) || undefined, + amount: new Prisma.Decimal(num(r.total_amount)), + status: mapExpenseStatus(r.approval_status), + remark: clip(r.remark, 1000) || undefined, + submittedAt: when(r.created_time) || undefined, + decidedAt: str(r.approval_status) === '2' ? when(r.created_time) || undefined : undefined, + decidedById: str(r.approval_status) === '2' ? admin.id : undefined, + }, + }); + } + + for (const r of raw.borrow_advance || []) { + const loanNo = str(r.advance_no); + if (!loanNo) continue; + const projectId = r.project_id != null ? projectByProd.get(String(r.project_id)) : projectByName.get(str(r.project_name)); + await prisma.loanRecord.create({ + data: { + loanNo, + applicantId: admin.id, + costType: projectId ? 'PROJECT' : 'DEPT', + projectId, + amount: new Prisma.Decimal(num(r.amount)), + offsetAmount: new Prisma.Decimal(num(r.offset_amount)), + purpose: clip(r.reason || r.subject, 200) || 'OA 借款', + status: mapLoanStatus(r), + remark: undefined, + submittedAt: when(r.apply_time) || undefined, + }, + }); + } + + for (const r of raw.oa_seal_apply || []) { + const reason = clip(r.content || r.apply_subject, 500) || '用章'; + const bidHit = tenders.find((t) => str(t.project_name) && reason.includes(str(t.project_name))); + await prisma.sealRequest.create({ + data: { + sealType: clip(r.seal_type, 40) || '公章', + reason, + bidCaseId: bidHit ? bidByProd.get(String(bidHit.id)) : undefined, + applicantId: admin.id, + takeOut: str(r.is_borrow) === '是', + returnAt: when(r.borrow_end) || undefined, + status: mapSealStatus(r.approval_status), + createdAt: when(r.create_time) || undefined, + }, + }); + } + + for (const r of raw.oa_purchasing || []) { + const title = [str(r.contract_no), str(r.contract_name)].filter(Boolean).join(' '); + if (!title) continue; + await prisma.purchaseRequest.create({ + data: { + title: clip(title, 200)!, + amount: new Prisma.Decimal(num(r.amount)), + status: str(r.approval_status) === '已通过' ? 'APPROVED' : str(r.approval_status) === '审批中' ? 'PENDING' : 'DRAFT', + remark: [r.party_a_name, r.payment_method, r.sign_date, r.remark].map(str).filter(Boolean).join(' / ') || undefined, + applicantId: admin.id, + }, + }); + } + + const counts = { + parties: partyByName.size, + bids: tenders.length, + contracts: (raw.oa_contract || []).length, + projects: (raw.project_list || []).length, + expenses: (raw.reimbursement || []).length, + loans: (raw.borrow_advance || []).length, + seals: (raw.oa_seal_apply || []).length, + purchases: (raw.oa_purchasing || []).length, + }; + console.log(JSON.stringify(counts, null, 2)); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/import-salary-from-oa.ts b/apps/api/prisma/import-salary-from-oa.ts new file mode 100644 index 0000000..08a455b --- /dev/null +++ b/apps/api/prisma/import-salary-from-oa.ts @@ -0,0 +1,239 @@ +import { createReadStream } from 'fs'; +import { createGunzip } from 'zlib'; +import { PrismaClient } from '@prisma/client'; +import { parseOaJson } from './oa-json'; + +const prisma = new PrismaClient(); +const DUMP = process.env.OA_DUMP || '/opt/backup/cloud-20260830/oa.sql.gz'; +const HR = '/opt/import/prod/oa_hr.json'; + +function n(v: unknown) { + if (v == null || v === '') return 0; + const x = Number(v); + return Number.isFinite(x) ? x : 0; +} + +function parseMysqlTuples(s: string): unknown[][] { + const rows: unknown[][] = []; + let i = 0; + while (i < s.length) { + while (i < s.length && ' \n\r\t,'.includes(s[i])) i += 1; + if (i >= s.length) break; + if (s[i] !== '(') throw new Error(`expected ( at ${i}`); + i += 1; + const row: unknown[] = []; + while (true) { + while (i < s.length && ' \n\r\t'.includes(s[i])) i += 1; + if (s.startsWith('NULL', i) && (i + 4 === s.length || ',) \n\r\t'.includes(s[i + 4]))) { + row.push(null); + i += 4; + } else if (s[i] === "'" || s[i] === '"') { + const q = s[i]; + i += 1; + let out = ''; + while (i < s.length) { + const ch = s[i]; + if (ch === '\\') { + out += s[i + 1]; + i += 2; + continue; + } + if (ch === q) { + if (s[i + 1] === q) { + out += q; + i += 2; + continue; + } + i += 1; + break; + } + out += ch; + i += 1; + } + row.push(out); + } else { + const start = i; + while (i < s.length && s[i] !== ',' && s[i] !== ')') i += 1; + const raw = s.slice(start, i).trim(); + row.push(raw === '' ? null : /^-?\d+(\.\d+)?$/.test(raw) ? Number(raw) : raw); + } + while (i < s.length && ' \n\r\t'.includes(s[i])) i += 1; + if (s[i] === ',') { + i += 1; + continue; + } + if (s[i] === ')') { + i += 1; + rows.push(row); + break; + } + throw new Error(`bad token at ${i}`); + } + } + return rows; +} + +function insertsFor(dump: string, table: string) { + const rows: unknown[][] = []; + const needle = `INSERT INTO \`${table}\` VALUES `; + let from = 0; + while (true) { + const start = dump.indexOf(needle, from); + if (start < 0) break; + let i = start + needle.length; + while (i < dump.length) { + while (i < dump.length && ' \n\r\t'.includes(dump[i])) i += 1; + if (dump[i] === ';') { + i += 1; + break; + } + if (dump[i] === ',') { + i += 1; + continue; + } + if (dump[i] !== '(') break; + const sliceStart = i; + let depth = 0; + let inStr = false; + let q = ''; + while (i < dump.length) { + const ch = dump[i]; + if (inStr) { + if (ch === '\\') { + i += 2; + continue; + } + if (ch === q) inStr = false; + i += 1; + continue; + } + if (ch === "'" || ch === '"') { + inStr = true; + q = ch; + i += 1; + continue; + } + if (ch === '(') depth += 1; + if (ch === ')') { + depth -= 1; + if (depth === 0) { + i += 1; + break; + } + } + i += 1; + } + rows.push(...parseMysqlTuples(dump.slice(sliceStart, i))); + } + from = i; + } + return rows; +} + +async function readDump() { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + createReadStream(DUMP) + .pipe(createGunzip()) + .on('data', (c: Buffer) => chunks.push(c)) + .on('end', () => resolve(Buffer.concat(chunks).toString('utf8'))) + .on('error', reject); + }); +} + +async function main() { + const dump = await readDump(); + const hr = parseOaJson(HR) as { users?: { id: number; nickName?: string; phone?: string }[] }; + const employees = await prisma.employee.findMany({ select: { id: true, name: true, mobile: true } }); + const byName = new Map(employees.map((e) => [e.name, e])); + const byMobile = new Map(employees.filter((e) => e.mobile).map((e) => [e.mobile as string, e])); + const oaUserToEmp = new Map(); + for (const u of hr.users || []) { + const hit = (u.phone && byMobile.get(String(u.phone))) || (u.nickName && byName.get(u.nickName)); + if (hit) oaUserToEmp.set(Number(u.id), hit.id); + } + + const months = insertsFor(dump, 'oa_salary_month'); + const slips = insertsFor(dump, 'oa_salary_slip'); + const admin = await prisma.user.findFirst({ where: { username: '18115604840' } }); + + await prisma.payrollItem.deleteMany(); + await prisma.payrollRun.deleteMany(); + + const runByYm = new Map(); + for (const row of months) { + const ym = String(row[1] || ''); + if (!/^\d{4}-\d{2}$/.test(ym)) continue; + const confirmed = Number(row[2]) === 1; + const run = await prisma.payrollRun.create({ + data: { + tenantId: '1', + yearMonth: ym, + status: confirmed ? 'CONFIRMED' : 'DRAFT', + createdById: admin?.id, + confirmedAt: confirmed ? (row[7] ? new Date(String(row[7])) : new Date()) : null, + }, + }); + runByYm.set(ym, run.id); + } + + let ok = 0; + let skip = 0; + for (const row of slips) { + const ym = String(row[2] || ''); + const oaUserId = Number(row[3]); + const name = String(row[4] || ''); + let empId = oaUserToEmp.get(oaUserId); + if (!empId && name) empId = byName.get(name)?.id; + if (!empId || !runByYm.has(ym)) { + skip += 1; + continue; + } + await prisma.payrollItem.upsert({ + where: { runId_employeeId: { runId: runByYm.get(ym) as string, employeeId: empId } }, + update: { + basePay: n(row[9]) + n(row[10]), + performancePay: n(row[18] || row[11]), + leaveDeduct: n(row[22]), + sickDeduct: n(row[23]), + socialPersonal: n(row[27]), + tax: n(row[28]), + grossPay: n(row[16]), + netPay: n(row[30]), + scheduledDays: n(row[31]), + prorationFactor: n(row[29]) || 1, + }, + create: { + runId: runByYm.get(ym) as string, + employeeId: empId, + basePay: n(row[9]) + n(row[10]), + performancePay: n(row[18] || row[11]), + leaveDeduct: n(row[22]), + sickDeduct: n(row[23]), + socialPersonal: n(row[27]), + tax: n(row[28]), + grossPay: n(row[16]), + netPay: n(row[30]), + scheduledDays: n(row[31]), + prorationFactor: n(row[29]) || 1, + }, + }); + ok += 1; + } + console.log( + JSON.stringify({ + months: runByYm.size, + slips: slips.length, + imported: ok, + skipped: skip, + }), + ); +} + +main() + .then(async () => prisma.$disconnect()) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/menus.ts b/apps/api/prisma/menus.ts new file mode 100644 index 0000000..3c55d34 --- /dev/null +++ b/apps/api/prisma/menus.ts @@ -0,0 +1,201 @@ +export type MenuNode = { + name: string; + code: string; + path: string; + icon?: string; + children?: MenuNode[]; +}; + +/** 人事业务在「组织人事」;组织/班次/参数进「系统设置 · 人事设置」。 */ +export const MENUS: MenuNode[] = [ + { + name: '工作台', + code: 'office:overview', + path: '/workbench', + icon: 'HomeOutlined', + }, + { + name: '个人办公', + code: 'office', + path: '/office/todos', + icon: 'AppstoreOutlined', + children: [ + { name: '待办事项', code: 'office:todos', path: '/office/todos' }, + { name: '待我审批', code: 'office:approvals', path: '/office/approvals' }, + { name: '我的申请', code: 'office:flow', path: '/office/flow' }, + { name: '我的日程', code: 'office:calendar', path: '/office/calendar' }, + { name: '工作汇报', code: 'office:reports', path: '/office/reports' }, + { name: '我的费用', code: 'office:expenses', path: '/office/expenses' }, + { name: '人事申请', code: 'office:hr-apply', path: '/office/hr-apply' }, + { name: '用章申请', code: 'office:seal-apply', path: '/office/seal-apply' }, + { name: '办公申请', code: 'office:apply', path: '/office/apply' }, + { name: '公司公告', code: 'office:notices', path: '/office/notices' }, + { name: '发送短信', code: 'office:sms', path: '/office/sms' }, + { name: '工作安排', code: 'office:assign', path: '/office/assign' }, + { name: '消息', code: 'office:im', path: '/office/im' }, + { name: '员工通讯', code: 'office:directory', path: '/office/directory' }, + ], + }, + { + name: '往来单位', + code: 'party', + path: '/party/tenders', + icon: 'ClusterOutlined', + children: [ + { name: '招标单位', code: 'party:tenders', path: '/party/tenders' }, + { name: '参与主体', code: 'party:entities', path: '/party/entities' }, + { name: '联系名录', code: 'party:contacts', path: '/party/contacts' }, + ], + }, + { + name: '证照用章', + code: 'seal', + path: '/seal/qualifications', + icon: 'SafetyCertificateOutlined', + children: [ + { name: '资质台账', code: 'seal:qualifications', path: '/seal/qualifications' }, + { name: '证照借用', code: 'seal:borrows', path: '/seal/borrows' }, + { name: '用章登记', code: 'seal:requests', path: '/seal/requests' }, + { name: '印章台账', code: 'seal:registry', path: '/seal/registry' }, + ], + }, + { + name: '投标管理', + code: 'bid', + path: '/bid/screening', + icon: 'AuditOutlined', + children: [ + { name: '项目筛选', code: 'bid:screening', path: '/bid/screening' }, + { name: '特殊筛选', code: 'bid:special-screening', path: '/bid/special-screening' }, + { name: '标书制作', code: 'bid:drafting', path: '/bid/drafting' }, + { name: '特殊制标', code: 'bid:special-drafting', path: '/bid/special-drafting' }, + { name: '投标执行', code: 'bid:execution', path: '/bid/execution' }, + { name: '开标结果', code: 'bid:result', path: '/bid/result' }, + { name: '终止投标', code: 'bid:terminated', path: '/bid/terminated' }, + ], + }, + { + name: '合同管理', + code: 'contract', + path: '/contract/tender', + icon: 'FileProtectOutlined', + children: [ + { name: '投标合同', code: 'contract:approval', path: '/contract/tender' }, + { name: '直签合同', code: 'contract:direct', path: '/contract/direct' }, + { name: '其他合同', code: 'contract:list', path: '/contract/incremental' }, + { name: '人事合同', code: 'contract:hr', path: '/contract/hr' }, + ], + }, + { + name: '资产管理', + code: 'asset', + path: '/asset/digital', + icon: 'DatabaseOutlined', + children: [ + { name: '数字资产', code: 'asset:digital', path: '/asset/digital' }, + { name: '固定资产', code: 'asset:fixed', path: '/asset/fixed' }, + ], + }, + { + name: '项目管理', + code: 'project', + path: '/project/list', + icon: 'ProjectOutlined', + children: [ + { name: '项目台账', code: 'project:list', path: '/project/list' }, + { name: '任务分解', code: 'project:tasks', path: '/project/tasks' }, + { name: '工时填报', code: 'project:timesheets', path: '/project/timesheets' }, + { name: '资源看板', code: 'project:resources', path: '/project/resources' }, + { name: '项目验收', code: 'project:acceptance', path: '/project/acceptance' }, + { name: '采购申请', code: 'project:purchases', path: '/project/purchases' }, + { name: '器材占用', code: 'project:assets', path: '/project/assets' }, + ], + }, + { + name: '财务收支', + code: 'finance', + path: '/finance/expenses', + icon: 'AccountBookOutlined', + children: [ + { name: '费用报销', code: 'finance:expenses', path: '/finance/expenses' }, + { name: '借支记录', code: 'finance:loans', path: '/finance/loans' }, + { name: '资产购置台账', code: 'finance:asset-purchases', path: '/finance/asset-purchases' }, + { name: '投标保证金', code: 'finance:tender-bonds', path: '/finance/bonds/tender' }, + { name: '履约保证金', code: 'finance:perf-bonds', path: '/finance/bonds/performance' }, + { name: '项目质保金', code: 'finance:warranty-bonds', path: '/finance/bonds/warranty' }, + { name: '开票收款', code: 'finance:invoices', path: '/finance/invoices' }, + { name: '付款执行', code: 'finance:payments', path: '/finance/payments' }, + ], + }, + { + name: '组织人事', + code: 'hr', + path: '/hr/employees', + icon: 'TeamOutlined', + children: [ + { name: '在职员工', code: 'hr:employees', path: '/hr/employees' }, + { name: '组织管理', code: 'hr:org', path: '/hr/org' }, + { name: '入转调离', code: 'hr:lifecycle', path: '/hr/lifecycle' }, + { name: '假勤结果', code: 'hr:attendance', path: '/hr/attendance' }, + { name: '薪酬核算', code: 'hr:payroll', path: '/hr/payroll' }, + { name: '绩效发放', code: 'hr:performance', path: '/hr/performance' }, + { name: '发布公告', code: 'hr:notices', path: '/hr/notices' }, + { name: '规章制度', code: 'hr:policies', path: '/hr/policies' }, + ], + }, + { + name: '经营报表', + code: 'report', + path: '/report/conversion', + icon: 'BarChartOutlined', + children: [ + { name: '投标转化', code: 'report:conversion', path: '/report/conversion' }, + { name: '落标对照', code: 'report:lost', path: '/report/lost' }, + { name: '项目利润', code: 'report:profit', path: '/report/profit' }, + { name: '部门人效', code: 'report:efficiency', path: '/report/efficiency' }, + { name: '资金占用', code: 'report:cash', path: '/report/cash' }, + ], + }, + { + name: '系统设置', + code: 'system', + path: '/system/acl', + icon: 'SettingOutlined', + children: [ + { + name: '人事设置', + code: 'system:hr', + path: '/system/hr-params', + children: [ + { name: '员工账号', code: 'system:acl', path: '/system/acl' }, + { name: '人事参数', code: 'system:hr-params', path: '/system/hr-params' }, + { name: '考勤设置', code: 'system:attendance-rules', path: '/system/attendance-rules' }, + ], + }, + { name: '数据字典', code: 'system:dicts', path: '/system/dicts' }, + { name: '权限设置', code: 'system:perms', path: '/system/perms' }, + { name: '编号规则', code: 'system:numbers', path: '/system/numbers' }, + { name: '短信服务', code: 'system:sms', path: '/system/sms' }, + { name: '消息通道', code: 'system:messages', path: '/system/messages' }, + { name: '手机端升级', code: 'system:app-release', path: '/system/app-release' }, + { name: '操作日志', code: 'system:logs', path: '/system/logs' }, + ], + }, +]; + +export function walkMenus(nodes: MenuNode[], visit: (node: MenuNode, parent: MenuNode | null, sortNo: number) => void, parent: MenuNode | null = null) { + nodes.forEach((node, i) => { + visit(node, parent, parent ? i + 1 : (i + 1) * 10); + if (node.children?.length) walkMenus(node.children, visit, node); + }); +} + +export function isHrSettingCode(code: string) { + return ( + code === 'system:acl' || + code === 'system:hr' || + code.startsWith('system:hr') || + code === 'system:org' || + code === 'system:attendance-rules' + ); +} diff --git a/apps/api/prisma/migrations/20260829065602_init/migration.sql b/apps/api/prisma/migrations/20260829065602_init/migration.sql new file mode 100644 index 0000000..628c35d --- /dev/null +++ b/apps/api/prisma/migrations/20260829065602_init/migration.sql @@ -0,0 +1,567 @@ +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "username" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "displayName" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "employeeId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RefreshToken" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "tokenHash" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "revokedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Department" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "parentId" TEXT, + "name" TEXT NOT NULL, + "code" TEXT NOT NULL, + "deptType" TEXT NOT NULL, + "categoryCode" TEXT, + "sortNo" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "Department_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Employee" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "name" TEXT NOT NULL, + "employeeNo" TEXT NOT NULL, + "departmentId" TEXT NOT NULL, + "title" TEXT, + "restSchedule" TEXT NOT NULL DEFAULT 'SINGLE', + "employmentStatus" TEXT NOT NULL DEFAULT 'REGULAR', + "hiredAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Employee_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Role" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "name" TEXT NOT NULL, + "code" TEXT NOT NULL, + "description" TEXT NOT NULL DEFAULT '', + + CONSTRAINT "Role_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Permission" ( + "id" TEXT NOT NULL, + "parentId" TEXT, + "name" TEXT NOT NULL, + "code" TEXT NOT NULL, + "permType" TEXT NOT NULL, + "path" TEXT, + "icon" TEXT, + "sortNo" INTEGER NOT NULL DEFAULT 0, + "visible" BOOLEAN NOT NULL DEFAULT true, + + CONSTRAINT "Permission_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "RolePermission" ( + "roleId" TEXT NOT NULL, + "permissionId" TEXT NOT NULL, + + CONSTRAINT "RolePermission_pkey" PRIMARY KEY ("roleId","permissionId") +); + +-- CreateTable +CREATE TABLE "UserRole" ( + "userId" TEXT NOT NULL, + "roleId" TEXT NOT NULL, + + CONSTRAINT "UserRole_pkey" PRIMARY KEY ("userId","roleId") +); + +-- CreateTable +CREATE TABLE "DictItem" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "dictType" TEXT NOT NULL, + "label" TEXT NOT NULL, + "value" TEXT NOT NULL, + "sortNo" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "DictItem_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LegalEntity" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "name" TEXT NOT NULL, + "shortName" TEXT NOT NULL, + "creditNo" TEXT, + "isDefault" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "LegalEntity_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BusinessParty" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "name" TEXT NOT NULL, + "creditNo" TEXT, + "level" TEXT NOT NULL DEFAULT 'B', + "riskFlag" BOOLEAN NOT NULL DEFAULT false, + "remark" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BusinessParty_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PartyContact" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "partyId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "phone" TEXT, + "title" TEXT, + + CONSTRAINT "PartyContact_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Qualification" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "name" TEXT NOT NULL, + "code" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3), + "keeper" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Qualification_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "SealRequest" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "sealType" TEXT NOT NULL, + "reason" TEXT NOT NULL, + "bidCaseId" TEXT, + "takeOut" BOOLEAN NOT NULL DEFAULT false, + "returnAt" TIMESTAMP(3), + "status" TEXT NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "SealRequest_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CredentialBorrow" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "itemName" TEXT NOT NULL, + "borrower" TEXT NOT NULL, + "dueAt" TIMESTAMP(3), + "returnedAt" TIMESTAMP(3), + "status" TEXT NOT NULL DEFAULT 'BORROWED', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "CredentialBorrow_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BidCase" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "bidNo" TEXT NOT NULL, + "name" TEXT NOT NULL, + "externalNo" TEXT, + "projectType" TEXT NOT NULL, + "sourceType" TEXT NOT NULL DEFAULT 'FLOW', + "source" TEXT, + "qualificationNeed" TEXT, + "legalEntityId" TEXT, + "tenderMethod" TEXT, + "applyStartAt" TIMESTAMP(3), + "applyEndAt" TIMESTAMP(3), + "applyMethod" TEXT, + "openAt" TIMESTAMP(3), + "openMethod" TEXT, + "openPlace" TEXT, + "securityLevel" TEXT NOT NULL DEFAULT 'INTERNAL', + "projectNature" TEXT, + "partyId" TEXT, + "contactName" TEXT, + "contactPhone" TEXT, + "bondMethod" TEXT, + "bondStatus" TEXT, + "bondPaidAt" TIMESTAMP(3), + "delivererName" TEXT, + "priceCap" TEXT, + "quoteAmount" TEXT, + "summary" TEXT, + "remark" TEXT, + "status" TEXT NOT NULL DEFAULT 'TECH_INITIAL', + "nasPath" TEXT, + "createdById" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "BidCase_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BidReviewer" ( + "id" TEXT NOT NULL, + "bidCaseId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "stage" TEXT NOT NULL, + "result" TEXT NOT NULL DEFAULT 'PENDING', + "comment" TEXT, + + CONSTRAINT "BidReviewer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TodoTask" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "title" TEXT NOT NULL, + "bizType" TEXT NOT NULL, + "bizId" TEXT, + "assigneeId" TEXT NOT NULL, + "dueAt" TIMESTAMP(3), + "status" TEXT NOT NULL DEFAULT 'OPEN', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "TodoTask_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ApprovalTask" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "title" TEXT NOT NULL, + "bizType" TEXT NOT NULL, + "bizId" TEXT, + "assigneeId" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ApprovalTask_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "WorkReport" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "authorId" TEXT NOT NULL, + "period" TEXT NOT NULL, + "content" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "WorkReport_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Contract" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "contractNo" TEXT NOT NULL, + "name" TEXT NOT NULL, + "bidCaseId" TEXT, + "amount" DECIMAL(18,2) NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'DRAFT', + "signedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Contract_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PaymentMilestone" ( + "id" TEXT NOT NULL, + "contractId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "ratio" DECIMAL(5,2) NOT NULL, + "dueAt" TIMESTAMP(3), + "trigger" TEXT, + + CONSTRAINT "PaymentMilestone_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Project" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "projectNo" TEXT NOT NULL, + "name" TEXT NOT NULL, + "contractId" TEXT, + "status" TEXT NOT NULL DEFAULT 'ACTIVE', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ProjectTask" ( + "id" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "department" TEXT, + "dueAt" TIMESTAMP(3), + "status" TEXT NOT NULL DEFAULT 'OPEN', + + CONSTRAINT "ProjectTask_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Timesheet" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "projectId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "workDate" TIMESTAMP(3) NOT NULL, + "hours" DECIMAL(6,2) NOT NULL, + "note" TEXT, + + CONSTRAINT "Timesheet_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "ExpenseClaim" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "claimNo" TEXT NOT NULL, + "applicantId" TEXT NOT NULL, + "costType" TEXT NOT NULL, + "bidCaseId" TEXT, + "projectId" TEXT, + "amount" DECIMAL(18,2) NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'DRAFT', + "remark" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ExpenseClaim_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LoanRecord" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "applicantId" TEXT NOT NULL, + "amount" DECIMAL(18,2) NOT NULL, + "purpose" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'OPEN', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "LoanRecord_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "BondRecord" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "bidCaseId" TEXT NOT NULL, + "bondType" TEXT NOT NULL, + "amount" DECIMAL(18,2) NOT NULL, + "paidAt" TIMESTAMP(3), + "dueBackAt" TIMESTAMP(3), + "returnedAt" TIMESTAMP(3), + "status" TEXT NOT NULL DEFAULT 'UNPAID', + + CONSTRAINT "BondRecord_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "InvoiceRecord" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "invoiceNo" TEXT, + "amount" DECIMAL(18,2) NOT NULL, + "status" TEXT NOT NULL DEFAULT 'TO_ISSUE', + "issuedAt" TIMESTAMP(3), + "receivedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "InvoiceRecord_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "PurchaseRequest" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "title" TEXT NOT NULL, + "projectId" TEXT, + "amount" DECIMAL(18,2) NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'DRAFT', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "PurchaseRequest_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "AssetOccupancy" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "assetName" TEXT NOT NULL, + "holder" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'IN_USE', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AssetOccupancy_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OperationLog" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "userId" TEXT, + "action" TEXT NOT NULL, + "path" TEXT NOT NULL, + "requestId" TEXT, + "ip" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "OperationLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_tenantId_username_key" ON "User"("tenantId", "username"); + +-- CreateIndex +CREATE INDEX "RefreshToken_userId_idx" ON "RefreshToken"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Department_tenantId_code_key" ON "Department"("tenantId", "code"); + +-- CreateIndex +CREATE UNIQUE INDEX "Employee_tenantId_employeeNo_key" ON "Employee"("tenantId", "employeeNo"); + +-- CreateIndex +CREATE UNIQUE INDEX "Role_tenantId_code_key" ON "Role"("tenantId", "code"); + +-- CreateIndex +CREATE UNIQUE INDEX "Permission_code_key" ON "Permission"("code"); + +-- CreateIndex +CREATE UNIQUE INDEX "DictItem_tenantId_dictType_value_key" ON "DictItem"("tenantId", "dictType", "value"); + +-- CreateIndex +CREATE UNIQUE INDEX "LegalEntity_tenantId_name_key" ON "LegalEntity"("tenantId", "name"); + +-- CreateIndex +CREATE UNIQUE INDEX "BusinessParty_tenantId_name_key" ON "BusinessParty"("tenantId", "name"); + +-- CreateIndex +CREATE UNIQUE INDEX "BidCase_tenantId_bidNo_key" ON "BidCase"("tenantId", "bidNo"); + +-- CreateIndex +CREATE INDEX "BidReviewer_bidCaseId_idx" ON "BidReviewer"("bidCaseId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Contract_tenantId_contractNo_key" ON "Contract"("tenantId", "contractNo"); + +-- CreateIndex +CREATE UNIQUE INDEX "Project_tenantId_projectNo_key" ON "Project"("tenantId", "projectNo"); + +-- CreateIndex +CREATE UNIQUE INDEX "ExpenseClaim_tenantId_claimNo_key" ON "ExpenseClaim"("tenantId", "claimNo"); + +-- AddForeignKey +ALTER TABLE "User" ADD CONSTRAINT "User_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Department" ADD CONSTRAINT "Department_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Department"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Employee" ADD CONSTRAINT "Employee_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "Department"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Permission" ADD CONSTRAINT "Permission_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Permission"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RolePermission" ADD CONSTRAINT "RolePermission_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "RolePermission" ADD CONSTRAINT "RolePermission_permissionId_fkey" FOREIGN KEY ("permissionId") REFERENCES "Permission"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "UserRole" ADD CONSTRAINT "UserRole_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PartyContact" ADD CONSTRAINT "PartyContact_partyId_fkey" FOREIGN KEY ("partyId") REFERENCES "BusinessParty"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BidCase" ADD CONSTRAINT "BidCase_legalEntityId_fkey" FOREIGN KEY ("legalEntityId") REFERENCES "LegalEntity"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BidCase" ADD CONSTRAINT "BidCase_partyId_fkey" FOREIGN KEY ("partyId") REFERENCES "BusinessParty"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BidCase" ADD CONSTRAINT "BidCase_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BidReviewer" ADD CONSTRAINT "BidReviewer_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TodoTask" ADD CONSTRAINT "TodoTask_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ApprovalTask" ADD CONSTRAINT "ApprovalTask_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Contract" ADD CONSTRAINT "Contract_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "PaymentMilestone" ADD CONSTRAINT "PaymentMilestone_contractId_fkey" FOREIGN KEY ("contractId") REFERENCES "Contract"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_contractId_fkey" FOREIGN KEY ("contractId") REFERENCES "Contract"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ProjectTask" ADD CONSTRAINT "ProjectTask_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Timesheet" ADD CONSTRAINT "Timesheet_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ExpenseClaim" ADD CONSTRAINT "ExpenseClaim_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ExpenseClaim" ADD CONSTRAINT "ExpenseClaim_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BondRecord" ADD CONSTRAINT "BondRecord_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260829071500_bid_workflow_phase1/migration.sql b/apps/api/prisma/migrations/20260829071500_bid_workflow_phase1/migration.sql new file mode 100644 index 0000000..eeaa36e --- /dev/null +++ b/apps/api/prisma/migrations/20260829071500_bid_workflow_phase1/migration.sql @@ -0,0 +1,34 @@ +-- AlterTable +ALTER TABLE "BidCase" ADD COLUMN "fetchedAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "BidReviewer" ADD COLUMN "decidedAt" TIMESTAMP(3); + +-- CreateTable +CREATE TABLE "BidActionLog" ( + "id" TEXT NOT NULL, + "bidCaseId" TEXT NOT NULL, + "actorId" TEXT NOT NULL, + "action" TEXT NOT NULL, + "fromStatus" TEXT, + "toStatus" TEXT, + "comment" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "BidActionLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "BidReviewer_bidCaseId_userId_stage_key" ON "BidReviewer"("bidCaseId", "userId", "stage"); + +-- CreateIndex +CREATE INDEX "BidActionLog_bidCaseId_idx" ON "BidActionLog"("bidCaseId"); + +-- AddForeignKey +ALTER TABLE "BidReviewer" ADD CONSTRAINT "BidReviewer_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BidActionLog" ADD CONSTRAINT "BidActionLog_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "BidActionLog" ADD CONSTRAINT "BidActionLog_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260829073000_bid_draft_to_open/migration.sql b/apps/api/prisma/migrations/20260829073000_bid_draft_to_open/migration.sql new file mode 100644 index 0000000..673fdb3 --- /dev/null +++ b/apps/api/prisma/migrations/20260829073000_bid_draft_to_open/migration.sql @@ -0,0 +1,50 @@ +-- AlterTable +ALTER TABLE "BidCase" ADD COLUMN "reviewCycle" INTEGER NOT NULL DEFAULT 1; +ALTER TABLE "BidCase" ADD COLUMN "printCost" DECIMAL(18,2); +ALTER TABLE "BidCase" ADD COLUMN "printNote" TEXT; +ALTER TABLE "BidCase" ADD COLUMN "printedAt" TIMESTAMP(3); +ALTER TABLE "BidCase" ADD COLUMN "costDisposition" TEXT; +ALTER TABLE "BidCase" ADD COLUMN "resultNote" TEXT; +ALTER TABLE "BidCase" ADD COLUMN "openedResultAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "BidReviewer" ADD COLUMN "cycle" INTEGER NOT NULL DEFAULT 1; + +DROP INDEX IF EXISTS "BidReviewer_bidCaseId_userId_stage_key"; +CREATE UNIQUE INDEX "BidReviewer_bidCaseId_userId_stage_cycle_key" ON "BidReviewer"("bidCaseId", "userId", "stage", "cycle"); + +-- CreateTable +CREATE TABLE "BidAssignee" ( + "id" TEXT NOT NULL, + "bidCaseId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "role" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "BidAssignee_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "BidAssignee_bidCaseId_userId_role_key" ON "BidAssignee"("bidCaseId", "userId", "role"); +CREATE INDEX "BidAssignee_bidCaseId_idx" ON "BidAssignee"("bidCaseId"); + +ALTER TABLE "BidAssignee" ADD CONSTRAINT "BidAssignee_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "BidAssignee" ADD CONSTRAINT "BidAssignee_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- CreateTable +CREATE TABLE "BidDocVersion" ( + "id" TEXT NOT NULL, + "bidCaseId" TEXT NOT NULL, + "versionNo" INTEGER NOT NULL, + "nasPath" TEXT NOT NULL, + "note" TEXT, + "submittedById" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "BidDocVersion_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "BidDocVersion_bidCaseId_versionNo_key" ON "BidDocVersion"("bidCaseId", "versionNo"); +CREATE INDEX "BidDocVersion_bidCaseId_idx" ON "BidDocVersion"("bidCaseId"); + +ALTER TABLE "BidDocVersion" ADD CONSTRAINT "BidDocVersion_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "BidDocVersion" ADD CONSTRAINT "BidDocVersion_submittedById_fkey" FOREIGN KEY ("submittedById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260829080000_contract_and_project/migration.sql b/apps/api/prisma/migrations/20260829080000_contract_and_project/migration.sql new file mode 100644 index 0000000..05e9821 --- /dev/null +++ b/apps/api/prisma/migrations/20260829080000_contract_and_project/migration.sql @@ -0,0 +1,56 @@ +-- AlterTable +ALTER TABLE "Contract" ADD COLUMN "legalEntityId" TEXT; +ALTER TABLE "Contract" ADD COLUMN "nasPath" TEXT; +ALTER TABLE "Contract" ADD COLUMN "remark" TEXT; +ALTER TABLE "Contract" ADD COLUMN "submittedAt" TIMESTAMP(3); +ALTER TABLE "Contract" ADD COLUMN "approvedAt" TIMESTAMP(3); +ALTER TABLE "Contract" ADD COLUMN "createdById" TEXT; + +ALTER TABLE "Contract" ADD CONSTRAINT "Contract_legalEntityId_fkey" FOREIGN KEY ("legalEntityId") REFERENCES "LegalEntity"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "Contract" ADD CONSTRAINT "Contract_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AlterTable +ALTER TABLE "SealRequest" ADD COLUMN "contractId" TEXT; +ALTER TABLE "SealRequest" ADD COLUMN "applicantId" TEXT; + +ALTER TABLE "SealRequest" ADD CONSTRAINT "SealRequest_contractId_fkey" FOREIGN KEY ("contractId") REFERENCES "Contract"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "presalesCost" DECIMAL(18,2) NOT NULL DEFAULT 0; + +-- CreateTable +CREATE TABLE "ContractReviewer" ( + "id" TEXT NOT NULL, + "contractId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "result" TEXT NOT NULL DEFAULT 'PENDING', + "comment" TEXT, + "decidedAt" TIMESTAMP(3), + + CONSTRAINT "ContractReviewer_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "ContractReviewer_contractId_userId_key" ON "ContractReviewer"("contractId", "userId"); +CREATE INDEX "ContractReviewer_contractId_idx" ON "ContractReviewer"("contractId"); + +ALTER TABLE "ContractReviewer" ADD CONSTRAINT "ContractReviewer_contractId_fkey" FOREIGN KEY ("contractId") REFERENCES "Contract"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "ContractReviewer" ADD CONSTRAINT "ContractReviewer_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- CreateTable +CREATE TABLE "ContractActionLog" ( + "id" TEXT NOT NULL, + "contractId" TEXT NOT NULL, + "actorId" TEXT NOT NULL, + "action" TEXT NOT NULL, + "fromStatus" TEXT, + "toStatus" TEXT, + "comment" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ContractActionLog_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX "ContractActionLog_contractId_idx" ON "ContractActionLog"("contractId"); + +ALTER TABLE "ContractActionLog" ADD CONSTRAINT "ContractActionLog_contractId_fkey" FOREIGN KEY ("contractId") REFERENCES "Contract"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "ContractActionLog" ADD CONSTRAINT "ContractActionLog_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260829090000_finance_funds/migration.sql b/apps/api/prisma/migrations/20260829090000_finance_funds/migration.sql new file mode 100644 index 0000000..e6482c9 --- /dev/null +++ b/apps/api/prisma/migrations/20260829090000_finance_funds/migration.sql @@ -0,0 +1,67 @@ +-- ExpenseClaim +ALTER TABLE "ExpenseClaim" ADD COLUMN "loanId" TEXT; +ALTER TABLE "ExpenseClaim" ADD COLUMN "submittedAt" TIMESTAMP(3); +ALTER TABLE "ExpenseClaim" ADD COLUMN "decidedAt" TIMESTAMP(3); +ALTER TABLE "ExpenseClaim" ADD COLUMN "decidedById" TEXT; +ALTER TABLE "ExpenseClaim" ADD COLUMN "rejectComment" TEXT; + +ALTER TABLE "ExpenseClaim" ADD CONSTRAINT "ExpenseClaim_applicantId_fkey" FOREIGN KEY ("applicantId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "ExpenseClaim" ADD CONSTRAINT "ExpenseClaim_decidedById_fkey" FOREIGN KEY ("decidedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "ExpenseClaim" ADD CONSTRAINT "ExpenseClaim_loanId_fkey" FOREIGN KEY ("loanId") REFERENCES "LoanRecord"("id") ON DELETE SET NULL ON UPDATE CASCADE; +CREATE INDEX "ExpenseClaim_applicantId_idx" ON "ExpenseClaim"("applicantId"); +CREATE INDEX "ExpenseClaim_loanId_idx" ON "ExpenseClaim"("loanId"); + +-- LoanRecord +ALTER TABLE "LoanRecord" ADD COLUMN "loanNo" TEXT; +ALTER TABLE "LoanRecord" ADD COLUMN "costType" TEXT; +ALTER TABLE "LoanRecord" ADD COLUMN "bidCaseId" TEXT; +ALTER TABLE "LoanRecord" ADD COLUMN "projectId" TEXT; +ALTER TABLE "LoanRecord" ADD COLUMN "offsetAmount" DECIMAL(18,2) NOT NULL DEFAULT 0; +ALTER TABLE "LoanRecord" ADD COLUMN "remark" TEXT; +ALTER TABLE "LoanRecord" ADD COLUMN "submittedAt" TIMESTAMP(3); +ALTER TABLE "LoanRecord" ADD COLUMN "decidedAt" TIMESTAMP(3); +ALTER TABLE "LoanRecord" ADD COLUMN "decidedById" TEXT; + +UPDATE "LoanRecord" SET "loanNo" = 'JK-LEGACY-' || substr("id", 1, 8) WHERE "loanNo" IS NULL; +UPDATE "LoanRecord" SET "costType" = 'ADMIN' WHERE "costType" IS NULL; + +ALTER TABLE "LoanRecord" ALTER COLUMN "loanNo" SET NOT NULL; +ALTER TABLE "LoanRecord" ALTER COLUMN "costType" SET NOT NULL; +ALTER TABLE "LoanRecord" ALTER COLUMN "status" SET DEFAULT 'DRAFT'; + +CREATE UNIQUE INDEX "LoanRecord_tenantId_loanNo_key" ON "LoanRecord"("tenantId", "loanNo"); +CREATE INDEX "LoanRecord_applicantId_idx" ON "LoanRecord"("applicantId"); +ALTER TABLE "LoanRecord" ADD CONSTRAINT "LoanRecord_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "LoanRecord" ADD CONSTRAINT "LoanRecord_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "LoanRecord" ADD CONSTRAINT "LoanRecord_applicantId_fkey" FOREIGN KEY ("applicantId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "LoanRecord" ADD CONSTRAINT "LoanRecord_decidedById_fkey" FOREIGN KEY ("decidedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- BondRecord +ALTER TABLE "BondRecord" ADD COLUMN "bondNo" TEXT; +ALTER TABLE "BondRecord" ADD COLUMN "projectId" TEXT; +ALTER TABLE "BondRecord" ADD COLUMN "contractId" TEXT; +ALTER TABLE "BondRecord" ADD COLUMN "remark" TEXT; +ALTER TABLE "BondRecord" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +UPDATE "BondRecord" SET "bondNo" = 'BZ-LEGACY-' || substr("id", 1, 8) WHERE "bondNo" IS NULL; +ALTER TABLE "BondRecord" ALTER COLUMN "bondNo" SET NOT NULL; +ALTER TABLE "BondRecord" ALTER COLUMN "bidCaseId" DROP NOT NULL; + +ALTER TABLE "BondRecord" DROP CONSTRAINT "BondRecord_bidCaseId_fkey"; +ALTER TABLE "BondRecord" ADD CONSTRAINT "BondRecord_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "BondRecord" ADD CONSTRAINT "BondRecord_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "BondRecord" ADD CONSTRAINT "BondRecord_contractId_fkey" FOREIGN KEY ("contractId") REFERENCES "Contract"("id") ON DELETE SET NULL ON UPDATE CASCADE; +CREATE UNIQUE INDEX "BondRecord_tenantId_bondNo_key" ON "BondRecord"("tenantId", "bondNo"); + +-- InvoiceRecord +DELETE FROM "InvoiceRecord"; +ALTER TABLE "InvoiceRecord" ADD COLUMN "contractId" TEXT NOT NULL; +ALTER TABLE "InvoiceRecord" ADD COLUMN "milestoneId" TEXT NOT NULL; +ALTER TABLE "InvoiceRecord" ADD COLUMN "dueAt" TIMESTAMP(3); +ALTER TABLE "InvoiceRecord" ADD COLUMN "remark" TEXT; +ALTER TABLE "InvoiceRecord" ADD COLUMN "createdById" TEXT; + +CREATE UNIQUE INDEX "InvoiceRecord_milestoneId_key" ON "InvoiceRecord"("milestoneId"); +ALTER TABLE "InvoiceRecord" ADD CONSTRAINT "InvoiceRecord_contractId_fkey" FOREIGN KEY ("contractId") REFERENCES "Contract"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "InvoiceRecord" ADD CONSTRAINT "InvoiceRecord_milestoneId_fkey" FOREIGN KEY ("milestoneId") REFERENCES "PaymentMilestone"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "InvoiceRecord" ADD CONSTRAINT "InvoiceRecord_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260829100000_project_execution/migration.sql b/apps/api/prisma/migrations/20260829100000_project_execution/migration.sql new file mode 100644 index 0000000..234080d --- /dev/null +++ b/apps/api/prisma/migrations/20260829100000_project_execution/migration.sql @@ -0,0 +1,57 @@ +-- Project hour rate and acceptance timestamps +ALTER TABLE "Project" ADD COLUMN "hourRate" DECIMAL(18,2) NOT NULL DEFAULT 200; +ALTER TABLE "Project" ADD COLUMN "internalAcceptedAt" TIMESTAMP(3); +ALTER TABLE "Project" ADD COLUMN "customerAcceptedAt" TIMESTAMP(3); + +-- ProjectTask WBS +ALTER TABLE "ProjectTask" ADD COLUMN "departmentId" TEXT; +ALTER TABLE "ProjectTask" ADD COLUMN "assigneeId" TEXT; +ALTER TABLE "ProjectTask" ADD COLUMN "sortNo" INTEGER NOT NULL DEFAULT 0; +ALTER TABLE "ProjectTask" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +CREATE INDEX "ProjectTask_projectId_idx" ON "ProjectTask"("projectId"); +ALTER TABLE "ProjectTask" ADD CONSTRAINT "ProjectTask_departmentId_fkey" FOREIGN KEY ("departmentId") REFERENCES "Department"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "ProjectTask" ADD CONSTRAINT "ProjectTask_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- Timesheet +ALTER TABLE "Timesheet" ADD COLUMN "taskId" TEXT; +ALTER TABLE "Timesheet" ADD COLUMN "costAmount" DECIMAL(18,2) NOT NULL DEFAULT 0; +ALTER TABLE "Timesheet" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +CREATE INDEX "Timesheet_projectId_idx" ON "Timesheet"("projectId"); +CREATE INDEX "Timesheet_userId_idx" ON "Timesheet"("userId"); +ALTER TABLE "Timesheet" ADD CONSTRAINT "Timesheet_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "ProjectTask"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "Timesheet" ADD CONSTRAINT "Timesheet_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Expense budget category (travel / purchase) +ALTER TABLE "ExpenseClaim" ADD COLUMN "budgetCategory" TEXT; + +-- ProjectBudget +CREATE TABLE "ProjectBudget" ( + "id" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "category" TEXT NOT NULL, + "amount" DECIMAL(18,2) NOT NULL, + + CONSTRAINT "ProjectBudget_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "ProjectBudget_projectId_category_key" ON "ProjectBudget"("projectId", "category"); +ALTER TABLE "ProjectBudget" ADD CONSTRAINT "ProjectBudget_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- ProjectAcceptance +CREATE TABLE "ProjectAcceptance" ( + "id" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "kind" TEXT NOT NULL, + "result" TEXT NOT NULL, + "comment" TEXT, + "actorId" TEXT NOT NULL, + "decidedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ProjectAcceptance_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX "ProjectAcceptance_projectId_idx" ON "ProjectAcceptance"("projectId"); +ALTER TABLE "ProjectAcceptance" ADD CONSTRAINT "ProjectAcceptance_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "ProjectAcceptance" ADD CONSTRAINT "ProjectAcceptance_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260829110000_seal_office/migration.sql b/apps/api/prisma/migrations/20260829110000_seal_office/migration.sql new file mode 100644 index 0000000..d884b9d --- /dev/null +++ b/apps/api/prisma/migrations/20260829110000_seal_office/migration.sql @@ -0,0 +1,19 @@ +-- SealRequest: actual return and decision +ALTER TABLE "SealRequest" ADD COLUMN "returnedAt" TIMESTAMP(3); +ALTER TABLE "SealRequest" ADD COLUMN "decidedAt" TIMESTAMP(3); +ALTER TABLE "SealRequest" ADD COLUMN "decidedById" TEXT; +ALTER TABLE "SealRequest" ADD COLUMN "rejectComment" TEXT; + +ALTER TABLE "SealRequest" ADD CONSTRAINT "SealRequest_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "SealRequest" ADD CONSTRAINT "SealRequest_applicantId_fkey" FOREIGN KEY ("applicantId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "SealRequest" ADD CONSTRAINT "SealRequest_decidedById_fkey" FOREIGN KEY ("decidedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- CredentialBorrow: link to person, qualification, bid +ALTER TABLE "CredentialBorrow" ADD COLUMN "borrowerId" TEXT; +ALTER TABLE "CredentialBorrow" ADD COLUMN "qualificationId" TEXT; +ALTER TABLE "CredentialBorrow" ADD COLUMN "bidCaseId" TEXT; +ALTER TABLE "CredentialBorrow" ADD COLUMN "remark" TEXT; + +ALTER TABLE "CredentialBorrow" ADD CONSTRAINT "CredentialBorrow_borrowerId_fkey" FOREIGN KEY ("borrowerId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "CredentialBorrow" ADD CONSTRAINT "CredentialBorrow_qualificationId_fkey" FOREIGN KEY ("qualificationId") REFERENCES "Qualification"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "CredentialBorrow" ADD CONSTRAINT "CredentialBorrow_bidCaseId_fkey" FOREIGN KEY ("bidCaseId") REFERENCES "BidCase"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260829120000_hr_org_payroll/migration.sql b/apps/api/prisma/migrations/20260829120000_hr_org_payroll/migration.sql new file mode 100644 index 0000000..074e35e --- /dev/null +++ b/apps/api/prisma/migrations/20260829120000_hr_org_payroll/migration.sql @@ -0,0 +1,134 @@ +-- Employee: salary, reporting line, lifecycle dates, compensatory leave +ALTER TABLE "Employee" ADD COLUMN "internStartAt" TIMESTAMP(3); +ALTER TABLE "Employee" ADD COLUMN "probationEndAt" TIMESTAMP(3); +ALTER TABLE "Employee" ADD COLUMN "leftAt" TIMESTAMP(3); +ALTER TABLE "Employee" ADD COLUMN "managerId" TEXT; +ALTER TABLE "Employee" ADD COLUMN "baseSalary" DECIMAL(18,2) NOT NULL DEFAULT 0; +ALTER TABLE "Employee" ADD COLUMN "perfBase" DECIMAL(18,2) NOT NULL DEFAULT 0; +ALTER TABLE "Employee" ADD COLUMN "socialBase" DECIMAL(18,2) NOT NULL DEFAULT 0; +ALTER TABLE "Employee" ADD COLUMN "compLeaveHours" DECIMAL(10,2) NOT NULL DEFAULT 0; + +CREATE INDEX "Employee_departmentId_idx" ON "Employee"("departmentId"); +CREATE INDEX "Employee_managerId_idx" ON "Employee"("managerId"); + +ALTER TABLE "Employee" ADD CONSTRAINT "Employee_managerId_fkey" FOREIGN KEY ("managerId") REFERENCES "Employee"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +UPDATE "Employee" SET "baseSalary" = 8000, "perfBase" = 0, "socialBase" = 8000 WHERE "employeeNo" = 'E000'; +UPDATE "Employee" SET "baseSalary" = 25000, "perfBase" = 10000, "socialBase" = 20000 WHERE "employeeNo" = 'E001'; +UPDATE "Employee" SET "baseSalary" = 18000, "perfBase" = 8000, "socialBase" = 18000 WHERE "employeeNo" = 'E010'; +UPDATE "Employee" SET "baseSalary" = 9000, "perfBase" = 2500, "socialBase" = 9000 WHERE "employeeNo" = 'E011'; +UPDATE "Employee" SET "baseSalary" = 15000, "perfBase" = 5000, "socialBase" = 15000 WHERE "employeeNo" IN ('E020','E021','E022','E023'); +UPDATE "Employee" SET "baseSalary" = 10000, "perfBase" = 2000, "socialBase" = 10000 WHERE "employeeNo" = 'E030'; +UPDATE "Employee" SET "baseSalary" = 9000, "perfBase" = 2000, "socialBase" = 9000 WHERE "employeeNo" = 'E040'; +UPDATE "Employee" SET "baseSalary" = 10000, "perfBase" = 3000, "socialBase" = 10000 WHERE "employeeNo" = 'E100'; + +UPDATE "Employee" AS e +SET "managerId" = d.id +FROM "Employee" AS d +WHERE e."employeeNo" = 'E100' AND d."employeeNo" = 'E023'; + +-- CreateTable +CREATE TABLE "EmploymentEvent" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "employeeId" TEXT NOT NULL, + "kind" TEXT NOT NULL, + "fromStatus" TEXT, + "toStatus" TEXT, + "fromDeptId" TEXT, + "toDeptId" TEXT, + "effectiveAt" TIMESTAMP(3) NOT NULL, + "comment" TEXT, + "actorId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "EmploymentEvent_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX "EmploymentEvent_employeeId_idx" ON "EmploymentEvent"("employeeId"); +CREATE INDEX "EmploymentEvent_effectiveAt_idx" ON "EmploymentEvent"("effectiveAt"); + +ALTER TABLE "EmploymentEvent" ADD CONSTRAINT "EmploymentEvent_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "EmploymentEvent" ADD CONSTRAINT "EmploymentEvent_fromDeptId_fkey" FOREIGN KEY ("fromDeptId") REFERENCES "Department"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "EmploymentEvent" ADD CONSTRAINT "EmploymentEvent_toDeptId_fkey" FOREIGN KEY ("toDeptId") REFERENCES "Department"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "EmploymentEvent" ADD CONSTRAINT "EmploymentEvent_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- CreateTable +CREATE TABLE "AttendanceMonth" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "employeeId" TEXT NOT NULL, + "yearMonth" TEXT NOT NULL, + "workDays" DECIMAL(6,2) NOT NULL DEFAULT 0, + "leaveDays" DECIMAL(6,2) NOT NULL DEFAULT 0, + "sickDays" DECIMAL(6,2) NOT NULL DEFAULT 0, + "overtimeHours" DECIMAL(6,2) NOT NULL DEFAULT 0, + "compLeaveDays" DECIMAL(6,2) NOT NULL DEFAULT 0, + "remark" TEXT, + "importedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "importedById" TEXT, + + CONSTRAINT "AttendanceMonth_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "AttendanceMonth_employeeId_yearMonth_key" ON "AttendanceMonth"("employeeId", "yearMonth"); +CREATE INDEX "AttendanceMonth_yearMonth_idx" ON "AttendanceMonth"("yearMonth"); + +ALTER TABLE "AttendanceMonth" ADD CONSTRAINT "AttendanceMonth_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "AttendanceMonth" ADD CONSTRAINT "AttendanceMonth_importedById_fkey" FOREIGN KEY ("importedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- CreateTable +CREATE TABLE "PayrollRun" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "yearMonth" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'DRAFT', + "createdById" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "confirmedAt" TIMESTAMP(3), + + CONSTRAINT "PayrollRun_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "PayrollRun_tenantId_yearMonth_key" ON "PayrollRun"("tenantId", "yearMonth"); + +ALTER TABLE "PayrollRun" ADD CONSTRAINT "PayrollRun_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- CreateTable +CREATE TABLE "PayrollItem" ( + "id" TEXT NOT NULL, + "runId" TEXT NOT NULL, + "employeeId" TEXT NOT NULL, + "prorationFactor" DECIMAL(8,6) NOT NULL DEFAULT 1, + "scheduledDays" DECIMAL(6,2) NOT NULL DEFAULT 0, + "dailyRate" DECIMAL(18,2) NOT NULL DEFAULT 0, + "basePay" DECIMAL(18,2) NOT NULL DEFAULT 0, + "performancePay" DECIMAL(18,2) NOT NULL DEFAULT 0, + "performanceRatio" DECIMAL(5,2) NOT NULL DEFAULT 100, + "leaveDeduct" DECIMAL(18,2) NOT NULL DEFAULT 0, + "sickDeduct" DECIMAL(18,2) NOT NULL DEFAULT 0, + "socialPersonal" DECIMAL(18,2) NOT NULL DEFAULT 0, + "housingFund" DECIMAL(18,2) NOT NULL DEFAULT 0, + "tax" DECIMAL(18,2) NOT NULL DEFAULT 0, + "grossPay" DECIMAL(18,2) NOT NULL DEFAULT 0, + "netPay" DECIMAL(18,2) NOT NULL DEFAULT 0, + "overtimeHours" DECIMAL(6,2) NOT NULL DEFAULT 0, + "compLeaveUsedDays" DECIMAL(6,2) NOT NULL DEFAULT 0, + "compLeaveHours" DECIMAL(10,2) NOT NULL DEFAULT 0, + + CONSTRAINT "PayrollItem_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "PayrollItem_runId_employeeId_key" ON "PayrollItem"("runId", "employeeId"); + +ALTER TABLE "PayrollItem" ADD CONSTRAINT "PayrollItem_runId_fkey" FOREIGN KEY ("runId") REFERENCES "PayrollRun"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "PayrollItem" ADD CONSTRAINT "PayrollItem_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Directors only see 绩效发放 under 组织人事, not the full HR module +INSERT INTO "RolePermission" ("roleId", "permissionId") +SELECT r.id, p.id +FROM "Role" r +CROSS JOIN "Permission" p +WHERE r.code IN ('tech_director', 'biz_director') + AND p.code = 'hr:performance' +ON CONFLICT DO NOTHING; diff --git a/apps/api/prisma/migrations/20260829130000_reports_and_ops/migration.sql b/apps/api/prisma/migrations/20260829130000_reports_and_ops/migration.sql new file mode 100644 index 0000000..36cb533 --- /dev/null +++ b/apps/api/prisma/migrations/20260829130000_reports_and_ops/migration.sql @@ -0,0 +1,98 @@ +ALTER TABLE "Role" ADD COLUMN "dataScope" TEXT NOT NULL DEFAULT 'ALL'; + +ALTER TABLE "WorkReport" ADD COLUMN "periodType" TEXT NOT NULL DEFAULT 'DAY'; +ALTER TABLE "WorkReport" ADD COLUMN "periodStart" TIMESTAMP(3); +ALTER TABLE "WorkReport" ADD COLUMN "hours" DECIMAL(8,2) NOT NULL DEFAULT 0; + +DELETE FROM "WorkReport" WHERE "authorId" IS NOT NULL AND "authorId" NOT IN (SELECT "id" FROM "User"); +ALTER TABLE "WorkReport" ADD CONSTRAINT "WorkReport_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +CREATE INDEX "WorkReport_authorId_idx" ON "WorkReport"("authorId"); + +ALTER TABLE "PurchaseRequest" ADD COLUMN "remark" TEXT; +ALTER TABLE "PurchaseRequest" ADD COLUMN "applicantId" TEXT; +ALTER TABLE "PurchaseRequest" ADD COLUMN "decidedById" TEXT; +ALTER TABLE "PurchaseRequest" ADD COLUMN "decidedAt" TIMESTAMP(3); +UPDATE "PurchaseRequest" SET "projectId" = NULL WHERE "projectId" IS NOT NULL AND "projectId" NOT IN (SELECT "id" FROM "Project"); +ALTER TABLE "PurchaseRequest" ADD CONSTRAINT "PurchaseRequest_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "PurchaseRequest" ADD CONSTRAINT "PurchaseRequest_applicantId_fkey" FOREIGN KEY ("applicantId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "PurchaseRequest" ADD CONSTRAINT "PurchaseRequest_decidedById_fkey" FOREIGN KEY ("decidedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "AssetOccupancy" ADD COLUMN "holderId" TEXT; +ALTER TABLE "AssetOccupancy" ADD COLUMN "projectId" TEXT; +ALTER TABLE "AssetOccupancy" ADD COLUMN "dueAt" TIMESTAMP(3); +ALTER TABLE "AssetOccupancy" ADD COLUMN "returnedAt" TIMESTAMP(3); +UPDATE "AssetOccupancy" SET "projectId" = NULL WHERE "projectId" IS NOT NULL AND "projectId" NOT IN (SELECT "id" FROM "Project"); +ALTER TABLE "AssetOccupancy" ADD CONSTRAINT "AssetOccupancy_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "AssetOccupancy" ADD CONSTRAINT "AssetOccupancy_holderId_fkey" FOREIGN KEY ("holderId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +ALTER TABLE "OperationLog" ADD COLUMN "username" TEXT; +ALTER TABLE "OperationLog" ADD COLUMN "detail" TEXT; +ALTER TABLE "OperationLog" ADD COLUMN "success" BOOLEAN NOT NULL DEFAULT true; +UPDATE "OperationLog" SET "userId" = NULL WHERE "userId" IS NOT NULL AND "userId" NOT IN (SELECT "id" FROM "User"); +ALTER TABLE "OperationLog" ADD CONSTRAINT "OperationLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; +CREATE INDEX "OperationLog_createdAt_idx" ON "OperationLog"("createdAt"); +CREATE INDEX "OperationLog_action_idx" ON "OperationLog"("action"); + +CREATE TABLE "NumberRule" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "code" TEXT NOT NULL, + "name" TEXT NOT NULL, + "prefix" TEXT NOT NULL, + "padLength" INTEGER NOT NULL DEFAULT 5, + CONSTRAINT "NumberRule_pkey" PRIMARY KEY ("id") +); +CREATE UNIQUE INDEX "NumberRule_tenantId_code_key" ON "NumberRule"("tenantId", "code"); + +INSERT INTO "NumberRule" ("id", "code", "name", "prefix", "padLength") VALUES + ('nr-bid', 'BID', '投标编号', 'BID', 5), + ('nr-ht', 'HT', '合同编号', 'HT', 5), + ('nr-prj', 'PRJ', '项目编号', 'PRJ', 5); + +CREATE TABLE "SealItem" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "name" TEXT NOT NULL, + "keeper" TEXT NOT NULL DEFAULT '行政', + "status" TEXT NOT NULL DEFAULT 'IN_STOCK', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "SealItem_pkey" PRIMARY KEY ("id") +); + +INSERT INTO "SealItem" ("id", "name", "keeper", "status") VALUES + ('seal-official', '公章', '行政', 'IN_STOCK'), + ('seal-legal', '法人章', '行政', 'IN_STOCK'); + +CREATE TABLE "ProjectChange" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "projectId" TEXT NOT NULL, + "summary" TEXT NOT NULL, + "amountDelta" DECIMAL(18,2) NOT NULL DEFAULT 0, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "comment" TEXT, + "actorId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "decidedAt" TIMESTAMP(3), + CONSTRAINT "ProjectChange_pkey" PRIMARY KEY ("id") +); +CREATE INDEX "ProjectChange_projectId_idx" ON "ProjectChange"("projectId"); +ALTER TABLE "ProjectChange" ADD CONSTRAINT "ProjectChange_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; +ALTER TABLE "ProjectChange" ADD CONSTRAINT "ProjectChange_actorId_fkey" FOREIGN KEY ("actorId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +CREATE TABLE "PaymentRecord" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "payNo" TEXT NOT NULL, + "kind" TEXT NOT NULL, + "refId" TEXT, + "amount" DECIMAL(18,2) NOT NULL, + "status" TEXT NOT NULL DEFAULT 'TODO', + "remark" TEXT, + "paidAt" TIMESTAMP(3), + "createdById" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "PaymentRecord_pkey" PRIMARY KEY ("id") +); +CREATE UNIQUE INDEX "PaymentRecord_tenantId_payNo_key" ON "PaymentRecord"("tenantId", "payNo"); +ALTER TABLE "PaymentRecord" ADD CONSTRAINT "PaymentRecord_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260829140000_work_report_flow/migration.sql b/apps/api/prisma/migrations/20260829140000_work_report_flow/migration.sql new file mode 100644 index 0000000..54acbb0 --- /dev/null +++ b/apps/api/prisma/migrations/20260829140000_work_report_flow/migration.sql @@ -0,0 +1,13 @@ +ALTER TABLE "WorkReport" ADD COLUMN "toUserId" TEXT; +ALTER TABLE "WorkReport" ADD COLUMN "ccUserId" TEXT; +ALTER TABLE "WorkReport" ADD COLUMN "status" TEXT NOT NULL DEFAULT 'DRAFT'; +ALTER TABLE "WorkReport" ADD COLUMN "submittedAt" TIMESTAMP(3); +ALTER TABLE "WorkReport" ALTER COLUMN "content" SET DEFAULT ''; + +UPDATE "WorkReport" SET "toUserId" = NULL WHERE "toUserId" IS NOT NULL AND "toUserId" NOT IN (SELECT "id" FROM "User"); +UPDATE "WorkReport" SET "ccUserId" = NULL WHERE "ccUserId" IS NOT NULL AND "ccUserId" NOT IN (SELECT "id" FROM "User"); + +ALTER TABLE "WorkReport" ADD CONSTRAINT "WorkReport_toUserId_fkey" FOREIGN KEY ("toUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "WorkReport" ADD CONSTRAINT "WorkReport_ccUserId_fkey" FOREIGN KEY ("ccUserId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; +CREATE INDEX "WorkReport_toUserId_idx" ON "WorkReport"("toUserId"); +CREATE INDEX "WorkReport_ccUserId_idx" ON "WorkReport"("ccUserId"); diff --git a/apps/api/prisma/migrations/20260829150000_party_expense_forms/migration.sql b/apps/api/prisma/migrations/20260829150000_party_expense_forms/migration.sql new file mode 100644 index 0000000..650d3fc --- /dev/null +++ b/apps/api/prisma/migrations/20260829150000_party_expense_forms/migration.sql @@ -0,0 +1,29 @@ +ALTER TABLE "ExpenseClaim" ADD COLUMN IF NOT EXISTS "expenseCategory" TEXT; + +CREATE TABLE IF NOT EXISTS "FileAsset" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "bizType" TEXT NOT NULL, + "bizId" TEXT NOT NULL, + "fileName" TEXT NOT NULL, + "mimeType" TEXT NOT NULL, + "size" INTEGER NOT NULL, + "storageKey" TEXT NOT NULL, + "uploadedById" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "FileAsset_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX IF NOT EXISTS "FileAsset_bizType_bizId_idx" ON "FileAsset"("bizType", "bizId"); +CREATE INDEX IF NOT EXISTS "FileAsset_uploadedById_idx" ON "FileAsset"("uploadedById"); + +INSERT INTO "DictItem" ("id", "tenantId", "dictType", "label", "value", "sortNo") +VALUES + ('e1c10000-0000-4000-8000-000000000001', '1', 'expenseCategory', '交通', '交通', 1), + ('e1c10000-0000-4000-8000-000000000002', '1', 'expenseCategory', '餐饮', '餐饮', 2), + ('e1c10000-0000-4000-8000-000000000003', '1', 'expenseCategory', '住宿', '住宿', 3), + ('e1c10000-0000-4000-8000-000000000004', '1', 'expenseCategory', '办公', '办公', 4), + ('e1c10000-0000-4000-8000-000000000005', '1', 'expenseCategory', '招待', '招待', 5), + ('e1c10000-0000-4000-8000-000000000006', '1', 'expenseCategory', '其他', '其他', 6) +ON CONFLICT ("tenantId", "dictType", "value") DO NOTHING; diff --git a/apps/api/prisma/migrations/20260829170000_sms_send_log/migration.sql b/apps/api/prisma/migrations/20260829170000_sms_send_log/migration.sql new file mode 100644 index 0000000..f6930c9 --- /dev/null +++ b/apps/api/prisma/migrations/20260829170000_sms_send_log/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "SmsSendLog" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "phones" TEXT NOT NULL, + "signName" TEXT NOT NULL, + "templateCode" TEXT NOT NULL, + "templateParam" TEXT, + "bizId" TEXT, + "code" TEXT NOT NULL, + "message" TEXT, + "createdById" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "SmsSendLog_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "SmsSendLog_createdAt_idx" ON "SmsSendLog"("createdAt"); diff --git a/apps/api/prisma/migrations/20260829180000_hr_archive_settings/migration.sql b/apps/api/prisma/migrations/20260829180000_hr_archive_settings/migration.sql new file mode 100644 index 0000000..82a9e0b --- /dev/null +++ b/apps/api/prisma/migrations/20260829180000_hr_archive_settings/migration.sql @@ -0,0 +1,67 @@ +-- Employee archive fields aligned with personnel files +ALTER TABLE "Employee" ADD COLUMN "gender" TEXT; +ALTER TABLE "Employee" ADD COLUMN "email" TEXT; +ALTER TABLE "Employee" ADD COLUMN "mobile" TEXT; +ALTER TABLE "Employee" ADD COLUMN "idNo" TEXT; +ALTER TABLE "Employee" ADD COLUMN "education" TEXT; +ALTER TABLE "Employee" ADD COLUMN "bankName" TEXT; +ALTER TABLE "Employee" ADD COLUMN "bankAccount" TEXT; +ALTER TABLE "Employee" ADD COLUMN "remark" TEXT; + +CREATE TABLE "HrSetting" ( + "id" TEXT NOT NULL DEFAULT 'default', + "tenantId" TEXT NOT NULL DEFAULT '1', + "idMask" BOOLEAN NOT NULL DEFAULT true, + "autoAccount" BOOLEAN NOT NULL DEFAULT false, + "probationMonths" INTEGER NOT NULL DEFAULT 3, + "defaultRest" TEXT NOT NULL DEFAULT 'SINGLE', + "workStart" TEXT NOT NULL DEFAULT '09:00', + "workEnd" TEXT NOT NULL DEFAULT '18:00', + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "HrSetting_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "HrSetting_tenantId_key" ON "HrSetting"("tenantId"); + +INSERT INTO "HrSetting" ("id", "tenantId", "idMask", "autoAccount", "probationMonths", "defaultRest", "workStart", "workEnd", "updatedAt") +VALUES ('default', '1', true, false, 3, 'SINGLE', '09:00', '18:00', CURRENT_TIMESTAMP); + +CREATE TABLE "AttendanceRule" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "name" TEXT NOT NULL, + "restSchedule" TEXT NOT NULL, + "workStart" TEXT NOT NULL, + "workEnd" TEXT NOT NULL, + "lateGraceMinutes" INTEGER NOT NULL DEFAULT 0, + "enabled" BOOLEAN NOT NULL DEFAULT true, + "sortNo" INTEGER NOT NULL DEFAULT 0, + "remark" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "AttendanceRule_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX "AttendanceRule_tenantId_name_key" ON "AttendanceRule"("tenantId", "name"); + +INSERT INTO "AttendanceRule" ("id", "tenantId", "name", "restSchedule", "workStart", "workEnd", "lateGraceMinutes", "enabled", "sortNo", "remark", "createdAt", "updatedAt") +VALUES + ('att-rule-single-000000000001', '1', '单休班次', 'SINGLE', '09:00', '18:00', 0, true, 1, '每周休一天,薪酬按 26.09 天核算', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP), + ('att-rule-double-000000000001', '1', '双休班次', 'DOUBLE', '09:00', '18:00', 0, true, 2, '每周休两天,薪酬按 21.75 天核算', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP); + +INSERT INTO "DictItem" ("id", "tenantId", "dictType", "label", "value", "sortNo") +SELECT d."id", '1', d."dictType", d."label", d."value", d."sortNo" +FROM (VALUES + ('dict-gender-male-00000000001', 'gender', '男', 'MALE', 1), + ('dict-gender-female-000000001', 'gender', '女', 'FEMALE', 2), + ('dict-edu-high-0000000000001', 'education', '高中及以下', 'HIGH_SCHOOL', 1), + ('dict-edu-college-00000000001', 'education', '专科', 'COLLEGE', 2), + ('dict-edu-bachelor-0000000001', 'education', '本科', 'BACHELOR', 3), + ('dict-edu-master-000000000001', 'education', '硕士', 'MASTER', 4), + ('dict-edu-doctor-000000000001', 'education', '博士', 'DOCTOR', 5) +) AS d("id", "dictType", "label", "value", "sortNo") +WHERE NOT EXISTS ( + SELECT 1 FROM "DictItem" x WHERE x."tenantId" = '1' AND x."dictType" = d."dictType" AND x."value" = d."value" +); diff --git a/apps/api/prisma/migrations/20260830140000_contract_parties/migration.sql b/apps/api/prisma/migrations/20260830140000_contract_parties/migration.sql new file mode 100644 index 0000000..2b2c5d8 --- /dev/null +++ b/apps/api/prisma/migrations/20260830140000_contract_parties/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Contract" ADD COLUMN "partyA" TEXT; +ALTER TABLE "Contract" ADD COLUMN "partyB" TEXT; diff --git a/apps/api/prisma/migrations/20260830150000_leave_and_punch/migration.sql b/apps/api/prisma/migrations/20260830150000_leave_and_punch/migration.sql new file mode 100644 index 0000000..3043e27 --- /dev/null +++ b/apps/api/prisma/migrations/20260830150000_leave_and_punch/migration.sql @@ -0,0 +1,48 @@ +-- CreateTable +CREATE TABLE "AttendancePunch" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "employeeId" TEXT NOT NULL, + "kind" TEXT NOT NULL, + "punchedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "remark" TEXT, + + CONSTRAINT "AttendancePunch_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "LeaveRequest" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "employeeId" TEXT NOT NULL, + "kind" TEXT NOT NULL, + "startAt" TIMESTAMP(3) NOT NULL, + "endAt" TIMESTAMP(3) NOT NULL, + "days" DECIMAL(6,2) NOT NULL, + "reason" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "comment" TEXT, + "decidedById" TEXT, + "decidedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "LeaveRequest_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "AttendancePunch_employeeId_punchedAt_idx" ON "AttendancePunch"("employeeId", "punchedAt"); + +-- CreateIndex +CREATE INDEX "LeaveRequest_employeeId_idx" ON "LeaveRequest"("employeeId"); + +-- CreateIndex +CREATE INDEX "LeaveRequest_status_idx" ON "LeaveRequest"("status"); + +-- AddForeignKey +ALTER TABLE "AttendancePunch" ADD CONSTRAINT "AttendancePunch_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LeaveRequest" ADD CONSTRAINT "LeaveRequest_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LeaveRequest" ADD CONSTRAINT "LeaveRequest_decidedById_fkey" FOREIGN KEY ("decidedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260830170000_labor_contracts/migration.sql b/apps/api/prisma/migrations/20260830170000_labor_contracts/migration.sql new file mode 100644 index 0000000..455e4f0 --- /dev/null +++ b/apps/api/prisma/migrations/20260830170000_labor_contracts/migration.sql @@ -0,0 +1,27 @@ +-- CreateTable +CREATE TABLE "LaborContract" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "employeeId" TEXT NOT NULL, + "contractNo" TEXT, + "startAt" TIMESTAMP(3), + "endAt" TIMESTAMP(3), + "remark" TEXT, + "createdById" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "LaborContract_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE INDEX "LaborContract_employeeId_idx" ON "LaborContract"("employeeId"); + +-- CreateIndex +CREATE INDEX "LaborContract_tenantId_createdAt_idx" ON "LaborContract"("tenantId", "createdAt"); + +-- AddForeignKey +ALTER TABLE "LaborContract" ADD CONSTRAINT "LaborContract_employeeId_fkey" FOREIGN KEY ("employeeId") REFERENCES "Employee"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "LaborContract" ADD CONSTRAINT "LaborContract_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260830180000_onboard_assets_assign/migration.sql b/apps/api/prisma/migrations/20260830180000_onboard_assets_assign/migration.sql new file mode 100644 index 0000000..4d6e41c --- /dev/null +++ b/apps/api/prisma/migrations/20260830180000_onboard_assets_assign/migration.sql @@ -0,0 +1,70 @@ +-- AlterTable +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "mustChangePassword" BOOLEAN NOT NULL DEFAULT true; +ALTER TABLE "User" ADD COLUMN IF NOT EXISTS "profileCompletedAt" TIMESTAMP(3); + +-- AlterTable +ALTER TABLE "Contract" ADD COLUMN IF NOT EXISTS "kind" TEXT NOT NULL DEFAULT 'OTHER'; +UPDATE "Contract" SET "kind" = 'TENDER' WHERE "bidCaseId" IS NOT NULL; + +-- AlterTable +ALTER TABLE "BidCase" ADD COLUMN IF NOT EXISTS "needSurvey" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "SealItem" ADD COLUMN IF NOT EXISTS "sealType" TEXT NOT NULL DEFAULT '公章'; +ALTER TABLE "SealItem" ADD COLUMN IF NOT EXISTS "legalEntityId" TEXT; + +-- CreateTable +CREATE TABLE IF NOT EXISTS "WorkAssignment" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "title" TEXT NOT NULL, + "content" TEXT NOT NULL DEFAULT '', + "assigneeId" TEXT NOT NULL, + "dueAt" TIMESTAMP(3), + "status" TEXT NOT NULL DEFAULT 'OPEN', + "createdById" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "WorkAssignment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE IF NOT EXISTS "RegisteredAsset" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "kind" TEXT NOT NULL, + "name" TEXT NOT NULL, + "code" TEXT, + "spec" TEXT, + "keeper" TEXT, + "location" TEXT, + "amount" DECIMAL(18,2), + "remark" TEXT, + "legalEntityId" TEXT, + "createdById" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "RegisteredAsset_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX IF NOT EXISTS "WorkAssignment_assigneeId_idx" ON "WorkAssignment"("assigneeId"); +CREATE INDEX IF NOT EXISTS "WorkAssignment_createdById_idx" ON "WorkAssignment"("createdById"); +CREATE INDEX IF NOT EXISTS "RegisteredAsset_kind_idx" ON "RegisteredAsset"("kind"); +CREATE INDEX IF NOT EXISTS "RegisteredAsset_legalEntityId_idx" ON "RegisteredAsset"("legalEntityId"); + +ALTER TABLE "WorkAssignment" DROP CONSTRAINT IF EXISTS "WorkAssignment_assigneeId_fkey"; +ALTER TABLE "WorkAssignment" ADD CONSTRAINT "WorkAssignment_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "WorkAssignment" DROP CONSTRAINT IF EXISTS "WorkAssignment_createdById_fkey"; +ALTER TABLE "WorkAssignment" ADD CONSTRAINT "WorkAssignment_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "RegisteredAsset" DROP CONSTRAINT IF EXISTS "RegisteredAsset_legalEntityId_fkey"; +ALTER TABLE "RegisteredAsset" ADD CONSTRAINT "RegisteredAsset_legalEntityId_fkey" FOREIGN KEY ("legalEntityId") REFERENCES "LegalEntity"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "RegisteredAsset" DROP CONSTRAINT IF EXISTS "RegisteredAsset_createdById_fkey"; +ALTER TABLE "RegisteredAsset" ADD CONSTRAINT "RegisteredAsset_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "SealItem" DROP CONSTRAINT IF EXISTS "SealItem_legalEntityId_fkey"; +ALTER TABLE "SealItem" ADD CONSTRAINT "SealItem_legalEntityId_fkey" FOREIGN KEY ("legalEntityId") REFERENCES "LegalEntity"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- 系统管理员不强制改密/补档 +UPDATE "User" u SET "mustChangePassword" = false, "profileCompletedAt" = CURRENT_TIMESTAMP +WHERE EXISTS (SELECT 1 FROM "UserRole" ur JOIN "Role" r ON r.id = ur."roleId" WHERE ur."userId" = u.id AND r.code = 'admin'); diff --git a/apps/api/prisma/migrations/20260830190000_project_party_budget/migration.sql b/apps/api/prisma/migrations/20260830190000_project_party_budget/migration.sql new file mode 100644 index 0000000..333867d --- /dev/null +++ b/apps/api/prisma/migrations/20260830190000_project_party_budget/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "Project" ADD COLUMN IF NOT EXISTS "partyName" TEXT; diff --git a/apps/api/prisma/migrations/20260830200000_project_accept_materials/migration.sql b/apps/api/prisma/migrations/20260830200000_project_accept_materials/migration.sql new file mode 100644 index 0000000..09ef78c --- /dev/null +++ b/apps/api/prisma/migrations/20260830200000_project_accept_materials/migration.sql @@ -0,0 +1,16 @@ +CREATE TABLE IF NOT EXISTS "ProjectAcceptMaterial" ( + "id" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + "name" TEXT NOT NULL, + "remark" TEXT, + "required" BOOLEAN NOT NULL DEFAULT true, + "sortNo" INTEGER NOT NULL DEFAULT 0, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "ProjectAcceptMaterial_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX IF NOT EXISTS "ProjectAcceptMaterial_projectId_idx" ON "ProjectAcceptMaterial"("projectId"); + +ALTER TABLE "ProjectAcceptMaterial" DROP CONSTRAINT IF EXISTS "ProjectAcceptMaterial_projectId_fkey"; +ALTER TABLE "ProjectAcceptMaterial" ADD CONSTRAINT "ProjectAcceptMaterial_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260830210000_office_apply_asset_link/migration.sql b/apps/api/prisma/migrations/20260830210000_office_apply_asset_link/migration.sql new file mode 100644 index 0000000..2840ba9 --- /dev/null +++ b/apps/api/prisma/migrations/20260830210000_office_apply_asset_link/migration.sql @@ -0,0 +1,51 @@ +-- AlterTable +ALTER TABLE "AssetOccupancy" ADD COLUMN IF NOT EXISTS "registeredAssetId" TEXT; + +CREATE INDEX IF NOT EXISTS "AssetOccupancy_registeredAssetId_idx" ON "AssetOccupancy"("registeredAssetId"); + +ALTER TABLE "AssetOccupancy" DROP CONSTRAINT IF EXISTS "AssetOccupancy_registeredAssetId_fkey"; +ALTER TABLE "AssetOccupancy" ADD CONSTRAINT "AssetOccupancy_registeredAssetId_fkey" FOREIGN KEY ("registeredAssetId") REFERENCES "RegisteredAsset"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- CreateTable +CREATE TABLE IF NOT EXISTS "OfficeApply" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "kind" TEXT NOT NULL, + "title" TEXT NOT NULL, + "reason" TEXT, + "status" TEXT NOT NULL DEFAULT 'PENDING', + "assetKind" TEXT, + "registeredAssetId" TEXT, + "amount" DECIMAL(18,2), + "destination" TEXT, + "startAt" TIMESTAMP(3), + "endAt" TIMESTAMP(3), + "projectId" TEXT, + "purchaseId" TEXT, + "occupancyId" TEXT, + "applicantId" TEXT NOT NULL, + "decidedById" TEXT, + "decidedAt" TIMESTAMP(3), + "remark" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "OfficeApply_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX IF NOT EXISTS "OfficeApply_applicantId_idx" ON "OfficeApply"("applicantId"); +CREATE INDEX IF NOT EXISTS "OfficeApply_status_idx" ON "OfficeApply"("status"); +CREATE INDEX IF NOT EXISTS "OfficeApply_kind_idx" ON "OfficeApply"("kind"); + +ALTER TABLE "OfficeApply" DROP CONSTRAINT IF EXISTS "OfficeApply_applicantId_fkey"; +ALTER TABLE "OfficeApply" ADD CONSTRAINT "OfficeApply_applicantId_fkey" FOREIGN KEY ("applicantId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "OfficeApply" DROP CONSTRAINT IF EXISTS "OfficeApply_decidedById_fkey"; +ALTER TABLE "OfficeApply" ADD CONSTRAINT "OfficeApply_decidedById_fkey" FOREIGN KEY ("decidedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "OfficeApply" DROP CONSTRAINT IF EXISTS "OfficeApply_registeredAssetId_fkey"; +ALTER TABLE "OfficeApply" ADD CONSTRAINT "OfficeApply_registeredAssetId_fkey" FOREIGN KEY ("registeredAssetId") REFERENCES "RegisteredAsset"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "OfficeApply" DROP CONSTRAINT IF EXISTS "OfficeApply_projectId_fkey"; +ALTER TABLE "OfficeApply" ADD CONSTRAINT "OfficeApply_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "OfficeApply" DROP CONSTRAINT IF EXISTS "OfficeApply_purchaseId_fkey"; +ALTER TABLE "OfficeApply" ADD CONSTRAINT "OfficeApply_purchaseId_fkey" FOREIGN KEY ("purchaseId") REFERENCES "PurchaseRequest"("id") ON DELETE SET NULL ON UPDATE CASCADE; +ALTER TABLE "OfficeApply" DROP CONSTRAINT IF EXISTS "OfficeApply_occupancyId_fkey"; +ALTER TABLE "OfficeApply" ADD CONSTRAINT "OfficeApply_occupancyId_fkey" FOREIGN KEY ("occupancyId") REFERENCES "AssetOccupancy"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260830220000_office_flow_inbound_notices/migration.sql b/apps/api/prisma/migrations/20260830220000_office_flow_inbound_notices/migration.sql new file mode 100644 index 0000000..6ada1d0 --- /dev/null +++ b/apps/api/prisma/migrations/20260830220000_office_flow_inbound_notices/migration.sql @@ -0,0 +1,24 @@ +ALTER TABLE "RegisteredAsset" ADD COLUMN IF NOT EXISTS "status" TEXT NOT NULL DEFAULT 'ACTIVE'; +ALTER TABLE "RegisteredAsset" ADD COLUMN IF NOT EXISTS "usage" TEXT NOT NULL DEFAULT 'GENERAL'; + +ALTER TABLE "OfficeApply" ADD COLUMN IF NOT EXISTS "inboundAssetId" TEXT; +ALTER TABLE "OfficeApply" DROP CONSTRAINT IF EXISTS "OfficeApply_inboundAssetId_fkey"; +ALTER TABLE "OfficeApply" ADD CONSTRAINT "OfficeApply_inboundAssetId_fkey" FOREIGN KEY ("inboundAssetId") REFERENCES "RegisteredAsset"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +CREATE TABLE IF NOT EXISTS "Notice" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "status" TEXT NOT NULL DEFAULT 'PUBLISHED', + "createdById" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "Notice_pkey" PRIMARY KEY ("id") +); + +CREATE INDEX IF NOT EXISTS "Notice_status_idx" ON "Notice"("status"); +CREATE INDEX IF NOT EXISTS "Notice_createdAt_idx" ON "Notice"("createdAt"); + +ALTER TABLE "Notice" DROP CONSTRAINT IF EXISTS "Notice_createdById_fkey"; +ALTER TABLE "Notice" ADD CONSTRAINT "Notice_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260830230000_sms_events_notice_kind/migration.sql b/apps/api/prisma/migrations/20260830230000_sms_events_notice_kind/migration.sql new file mode 100644 index 0000000..ceff0a2 --- /dev/null +++ b/apps/api/prisma/migrations/20260830230000_sms_events_notice_kind/migration.sql @@ -0,0 +1,29 @@ +ALTER TABLE "Notice" ADD COLUMN IF NOT EXISTS "kind" TEXT NOT NULL DEFAULT 'ANNOUNCEMENT'; +CREATE INDEX IF NOT EXISTS "Notice_kind_idx" ON "Notice"("kind"); + +ALTER TABLE "SmsSendLog" ADD COLUMN IF NOT EXISTS "eventKey" TEXT; +CREATE INDEX IF NOT EXISTS "SmsSendLog_eventKey_idx" ON "SmsSendLog"("eventKey"); + +CREATE TABLE IF NOT EXISTS "SmsEventBind" ( + "id" TEXT NOT NULL, + "tenantId" TEXT NOT NULL DEFAULT '1', + "eventKey" TEXT NOT NULL, + "enabled" BOOLEAN NOT NULL DEFAULT true, + "templateCode" TEXT NOT NULL DEFAULT '', + "signName" TEXT, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "SmsEventBind_pkey" PRIMARY KEY ("id") +); +CREATE UNIQUE INDEX IF NOT EXISTS "SmsEventBind_eventKey_key" ON "SmsEventBind"("eventKey"); + +CREATE TABLE IF NOT EXISTS "SmsOtp" ( + "id" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "codeHash" TEXT NOT NULL, + "purpose" TEXT NOT NULL, + "expiresAt" TIMESTAMP(3) NOT NULL, + "usedAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "SmsOtp_pkey" PRIMARY KEY ("id") +); +CREATE INDEX IF NOT EXISTS "SmsOtp_phone_purpose_idx" ON "SmsOtp"("phone", "purpose"); diff --git a/apps/api/prisma/migrations/20260831093000_project_type_amount/migration.sql b/apps/api/prisma/migrations/20260831093000_project_type_amount/migration.sql new file mode 100644 index 0000000..0677272 --- /dev/null +++ b/apps/api/prisma/migrations/20260831093000_project_type_amount/migration.sql @@ -0,0 +1,2 @@ +ALTER TABLE "Project" ADD COLUMN IF NOT EXISTS "projectType" TEXT NOT NULL DEFAULT ''; +ALTER TABLE "Project" ADD COLUMN IF NOT EXISTS "amount" DECIMAL(18,2) NOT NULL DEFAULT 0; diff --git a/apps/api/prisma/migrations/20260831095000_notice_audience/migration.sql b/apps/api/prisma/migrations/20260831095000_notice_audience/migration.sql new file mode 100644 index 0000000..37de3a2 --- /dev/null +++ b/apps/api/prisma/migrations/20260831095000_notice_audience/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "Notice" ADD COLUMN IF NOT EXISTS "audience" TEXT NOT NULL DEFAULT 'ALL'; +ALTER TABLE "Notice" ADD COLUMN IF NOT EXISTS "departmentIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[]; +ALTER TABLE "Notice" ADD COLUMN IF NOT EXISTS "userIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[]; diff --git a/apps/api/prisma/migrations/20260831102000_role_acl_custom/migration.sql b/apps/api/prisma/migrations/20260831102000_role_acl_custom/migration.sql new file mode 100644 index 0000000..b3367db --- /dev/null +++ b/apps/api/prisma/migrations/20260831102000_role_acl_custom/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "Role" ADD COLUMN IF NOT EXISTS "aclCustom" BOOLEAN NOT NULL DEFAULT false; diff --git a/apps/api/prisma/migrations/20260831140000_push_device/migration.sql b/apps/api/prisma/migrations/20260831140000_push_device/migration.sql new file mode 100644 index 0000000..3edb3ad --- /dev/null +++ b/apps/api/prisma/migrations/20260831140000_push_device/migration.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS "PushDevice" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "vendor" TEXT NOT NULL, + "regId" TEXT NOT NULL, + "brand" TEXT, + "model" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + CONSTRAINT "PushDevice_pkey" PRIMARY KEY ("id") +); + +CREATE UNIQUE INDEX IF NOT EXISTS "PushDevice_vendor_regId_key" ON "PushDevice"("vendor", "regId"); +CREATE INDEX IF NOT EXISTS "PushDevice_userId_vendor_idx" ON "PushDevice"("userId", "vendor"); + +ALTER TABLE "PushDevice" ADD CONSTRAINT "PushDevice_userId_fkey" + FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/api/prisma/migrations/20260901090000_im_message_lifecycle/migration.sql b/apps/api/prisma/migrations/20260901090000_im_message_lifecycle/migration.sql new file mode 100644 index 0000000..2afef7b --- /dev/null +++ b/apps/api/prisma/migrations/20260901090000_im_message_lifecycle/migration.sql @@ -0,0 +1,3 @@ +ALTER TABLE "ImMessage" ADD COLUMN IF NOT EXISTS "recalledAt" TIMESTAMP(3); +ALTER TABLE "ImMessage" ADD COLUMN IF NOT EXISTS "editedAt" TIMESTAMP(3); +CREATE INDEX IF NOT EXISTS "ImMessage_recalledAt_idx" ON "ImMessage"("recalledAt"); diff --git a/apps/api/prisma/migrations/20260901150000_bid_manual_number/migration.sql b/apps/api/prisma/migrations/20260901150000_bid_manual_number/migration.sql new file mode 100644 index 0000000..b82efd5 --- /dev/null +++ b/apps/api/prisma/migrations/20260901150000_bid_manual_number/migration.sql @@ -0,0 +1,3 @@ +-- 项目筛选编号是人工填写的公告编号,不是系统单号;允许重复公告/分包再次筛选。 +DROP INDEX IF EXISTS "BidCase_tenantId_bidNo_key"; +CREATE INDEX IF NOT EXISTS "BidCase_tenantId_bidNo_idx" ON "BidCase"("tenantId", "bidNo"); diff --git a/apps/api/prisma/migrations/20260902100000_im_message_seq/migration.sql b/apps/api/prisma/migrations/20260902100000_im_message_seq/migration.sql new file mode 100644 index 0000000..a7f3077 --- /dev/null +++ b/apps/api/prisma/migrations/20260902100000_im_message_seq/migration.sql @@ -0,0 +1,26 @@ +-- Phase 1.1: 会话级单调递增 seq,用于消息排序、去重与断线补拉 +ALTER TABLE "ImConversation" ADD COLUMN IF NOT EXISTS "nextSeq" INTEGER NOT NULL DEFAULT 1; + +ALTER TABLE "ImMessage" ADD COLUMN IF NOT EXISTS "seq" INTEGER NOT NULL DEFAULT 0; + +ALTER TABLE "ImReadCursor" ADD COLUMN IF NOT EXISTS "lastReadSeq" INTEGER NOT NULL DEFAULT 0; + +-- 按 createdAt 回填历史 seq +WITH ranked AS ( + SELECT id, ROW_NUMBER() OVER (PARTITION BY "conversationId" ORDER BY "createdAt", id) AS rn + FROM "ImMessage" +) +UPDATE "ImMessage" m SET seq = ranked.rn FROM ranked WHERE m.id = ranked.id; + +UPDATE "ImConversation" c +SET "nextSeq" = COALESCE(( + SELECT MAX(seq) + 1 FROM "ImMessage" WHERE "conversationId" = c.id +), 1); + +UPDATE "ImReadCursor" rc +SET "lastReadSeq" = COALESCE(( + SELECT MAX(seq) FROM "ImMessage" m + WHERE m."conversationId" = rc."conversationId" AND m."createdAt" <= rc."lastReadAt" +), 0); + +CREATE INDEX IF NOT EXISTS "ImMessage_conversationId_seq_idx" ON "ImMessage"("conversationId", "seq"); diff --git a/apps/api/prisma/migrations/migration_lock.toml b/apps/api/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..044d57c --- /dev/null +++ b/apps/api/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/apps/api/prisma/oa-json.ts b/apps/api/prisma/oa-json.ts new file mode 100644 index 0000000..337dbcc --- /dev/null +++ b/apps/api/prisma/oa-json.ts @@ -0,0 +1,23 @@ +import * as fs from 'fs'; + +/** OA 雪花 ID 超过 Number.MAX_SAFE_INTEGER,JSON.parse 会丢精度导致报销明细串单。 */ +export function parseOaJson>(file: string): T { + const text = fs.readFileSync(file, 'utf8'); + const safe = text.replace( + /"(id|reimbursement_id|project_id|employee_id|payer_id|created_by|updated_by|department_id|cc_user_id|offset_advance_id|current_approver_id|user_id|post_id|dept_id|apply_id|tender_id|contract_id)"\s*:\s*(-?\d+)/g, + '"$1":"$2"', + ); + return JSON.parse(safe) as T; +} + +export function mapExpenseLineKind(itemType: string, remark = '') { + const t = `${itemType}${remark}`; + if (t.includes('机票') || t.includes('航班')) return 'AIR'; + if (t.includes('高铁')) return 'HSR'; + if (t.includes('火车') || t.includes('卧铺')) return 'TRAIN'; + if (t.includes('出租') || t.includes('打车') || t.includes('交通') || t.includes('高速')) return 'TAXI'; + if (t.includes('住宿') || t.includes('酒店')) return 'HOTEL'; + if (t.includes('餐')) return 'MEAL'; + if (t.includes('办公') || t.includes('胶装')) return 'OFFICE'; + return 'OTHER'; +} diff --git a/apps/api/prisma/production-clean-orphan-contract-files-20260901.sql b/apps/api/prisma/production-clean-orphan-contract-files-20260901.sql new file mode 100644 index 0000000..8c77f1b --- /dev/null +++ b/apps/api/prisma/production-clean-orphan-contract-files-20260901.sql @@ -0,0 +1,38 @@ +\set ON_ERROR_STOP on + +BEGIN; + +DO $$ +DECLARE + orphan_count integer; + mirrored_count integer; +BEGIN + SELECT count(*) INTO orphan_count + FROM "FileAsset" file + WHERE file."bizType" LIKE 'CONTRACT%' + AND NOT EXISTS (SELECT 1 FROM "Contract" contract WHERE contract.id = file."bizId"); + + SELECT count(*) INTO mirrored_count + FROM "FileAsset" orphan + WHERE orphan."bizType" LIKE 'CONTRACT%' + AND NOT EXISTS (SELECT 1 FROM "Contract" contract WHERE contract.id = orphan."bizId") + AND EXISTS ( + SELECT 1 + FROM "FileAsset" current_file + JOIN "Contract" current_contract ON current_contract.id = current_file."bizId" + WHERE current_file."bizType" = orphan."bizType" + AND current_file."fileName" = orphan."fileName" + AND current_file.size = orphan.size + ); + + IF orphan_count <> 29 OR mirrored_count <> orphan_count THEN + RAISE EXCEPTION 'Unexpected orphan contract files: orphan %, mirrored %', + orphan_count, mirrored_count; + END IF; +END $$; + +DELETE FROM "FileAsset" file +WHERE file."bizType" LIKE 'CONTRACT%' + AND NOT EXISTS (SELECT 1 FROM "Contract" contract WHERE contract.id = file."bizId"); + +COMMIT; diff --git a/apps/api/prisma/production-deduplicate-20260901.sql b/apps/api/prisma/production-deduplicate-20260901.sql new file mode 100644 index 0000000..3ae37e6 --- /dev/null +++ b/apps/api/prisma/production-deduplicate-20260901.sql @@ -0,0 +1,111 @@ +\set ON_ERROR_STOP on + +BEGIN; + +CREATE TEMP TABLE expense_dedup_map ( + old_id text PRIMARY KEY, + new_id text NOT NULL UNIQUE +) ON COMMIT DROP; + +INSERT INTO expense_dedup_map (old_id, new_id) +SELECT old_claim.id, formal_claim.id +FROM "ExpenseClaim" old_claim +JOIN "ExpenseClaim" formal_claim + ON formal_claim."claimNo" ~ '^BX-[0-9]{8}-[0-9]{3}$' + AND formal_claim."applicantId" = old_claim."applicantId" + AND formal_claim.amount = old_claim.amount + AND formal_claim."projectId" IS NOT DISTINCT FROM old_claim."projectId" + AND formal_claim."bidCaseId" IS NOT DISTINCT FROM old_claim."bidCaseId" +WHERE old_claim."createdAt" >= TIMESTAMP '2026-08-29 11:00:30' + AND old_claim."createdAt" < TIMESTAMP '2026-08-29 11:00:31'; + +INSERT INTO expense_dedup_map (old_id, new_id) +SELECT old_claim.id, formal_claim.id +FROM "ExpenseClaim" old_claim +JOIN "ExpenseClaim" formal_claim + ON formal_claim."claimNo" = CASE old_claim."claimNo" + WHEN 'BX-2026-00043' THEN 'BX-20260617-004' + WHEN 'BX-2026-00044' THEN 'BX-20260617-005' + END +WHERE old_claim."claimNo" IN ('BX-2026-00043', 'BX-2026-00044'); + +DO $$ +BEGIN + IF (SELECT count(*) FROM expense_dedup_map) <> 83 THEN + RAISE EXCEPTION 'Expense dedup mapping is incomplete: expected 83, got %', + (SELECT count(*) FROM expense_dedup_map); + END IF; +END $$; + +-- FileAsset is intentionally reassigned before duplicate claims are removed. +UPDATE "FileAsset" file +SET "bizId" = mapping.new_id +FROM expense_dedup_map mapping +WHERE file."bizType" = 'EXPENSE' + AND file."bizId" = mapping.old_id; + +DELETE FROM "ApprovalTask" task +USING expense_dedup_map mapping +WHERE task."bizId" = mapping.old_id; + +DELETE FROM "TodoTask" task +USING expense_dedup_map mapping +WHERE task."bizId" = mapping.old_id; + +DELETE FROM "ExpenseClaim" claim +USING expense_dedup_map mapping +WHERE claim.id = mapping.old_id; + +CREATE TEMP TABLE loan_dedup_map ( + old_id text PRIMARY KEY, + new_id text NOT NULL UNIQUE, + new_status text NOT NULL +) ON COMMIT DROP; + +INSERT INTO loan_dedup_map (old_id, new_id, new_status) +SELECT old_loan.id, formal_loan.id, formal_loan.status +FROM "LoanRecord" old_loan +JOIN "LoanRecord" formal_loan + ON formal_loan."loanNo" ~ '^JZ-[0-9]{8}-[0-9]{3}$' + AND formal_loan."applicantId" = old_loan."applicantId" + AND formal_loan.amount = old_loan.amount + AND formal_loan."projectId" IS NOT DISTINCT FROM old_loan."projectId" + AND formal_loan.purpose = old_loan.purpose +WHERE old_loan."loanNo" ~ '^JK-2026-[0-9]{5}$' + AND old_loan."createdAt" >= TIMESTAMP '2026-08-29 11:00:30' + AND old_loan."createdAt" < TIMESTAMP '2026-08-29 11:00:31'; + +DO $$ +BEGIN + IF (SELECT count(*) FROM loan_dedup_map) <> 9 THEN + RAISE EXCEPTION 'Loan dedup mapping is incomplete: expected 9, got %', + (SELECT count(*) FROM loan_dedup_map); + END IF; +END $$; + +-- Keep approval tasks only when the surviving formal loan is still pending. +UPDATE "ApprovalTask" task +SET "bizId" = mapping.new_id +FROM loan_dedup_map mapping +WHERE task."bizId" = mapping.old_id + AND mapping.new_status = 'PENDING'; + +DELETE FROM "ApprovalTask" task +USING loan_dedup_map mapping +WHERE task."bizId" = mapping.old_id; + +DELETE FROM "TodoTask" task +USING loan_dedup_map mapping +WHERE task."bizId" = mapping.old_id; + +UPDATE "FileAsset" file +SET "bizId" = mapping.new_id +FROM loan_dedup_map mapping +WHERE file."bizType" = 'LOAN' + AND file."bizId" = mapping.old_id; + +DELETE FROM "LoanRecord" loan +USING loan_dedup_map mapping +WHERE loan.id = mapping.old_id; + +COMMIT; diff --git a/apps/api/prisma/production-restore-missing-expense-20260901.sql b/apps/api/prisma/production-restore-missing-expense-20260901.sql new file mode 100644 index 0000000..51ca3ac --- /dev/null +++ b/apps/api/prisma/production-restore-missing-expense-20260901.sql @@ -0,0 +1,61 @@ +\set ON_ERROR_STOP on + +BEGIN; + +DO $$ +BEGIN + IF EXISTS ( + SELECT 1 FROM "ExpenseClaim" + WHERE "tenantId" = '1' AND "claimNo" = 'BX-20260809-002' + ) THEN + RAISE EXCEPTION 'Expense BX-20260809-002 already exists'; + END IF; +END $$; + +WITH restored AS ( + INSERT INTO "ExpenseClaim" ( + id, "tenantId", "claimNo", "applicantId", "costType", + "budgetCategory", "projectId", "expenseCategory", purpose, + amount, status, "submittedAt", "createdAt" + ) + SELECT + gen_random_uuid()::text, + '1', + 'BX-20260809-002', + applicant.id, + 'PROJECT', + 'TRAVEL', + project.id, + '办公', + '日常办公(江苏风影)', + 689.08, + 'DRAFT', + TIMESTAMP '2026-08-09 21:39:30', + TIMESTAMP '2026-08-09 21:39:30' + FROM "User" applicant + CROSS JOIN "Project" project + WHERE applicant."displayName" = '戴永康' + AND project."projectNo" = 'XM-20260609-002' + RETURNING id +) +INSERT INTO "ExpenseLine" ( + id, "tenantId", "claimId", kind, amount, remark, "sortNo", "createdAt" +) +SELECT gen_random_uuid()::text, '1', restored.id, line.kind, line.amount, + line.remark, line.sort_no, TIMESTAMP '2026-08-09 21:39:30' +FROM restored +CROSS JOIN ( + VALUES + ('OFFICE', 122.65::numeric, 'OA平台短信通知费用', 1), + ('OFFICE', 566.43::numeric, 'OA云服务器及流量费用', 2) +) AS line(kind, amount, remark, sort_no); + +DO $$ +BEGIN + IF (SELECT count(*) FROM "ExpenseClaim" WHERE "claimNo" = 'BX-20260809-002') <> 1 + OR (SELECT count(*) FROM "ExpenseLine" line JOIN "ExpenseClaim" claim ON claim.id = line."claimId" WHERE claim."claimNo" = 'BX-20260809-002') <> 2 THEN + RAISE EXCEPTION 'Expense BX-20260809-002 restore verification failed'; + END IF; +END $$; + +COMMIT; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma new file mode 100644 index 0000000..1ff7a38 --- /dev/null +++ b/apps/api/prisma/schema.prisma @@ -0,0 +1,1421 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +model User { + id String @id @default(uuid()) + tenantId String @default("1") + username String + passwordHash String + displayName String + status String @default("ACTIVE") + employeeId String? + mustChangePassword Boolean @default(true) + profileCompletedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + employee Employee? @relation(fields: [employeeId], references: [id]) + roles UserRole[] + refreshTokens RefreshToken[] + todos TodoTask[] @relation("TodoAssignee") + approvals ApprovalTask[] @relation("ApprovalAssignee") + bidCreated BidCase[] @relation("BidCreator") + bidReviews BidReviewer[] + bidActions BidActionLog[] + bidAssignments BidAssignee[] + bidDocVersions BidDocVersion[] + contractsCreated Contract[] @relation("ContractCreator") + contractReviews ContractReviewer[] + contractActions ContractActionLog[] + expensesCreated ExpenseClaim[] @relation("ExpenseApplicant") + expenseDecided ExpenseClaim[] @relation("ExpenseDecider") + expensesPaid ExpenseClaim[] @relation("ExpensePayer") + loansCreated LoanRecord[] @relation("LoanApplicant") + loanDecided LoanRecord[] @relation("LoanDecider") + invoicesCreated InvoiceRecord[] @relation("InvoiceCreator") + timesheets Timesheet[] + assignedTasks ProjectTask[] @relation("TaskAssignee") + projectAcceptances ProjectAcceptance[] + sealApplied SealRequest[] @relation("SealApplicant") + sealDecided SealRequest[] @relation("SealDecider") + credentialBorrows CredentialBorrow[] + employmentEvents EmploymentEvent[] @relation("EventActor") + attendanceImports AttendanceMonth[] @relation("AttendanceImporter") + leaveDecided LeaveRequest[] @relation("LeaveDecider") + payrollRuns PayrollRun[] @relation("PayrollCreator") + workReports WorkReport[] + workReportsTo WorkReport[] @relation("ReportTo") + workReportsCc WorkReport[] @relation("ReportCc") + operationLogs OperationLog[] + purchases PurchaseRequest[] @relation("PurchaseApplicant") + purchaseDecided PurchaseRequest[] @relation("PurchaseDecider") + paymentsCreated PaymentRecord[] @relation("PaymentCreator") + paymentsReceived PaymentRecord[] @relation("PaymentPayee") + projectChanges ProjectChange[] @relation("ChangeActor") + assetsHeld AssetOccupancy[] @relation("AssetHolder") + laborContractsCreated LaborContract[] @relation("LaborContractCreator") + workAssigned WorkAssignment[] @relation("WorkAssigner") + workReceived WorkAssignment[] @relation("WorkAssignee") + assetsRegistered RegisteredAsset[] @relation("AssetRegistrar") + officeApplies OfficeApply[] @relation("OfficeApplicant") + officeApplyDecided OfficeApply[] @relation("OfficeDecider") + noticesCreated Notice[] @relation("NoticeAuthor") + pushDevices PushDevice[] + + @@unique([tenantId, username]) +} + +model PushDevice { + id String @id @default(uuid()) + userId String + vendor String + regId String + brand String? + model String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@unique([vendor, regId]) + @@index([userId, vendor]) +} + +model RefreshToken { + id String @id @default(uuid()) + userId String + tokenHash String + clientKind String @default("web") + expiresAt DateTime + revokedAt DateTime? + createdAt DateTime @default(now()) + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId, clientKind]) +} + +model AuthQrTicket { + id String @id @default(uuid()) + ticket String @unique + status String @default("pending") + userId String? + confirmCodeHash String? + confirmCodeIssuedAt DateTime? + confirmCodeAttempts Int @default(0) + expiresAt DateTime + consumedAt DateTime? + createdAt DateTime @default(now()) + + @@index([status, expiresAt]) +} + +model Department { + id String @id @default(uuid()) + tenantId String @default("1") + parentId String? + name String + code String + deptType String + categoryCode String? + sortNo Int @default(0) + + parent Department? @relation("DeptTree", fields: [parentId], references: [id]) + children Department[] @relation("DeptTree") + employees Employee[] + projectTasks ProjectTask[] + eventsFrom EmploymentEvent[] @relation("EventFromDept") + eventsTo EmploymentEvent[] @relation("EventToDept") + + @@unique([tenantId, code]) +} + +model Employee { + id String @id @default(uuid()) + tenantId String @default("1") + name String + employeeNo String + departmentId String + title String? + restSchedule String @default("SINGLE") + employmentStatus String @default("REGULAR") + hiredAt DateTime? + internStartAt DateTime? + probationEndAt DateTime? + leftAt DateTime? + managerId String? + managerIds String? + gender String? + email String? + mobile String? + idNo String? + idType String? + education String? + birthday DateTime? + ethnicity String? + maritalStatus String? + hukouAddress String? + liveAddress String? + emergencyName String? + emergencyPhone String? + idIssuer String? + idValidFrom DateTime? + idValidTo DateTime? + personCategory String? + ccTo String? + shortMobile String? + workMobile String? + school String? + major String? + graduatedAt DateTime? + professionalTitle String? + certificates String? + companyName String? + jobRole String? + jobLevel String? + attendGroup String? + probationMonths Int? + contractStartAt DateTime? + contractEndAt DateTime? + recruitSource String? + landline String? + landlineExt String? + groupShortNo String? + socialNo String? + housingFundNo String? + familyNote String? + jobType String? + ccDaily String? + ccWeekly String? + ccMonthly String? + bankName String? + bankAccount String? + remark String? + baseSalary Decimal @default(0) @db.Decimal(18, 2) + postSalary Decimal @default(0) @db.Decimal(18, 2) + attendanceBonus Decimal @default(0) @db.Decimal(18, 2) + phoneSubsidy Decimal @default(0) @db.Decimal(18, 2) + transportSubsidy Decimal @default(0) @db.Decimal(18, 2) + mealSubsidy Decimal @default(0) @db.Decimal(18, 2) + probationSalary Decimal @default(0) @db.Decimal(18, 2) + perfBase Decimal @default(0) @db.Decimal(18, 2) + socialBase Decimal @default(0) @db.Decimal(18, 2) + compLeaveHours Decimal @default(0) @db.Decimal(10, 2) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + department Department @relation(fields: [departmentId], references: [id]) + manager Employee? @relation("EmpManager", fields: [managerId], references: [id]) + reports Employee[] @relation("EmpManager") + users User[] + events EmploymentEvent[] + attendance AttendanceMonth[] + punches AttendancePunch[] + leaves LeaveRequest[] + payrollItems PayrollItem[] + laborContracts LaborContract[] + + @@unique([tenantId, employeeNo]) + @@index([departmentId]) + @@index([managerId]) +} + +model LaborContract { + id String @id @default(uuid()) + tenantId String @default("1") + employeeId String + contractNo String? + startAt DateTime? + endAt DateTime? + remark String? + createdById String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + createdBy User? @relation("LaborContractCreator", fields: [createdById], references: [id]) + + @@index([employeeId]) + @@index([tenantId, createdAt]) +} + +model EmploymentEvent { + id String @id @default(uuid()) + tenantId String @default("1") + employeeId String + kind String + fromStatus String? + toStatus String? + fromDeptId String? + toDeptId String? + effectiveAt DateTime + comment String? + actorId String? + createdAt DateTime @default(now()) + + employee Employee @relation(fields: [employeeId], references: [id]) + fromDept Department? @relation("EventFromDept", fields: [fromDeptId], references: [id]) + toDept Department? @relation("EventToDept", fields: [toDeptId], references: [id]) + actor User? @relation("EventActor", fields: [actorId], references: [id]) + + @@index([employeeId]) + @@index([effectiveAt]) +} + +model AttendanceMonth { + id String @id @default(uuid()) + tenantId String @default("1") + employeeId String + yearMonth String + workDays Decimal @default(0) @db.Decimal(6, 2) + leaveDays Decimal @default(0) @db.Decimal(6, 2) + sickDays Decimal @default(0) @db.Decimal(6, 2) + overtimeHours Decimal @default(0) @db.Decimal(6, 2) + compLeaveDays Decimal @default(0) @db.Decimal(6, 2) + remark String? + importedAt DateTime @default(now()) + importedById String? + + employee Employee @relation(fields: [employeeId], references: [id]) + importedBy User? @relation("AttendanceImporter", fields: [importedById], references: [id]) + + @@unique([employeeId, yearMonth]) + @@index([yearMonth]) +} + +model AttendancePunch { + id String @id @default(uuid()) + tenantId String @default("1") + employeeId String + kind String + punchedAt DateTime @default(now()) + remark String? + lat Decimal? @db.Decimal(10, 7) + lng Decimal? @db.Decimal(10, 7) + address String? + accuracy Float? + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + + @@index([employeeId, punchedAt]) +} + +model LeaveRequest { + id String @id @default(uuid()) + tenantId String @default("1") + employeeId String + kind String + startAt DateTime + endAt DateTime + days Decimal @db.Decimal(6, 2) + reason String + toDeptId String? + targetTitle String? + status String @default("PENDING") + comment String? + decidedById String? + decidedAt DateTime? + createdAt DateTime @default(now()) + employee Employee @relation(fields: [employeeId], references: [id], onDelete: Cascade) + decidedBy User? @relation("LeaveDecider", fields: [decidedById], references: [id]) + + @@index([employeeId]) + @@index([status]) +} + +model HrSetting { + id String @id @default("default") + tenantId String @default("1") @unique + idMask Boolean @default(true) + autoAccount Boolean @default(false) + probationMonths Int @default(3) + defaultRest String @default("SINGLE") + workStart String @default("09:00") + workEnd String @default("18:00") + updatedAt DateTime @updatedAt +} + +model AttendanceRule { + id String @id @default(uuid()) + tenantId String @default("1") + name String + restSchedule String + workStart String + workEnd String + lateGraceMinutes Int @default(0) + punchPlace String? + punchAddress String? + punchLat Decimal? @db.Decimal(10, 7) + punchLng Decimal? @db.Decimal(10, 7) + punchRadiusMeters Int @default(300) + enabled Boolean @default(true) + sortNo Int @default(0) + remark String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([tenantId, name]) +} + +model PayrollRun { + id String @id @default(uuid()) + tenantId String @default("1") + yearMonth String + status String @default("DRAFT") + createdById String? + createdAt DateTime @default(now()) + confirmedAt DateTime? + + createdBy User? @relation("PayrollCreator", fields: [createdById], references: [id]) + items PayrollItem[] + + @@unique([tenantId, yearMonth]) +} + +model PayrollItem { + id String @id @default(uuid()) + runId String + employeeId String + prorationFactor Decimal @default(1) @db.Decimal(8, 6) + scheduledDays Decimal @default(0) @db.Decimal(6, 2) + dailyRate Decimal @default(0) @db.Decimal(18, 2) + basePay Decimal @default(0) @db.Decimal(18, 2) + performancePay Decimal @default(0) @db.Decimal(18, 2) + performanceRatio Decimal @default(100) @db.Decimal(5, 2) + leaveDeduct Decimal @default(0) @db.Decimal(18, 2) + sickDeduct Decimal @default(0) @db.Decimal(18, 2) + socialPersonal Decimal @default(0) @db.Decimal(18, 2) + housingFund Decimal @default(0) @db.Decimal(18, 2) + tax Decimal @default(0) @db.Decimal(18, 2) + grossPay Decimal @default(0) @db.Decimal(18, 2) + netPay Decimal @default(0) @db.Decimal(18, 2) + overtimeHours Decimal @default(0) @db.Decimal(6, 2) + compLeaveUsedDays Decimal @default(0) @db.Decimal(6, 2) + compLeaveHours Decimal @default(0) @db.Decimal(10, 2) + + run PayrollRun @relation(fields: [runId], references: [id], onDelete: Cascade) + employee Employee @relation(fields: [employeeId], references: [id]) + + @@unique([runId, employeeId]) +} + +model Role { + id String @id @default(uuid()) + tenantId String @default("1") + name String + code String + description String @default("") + dataScope String @default("ALL") + aclCustom Boolean @default(false) + + permissions RolePermission[] + users UserRole[] + + @@unique([tenantId, code]) +} + +model Permission { + id String @id @default(uuid()) + parentId String? + name String + code String @unique + permType String + path String? + icon String? + sortNo Int @default(0) + visible Boolean @default(true) + + parent Permission? @relation("PermTree", fields: [parentId], references: [id]) + children Permission[] @relation("PermTree") + roles RolePermission[] +} + +model RolePermission { + roleId String + permissionId String + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) + permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade) + + @@id([roleId, permissionId]) +} + +model UserRole { + userId String + roleId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + role Role @relation(fields: [roleId], references: [id], onDelete: Cascade) + + @@id([userId, roleId]) +} + +model DictItem { + id String @id @default(uuid()) + tenantId String @default("1") + dictType String + label String + value String + sortNo Int @default(0) + + @@unique([tenantId, dictType, value]) +} + +model NumberRule { + id String @id @default(uuid()) + tenantId String @default("1") + code String + name String + prefix String + padLength Int @default(5) + + @@unique([tenantId, code]) +} + +model SealItem { + id String @id @default(uuid()) + tenantId String @default("1") + name String + sealType String @default("公章") + keeper String @default("行政") + legalEntityId String? + status String @default("IN_STOCK") + createdAt DateTime @default(now()) + legalEntity LegalEntity? @relation(fields: [legalEntityId], references: [id]) +} + +model LegalEntity { + id String @id @default(uuid()) + tenantId String @default("1") + name String + shortName String + creditNo String? + isDefault Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + bidCases BidCase[] + contracts Contract[] + sealItems SealItem[] + assets RegisteredAsset[] + + @@unique([tenantId, name]) +} + +model BusinessParty { + id String @id @default(uuid()) + tenantId String @default("1") + name String + creditNo String? + level String @default("B") + riskFlag Boolean @default(false) + remark String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + contacts PartyContact[] + bidCases BidCase[] + + @@unique([tenantId, name]) +} + +model PartyContact { + id String @id @default(uuid()) + tenantId String @default("1") + partyId String + name String + phone String? + title String? + party BusinessParty @relation(fields: [partyId], references: [id], onDelete: Cascade) +} + +model Qualification { + id String @id @default(uuid()) + tenantId String @default("1") + name String + code String + expiresAt DateTime? + keeper String? + createdAt DateTime @default(now()) + borrows CredentialBorrow[] +} + +model SealRequest { + id String @id @default(uuid()) + tenantId String @default("1") + sealType String + reason String + bidCaseId String? + contractId String? + applicantId String? + takeOut Boolean @default(false) + returnAt DateTime? + returnedAt DateTime? + decidedAt DateTime? + decidedById String? + rejectComment String? + status String @default("PENDING") + createdAt DateTime @default(now()) + contract Contract? @relation(fields: [contractId], references: [id]) + bidCase BidCase? @relation(fields: [bidCaseId], references: [id]) + applicant User? @relation("SealApplicant", fields: [applicantId], references: [id]) + decidedBy User? @relation("SealDecider", fields: [decidedById], references: [id]) +} + +model CredentialBorrow { + id String @id @default(uuid()) + tenantId String @default("1") + itemName String + borrower String + borrowerId String? + qualificationId String? + bidCaseId String? + dueAt DateTime? + returnedAt DateTime? + remark String? + status String @default("BORROWED") + createdAt DateTime @default(now()) + borrowerUser User? @relation(fields: [borrowerId], references: [id]) + qualification Qualification? @relation(fields: [qualificationId], references: [id]) + bidCase BidCase? @relation(fields: [bidCaseId], references: [id]) +} + +model BidCase { + id String @id @default(uuid()) + tenantId String @default("1") + bidNo String + name String + externalNo String? + projectType String + sourceType String @default("FLOW") + source String? + qualificationNeed String? + legalEntityId String? + tenderMethod String? + applyStartAt DateTime? + applyEndAt DateTime? + applyMethod String? + openAt DateTime? + openMethod String? + openPlace String? + securityLevel String @default("INTERNAL") + projectNature String? + partyId String? + contactName String? + contactPhone String? + bondMethod String? + bondStatus String? + bondPaidAt DateTime? + delivererName String? + priceCap String? + quoteAmount String? + summary String? + remark String? + status String @default("TECH_INITIAL") + nasPath String? + fetchedAt DateTime? + reviewCycle Int @default(1) + printCost Decimal? @db.Decimal(18, 2) + printNote String? + printedAt DateTime? + costDisposition String? + resultNote String? + winnerName String? + winnerAmount Decimal? @db.Decimal(18, 2) + winnerNote String? + openedResultAt DateTime? + needSurvey Boolean @default(false) + createdById String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + legalEntity LegalEntity? @relation(fields: [legalEntityId], references: [id]) + party BusinessParty? @relation(fields: [partyId], references: [id]) + createdBy User? @relation("BidCreator", fields: [createdById], references: [id]) + reviewers BidReviewer[] + actionLogs BidActionLog[] + assignees BidAssignee[] + versions BidDocVersion[] + expenses ExpenseClaim[] + loans LoanRecord[] + contracts Contract[] + bonds BondRecord[] + sealRequests SealRequest[] + credentialBorrows CredentialBorrow[] + + @@index([tenantId, bidNo]) +} + +model BidReviewer { + id String @id @default(uuid()) + bidCaseId String + userId String + stage String + cycle Int @default(1) + result String @default("PENDING") + comment String? + decidedAt DateTime? + bidCase BidCase @relation(fields: [bidCaseId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id]) + + @@unique([bidCaseId, userId, stage, cycle]) + @@index([bidCaseId]) +} + +model BidAssignee { + id String @id @default(uuid()) + bidCaseId String + userId String + role String + createdAt DateTime @default(now()) + bidCase BidCase @relation(fields: [bidCaseId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id]) + + @@unique([bidCaseId, userId, role]) + @@index([bidCaseId]) +} + +model BidDocVersion { + id String @id @default(uuid()) + bidCaseId String + versionNo Int + nasPath String + note String? + submittedById String + createdAt DateTime @default(now()) + bidCase BidCase @relation(fields: [bidCaseId], references: [id], onDelete: Cascade) + submittedBy User @relation(fields: [submittedById], references: [id]) + + @@unique([bidCaseId, versionNo]) + @@index([bidCaseId]) +} + +model BidActionLog { + id String @id @default(uuid()) + bidCaseId String + actorId String + action String + fromStatus String? + toStatus String? + comment String? + createdAt DateTime @default(now()) + bidCase BidCase @relation(fields: [bidCaseId], references: [id], onDelete: Cascade) + actor User @relation(fields: [actorId], references: [id]) + + @@index([bidCaseId]) +} + +model TodoTask { + id String @id @default(uuid()) + tenantId String @default("1") + title String + bizType String + bizId String? + assigneeId String + dueAt DateTime? + status String @default("OPEN") + createdAt DateTime @default(now()) + assignee User @relation("TodoAssignee", fields: [assigneeId], references: [id]) +} + +model ApprovalTask { + id String @id @default(uuid()) + tenantId String @default("1") + title String + bizType String + bizId String? + assigneeId String + status String @default("PENDING") + createdAt DateTime @default(now()) + assignee User @relation("ApprovalAssignee", fields: [assigneeId], references: [id]) +} + +model WorkReport { + id String @id @default(uuid()) + tenantId String @default("1") + authorId String + toUserId String? + ccUserId String? + period String + periodType String @default("DAY") + periodStart DateTime? + hours Decimal @default(0) @db.Decimal(8, 2) + content String @default("") + status String @default("DRAFT") + submittedAt DateTime? + createdAt DateTime @default(now()) + author User @relation(fields: [authorId], references: [id]) + toUser User? @relation("ReportTo", fields: [toUserId], references: [id]) + ccUser User? @relation("ReportCc", fields: [ccUserId], references: [id]) + + @@index([authorId]) + @@index([toUserId]) + @@index([ccUserId]) +} + +model Contract { + id String @id @default(uuid()) + tenantId String @default("1") + contractNo String? + name String + kind String @default("OTHER") + bidCaseId String? + legalEntityId String? + amount Decimal @default(0) @db.Decimal(18, 2) + status String @default("DRAFT") + partyA String? + partyB String? + nasPath String? + remark String? + signedAt DateTime? + submittedAt DateTime? + approvedAt DateTime? + createdById String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + bidCase BidCase? @relation(fields: [bidCaseId], references: [id]) + legalEntity LegalEntity? @relation(fields: [legalEntityId], references: [id]) + createdBy User? @relation("ContractCreator", fields: [createdById], references: [id]) + projects Project[] + milestones PaymentMilestone[] + reviewers ContractReviewer[] + actionLogs ContractActionLog[] + sealRequests SealRequest[] + invoices InvoiceRecord[] + bonds BondRecord[] + + @@unique([tenantId, contractNo]) +} + +model ContractReviewer { + id String @id @default(uuid()) + contractId String + userId String + result String @default("PENDING") + comment String? + decidedAt DateTime? + contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade) + user User @relation(fields: [userId], references: [id]) + + @@unique([contractId, userId]) + @@index([contractId]) +} + +model ContractActionLog { + id String @id @default(uuid()) + contractId String + actorId String + action String + fromStatus String? + toStatus String? + comment String? + createdAt DateTime @default(now()) + contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade) + actor User @relation(fields: [actorId], references: [id]) + + @@index([contractId]) +} + +model PaymentMilestone { + id String @id @default(uuid()) + contractId String + name String + ratio Decimal @db.Decimal(5, 2) + dueAt DateTime? + trigger String? + contract Contract @relation(fields: [contractId], references: [id], onDelete: Cascade) + invoice InvoiceRecord? +} + +model Project { + id String @id @default(uuid()) + tenantId String @default("1") + projectNo String + name String + partyName String? + contractId String? + projectType String @default("") + amount Decimal @default(0) @db.Decimal(18, 2) + status String @default("ACTIVE") + presalesCost Decimal @default(0) @db.Decimal(18, 2) + hourRate Decimal @default(200) @db.Decimal(18, 2) + internalAcceptedAt DateTime? + customerAcceptedAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + contract Contract? @relation(fields: [contractId], references: [id]) + tasks ProjectTask[] + timesheets Timesheet[] + expenses ExpenseClaim[] + loans LoanRecord[] + bonds BondRecord[] + budgets ProjectBudget[] + acceptances ProjectAcceptance[] + acceptMaterials ProjectAcceptMaterial[] + purchases PurchaseRequest[] + changes ProjectChange[] + assets AssetOccupancy[] + officeApplies OfficeApply[] + + @@unique([tenantId, projectNo]) +} + +model ProjectBudget { + id String @id @default(uuid()) + projectId String + category String + amount Decimal @db.Decimal(18, 2) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@unique([projectId, category]) +} + +model ProjectAcceptance { + id String @id @default(uuid()) + projectId String + kind String + result String + comment String? + actorId String + decidedAt DateTime @default(now()) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + actor User @relation(fields: [actorId], references: [id]) + + @@index([projectId]) +} + +model ProjectAcceptMaterial { + id String @id @default(uuid()) + projectId String + name String + remark String? + required Boolean @default(true) + sortNo Int @default(0) + createdAt DateTime @default(now()) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + + @@index([projectId]) +} + +model ProjectTask { + id String @id @default(uuid()) + projectId String + name String + department String? + departmentId String? + assigneeId String? + sortNo Int @default(0) + dueAt DateTime? + status String @default("OPEN") + createdAt DateTime @default(now()) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + dept Department? @relation(fields: [departmentId], references: [id]) + assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id]) + timesheets Timesheet[] + + @@index([projectId]) +} + +model Timesheet { + id String @id @default(uuid()) + tenantId String @default("1") + projectId String + taskId String? + userId String + workDate DateTime + hours Decimal @db.Decimal(6, 2) + costAmount Decimal @default(0) @db.Decimal(18, 2) + note String? + createdAt DateTime @default(now()) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + task ProjectTask? @relation(fields: [taskId], references: [id], onDelete: SetNull) + user User @relation(fields: [userId], references: [id]) + + @@index([projectId]) + @@index([userId]) +} + +model ExpenseClaim { + id String @id @default(uuid()) + tenantId String @default("1") + claimNo String + applicantId String + costType String + budgetCategory String? + bidCaseId String? + projectId String? + loanId String? + expenseCategory String? + purpose String? + amount Decimal @default(0) @db.Decimal(18, 2) + status String @default("DRAFT") + remark String? + submittedAt DateTime? + decidedAt DateTime? + decidedById String? + rejectComment String? + prepaid Boolean @default(false) + payerId String? + createdAt DateTime @default(now()) + bidCase BidCase? @relation(fields: [bidCaseId], references: [id]) + project Project? @relation(fields: [projectId], references: [id]) + loan LoanRecord? @relation(fields: [loanId], references: [id]) + applicant User @relation("ExpenseApplicant", fields: [applicantId], references: [id]) + decidedBy User? @relation("ExpenseDecider", fields: [decidedById], references: [id]) + payer User? @relation("ExpensePayer", fields: [payerId], references: [id]) + lines ExpenseLine[] + + @@unique([tenantId, claimNo]) + @@index([applicantId]) + @@index([loanId]) + @@index([payerId]) +} + +model ExpenseLine { + id String @id @default(uuid()) + tenantId String @default("1") + claimId String + kind String + amount Decimal @db.Decimal(18, 2) + invoiceNo String? + occurAt DateTime? + remark String? + sortNo Int @default(0) + createdAt DateTime @default(now()) + claim ExpenseClaim @relation(fields: [claimId], references: [id], onDelete: Cascade) + + @@index([claimId]) +} + +model LoanRecord { + id String @id @default(uuid()) + tenantId String @default("1") + loanNo String + applicantId String + costType String + bidCaseId String? + projectId String? + amount Decimal @db.Decimal(18, 2) + offsetAmount Decimal @default(0) @db.Decimal(18, 2) + purpose String + status String @default("DRAFT") + remark String? + submittedAt DateTime? + decidedAt DateTime? + decidedById String? + createdAt DateTime @default(now()) + bidCase BidCase? @relation(fields: [bidCaseId], references: [id]) + project Project? @relation(fields: [projectId], references: [id]) + applicant User @relation("LoanApplicant", fields: [applicantId], references: [id]) + decidedBy User? @relation("LoanDecider", fields: [decidedById], references: [id]) + expenses ExpenseClaim[] + + @@unique([tenantId, loanNo]) + @@index([applicantId]) +} + +model BondRecord { + id String @id @default(uuid()) + tenantId String @default("1") + bondNo String + bidCaseId String? + projectId String? + contractId String? + bondType String + amount Decimal @db.Decimal(18, 2) + paidAt DateTime? + dueBackAt DateTime? + returnedAt DateTime? + status String @default("UNPAID") + remark String? + createdAt DateTime @default(now()) + bidCase BidCase? @relation(fields: [bidCaseId], references: [id], onDelete: SetNull) + project Project? @relation(fields: [projectId], references: [id]) + contract Contract? @relation(fields: [contractId], references: [id]) + + @@unique([tenantId, bondNo]) +} + +model InvoiceRecord { + id String @id @default(uuid()) + tenantId String @default("1") + invoiceNo String? + contractId String + milestoneId String @unique + amount Decimal @db.Decimal(18, 2) + status String @default("TO_ISSUE") + dueAt DateTime? + issuedAt DateTime? + receivedAt DateTime? + remark String? + createdById String? + createdAt DateTime @default(now()) + contract Contract @relation(fields: [contractId], references: [id]) + milestone PaymentMilestone @relation(fields: [milestoneId], references: [id]) + createdBy User? @relation("InvoiceCreator", fields: [createdById], references: [id]) +} + +model FileAsset { + id String @id @default(uuid()) + tenantId String @default("1") + bizType String + bizId String + fileName String + mimeType String + size Int + storageKey String + uploadedById String + createdAt DateTime @default(now()) + + @@index([bizType, bizId]) + @@index([uploadedById]) +} + +model PurchaseRequest { + id String @id @default(uuid()) + tenantId String @default("1") + title String + projectId String? + amount Decimal @default(0) @db.Decimal(18, 2) + status String @default("DRAFT") + remark String? + applicantId String? + decidedById String? + decidedAt DateTime? + createdAt DateTime @default(now()) + project Project? @relation(fields: [projectId], references: [id]) + applicant User? @relation("PurchaseApplicant", fields: [applicantId], references: [id]) + decidedBy User? @relation("PurchaseDecider", fields: [decidedById], references: [id]) + officeApplies OfficeApply[] +} + +model AssetOccupancy { + id String @id @default(uuid()) + tenantId String @default("1") + assetName String + holder String + holderId String? + projectId String? + registeredAssetId String? + status String @default("IN_USE") + dueAt DateTime? + returnedAt DateTime? + createdAt DateTime @default(now()) + project Project? @relation(fields: [projectId], references: [id]) + holderUser User? @relation("AssetHolder", fields: [holderId], references: [id]) + registeredAsset RegisteredAsset? @relation(fields: [registeredAssetId], references: [id]) + officeApplies OfficeApply[] @relation("ApplyOccupancy") + + @@index([registeredAssetId]) +} + +model ProjectChange { + id String @id @default(uuid()) + tenantId String @default("1") + projectId String + summary String + amountDelta Decimal @default(0) @db.Decimal(18, 2) + status String @default("PENDING") + comment String? + actorId String? + createdAt DateTime @default(now()) + decidedAt DateTime? + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + actor User? @relation("ChangeActor", fields: [actorId], references: [id]) + + @@index([projectId]) +} + +model PaymentRecord { + id String @id @default(uuid()) + tenantId String @default("1") + payNo String + kind String + refId String? + amount Decimal @db.Decimal(18, 2) + status String @default("TODO") + remark String? + paidAt DateTime? + createdById String? + payeeUserId String? + payeeName String? + payeeKind String? + bankName String? + bankAccount String? + payChannel String @default("MANUAL") + bankStatus String @default("NONE") + bankTxnNo String? + bankPayload String? + createdAt DateTime @default(now()) + createdBy User? @relation("PaymentCreator", fields: [createdById], references: [id]) + payeeUser User? @relation("PaymentPayee", fields: [payeeUserId], references: [id]) + + @@unique([tenantId, payNo]) + @@index([refId]) + @@index([payeeUserId]) +} + +model OperationLog { + id String @id @default(uuid()) + tenantId String @default("1") + userId String? + username String? + action String + path String + detail String? + success Boolean @default(true) + requestId String? + ip String? + createdAt DateTime @default(now()) + user User? @relation(fields: [userId], references: [id]) + + @@index([createdAt]) + @@index([action]) +} + +model ImConversation { + id String @id @default(uuid()) + tenantId String @default("1") + type String @default("direct") + name String @default("") + ownerId String? + userAId String? + userBId String? + lastText String @default("") + lastAt DateTime @default(now()) + nextSeq Int @default(1) + createdAt DateTime @default(now()) + + @@unique([userAId, userBId]) + @@index([lastAt]) + @@index([type, lastAt]) +} + +model ImConversationMember { + id String @id @default(uuid()) + conversationId String + userId String + role String @default("member") + joinedAt DateTime @default(now()) + leftAt DateTime? + + @@unique([conversationId, userId]) + @@index([userId]) +} + +model ImMessage { + id String @id @default(uuid()) + tenantId String @default("1") + conversationId String + seq Int @default(0) + fromUserId String + toUserId String @default("") + kind String @default("chat") + contentType String @default("text") + body String + meta String @default("") + fingerprint String + createdAt DateTime @default(now()) + + @@unique([fingerprint]) + @@index([conversationId, createdAt]) + @@index([conversationId, seq]) + @@index([toUserId, createdAt]) +} + +model ImReadCursor { + id String @id @default(uuid()) + userId String + conversationId String + lastReadAt DateTime @default(now()) + lastReadSeq Int @default(0) + + @@unique([userId, conversationId]) +} + +model ImMessageReceipt { + id String @id @default(uuid()) + messageId String + userId String + readAt DateTime @default(now()) + + @@unique([messageId, userId]) + @@index([messageId]) +} + +model ImCall { + id String @id @default(uuid()) + tenantId String @default("1") + conversationId String? + kind String + status String @default("ringing") + initiatorId String + title String @default("") + createdAt DateTime @default(now()) + endedAt DateTime? + + @@index([status, createdAt]) +} + +model ImCallMember { + id String @id @default(uuid()) + callId String + userId String + status String @default("invited") + joinedAt DateTime? + leftAt DateTime? + + @@unique([callId, userId]) +} + +model ImCallSignal { + id String @id @default(uuid()) + callId String + fromUserId String + toUserId String + kind String + payload String + createdAt DateTime @default(now()) + + @@index([callId, createdAt]) + @@index([toUserId, createdAt]) +} + +model SmsSendLog { + id String @id @default(uuid()) + tenantId String @default("1") + phones String + signName String + templateCode String + templateParam String? + eventKey String? + bizId String? + code String + message String? + createdById String? + createdAt DateTime @default(now()) + + @@index([createdAt]) + @@index([eventKey]) +} + +model SmsEventBind { + id String @id @default(uuid()) + tenantId String @default("1") + eventKey String @unique + enabled Boolean @default(true) + templateCode String @default("") + signName String? + updatedAt DateTime @updatedAt +} + +model SmsOtp { + id String @id @default(uuid()) + phone String + codeHash String + purpose String + expiresAt DateTime + usedAt DateTime? + createdAt DateTime @default(now()) + + @@index([phone, purpose]) +} + +model WorkAssignment { + id String @id @default(uuid()) + tenantId String @default("1") + title String + content String @default("") + assigneeId String + dueAt DateTime? + status String @default("OPEN") + createdById String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + assignee User @relation("WorkAssignee", fields: [assigneeId], references: [id]) + createdBy User @relation("WorkAssigner", fields: [createdById], references: [id]) + + @@index([assigneeId]) + @@index([createdById]) +} + +model RegisteredAsset { + id String @id @default(uuid()) + tenantId String @default("1") + kind String + name String + code String? + spec String? + keeper String? + location String? + amount Decimal? @db.Decimal(18, 2) + remark String? + status String @default("ACTIVE") + usage String @default("GENERAL") + legalEntityId String? + createdById String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + legalEntity LegalEntity? @relation(fields: [legalEntityId], references: [id]) + createdBy User? @relation("AssetRegistrar", fields: [createdById], references: [id]) + occupancies AssetOccupancy[] + officeApplies OfficeApply[] + inboundFrom OfficeApply[] @relation("ApplyInbound") + + @@index([kind]) + @@index([legalEntityId]) +} + +model OfficeApply { + id String @id @default(uuid()) + tenantId String @default("1") + kind String + title String + reason String? + status String @default("PENDING") + assetKind String? + registeredAssetId String? + amount Decimal? @db.Decimal(18, 2) + destination String? + startAt DateTime? + endAt DateTime? + projectId String? + purchaseId String? + occupancyId String? + inboundAssetId String? + applicantId String + decidedById String? + decidedAt DateTime? + remark String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + applicant User @relation("OfficeApplicant", fields: [applicantId], references: [id]) + decidedBy User? @relation("OfficeDecider", fields: [decidedById], references: [id]) + registeredAsset RegisteredAsset? @relation(fields: [registeredAssetId], references: [id]) + project Project? @relation(fields: [projectId], references: [id]) + purchase PurchaseRequest? @relation(fields: [purchaseId], references: [id]) + occupancy AssetOccupancy? @relation("ApplyOccupancy", fields: [occupancyId], references: [id]) + inboundAsset RegisteredAsset? @relation("ApplyInbound", fields: [inboundAssetId], references: [id]) + + @@index([applicantId]) + @@index([status]) + @@index([kind]) +} + +model Notice { + id String @id @default(uuid()) + tenantId String @default("1") + title String + content String + status String @default("PUBLISHED") + kind String @default("ANNOUNCEMENT") + audience String @default("ALL") + departmentIds String[] @default([]) + userIds String[] @default([]) + createdById String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + createdBy User @relation("NoticeAuthor", fields: [createdById], references: [id]) + + @@index([status]) + @@index([kind]) + @@index([createdAt]) +} diff --git a/apps/api/prisma/seed.ts b/apps/api/prisma/seed.ts new file mode 100644 index 0000000..830e2ac --- /dev/null +++ b/apps/api/prisma/seed.ts @@ -0,0 +1,383 @@ +import { PrismaClient } from '@prisma/client'; +import * as argon2 from 'argon2'; +import { MENUS, type MenuNode } from './menus'; + +const prisma = new PrismaClient(); + +async function main() { + const passwordHash = await argon2.hash('Demo@123456'); + const adminHash = await argon2.hash('Admin@123456'); + + await prisma.$transaction(async (tx) => { + await tx.operationLog.deleteMany(); + await tx.paymentRecord.deleteMany(); + await tx.projectChange.deleteMany(); + await tx.sealItem.deleteMany(); + await tx.numberRule.deleteMany(); + await tx.todoTask.deleteMany(); + await tx.approvalTask.deleteMany(); + await tx.workReport.deleteMany(); + await tx.payrollItem.deleteMany(); + await tx.payrollRun.deleteMany(); + await tx.attendanceMonth.deleteMany(); + await tx.employmentEvent.deleteMany(); + await tx.expenseClaim.deleteMany(); + await tx.loanRecord.deleteMany(); + await tx.bondRecord.deleteMany(); + await tx.invoiceRecord.deleteMany(); + await tx.timesheet.deleteMany(); + await tx.projectAcceptance.deleteMany(); + await tx.projectBudget.deleteMany(); + await tx.projectTask.deleteMany(); + await tx.purchaseRequest.deleteMany(); + await tx.assetOccupancy.deleteMany(); + await tx.contractReviewer.deleteMany(); + await tx.contractActionLog.deleteMany(); + await tx.paymentMilestone.deleteMany(); + await tx.project.deleteMany(); + await tx.sealRequest.deleteMany(); + await tx.contract.deleteMany(); + await tx.bidDocVersion.deleteMany(); + await tx.bidAssignee.deleteMany(); + await tx.bidActionLog.deleteMany(); + await tx.bidReviewer.deleteMany(); + await tx.credentialBorrow.deleteMany(); + await tx.bidCase.deleteMany(); + await tx.partyContact.deleteMany(); + await tx.qualification.deleteMany(); + await tx.userRole.deleteMany(); + await tx.rolePermission.deleteMany(); + await tx.refreshToken.deleteMany(); + await tx.user.deleteMany(); + await tx.employee.deleteMany(); + await tx.department.deleteMany(); + await tx.permission.deleteMany(); + await tx.role.deleteMany(); + await tx.dictItem.deleteMany(); + await tx.attendanceRule.deleteMany(); + await tx.hrSetting.deleteMany(); + await tx.legalEntity.deleteMany(); + await tx.businessParty.deleteMany(); + + const root = await tx.department.create({ + data: { + name: '演示科技有限公司', + code: 'ROOT', + deptType: 'ADMIN', + sortNo: 0, + }, + }); + const depts = await Promise.all( + [ + ['商务部', 'BIZ', 'BUSINESS', null, 10], + ['三维部门', 'TECH_3D', 'TECH', 'CAT_3D', 20], + ['摄制部门', 'TECH_FILM', 'TECH', 'CAT_FILM', 30], + ['物资部门', 'TECH_MATERIAL', 'TECH', 'CAT_MATERIAL', 40], + ['软件研发部门', 'TECH_SOFTWARE', 'TECH', 'CAT_SOFTWARE', 50], + ['财务部', 'FIN', 'FINANCE', null, 60], + ['人事行政', 'HR', 'HR', null, 70], + ].map(([name, code, deptType, categoryCode, sortNo]) => + tx.department.create({ + data: { + parentId: root.id, + name: name as string, + code: code as string, + deptType: deptType as string, + categoryCode: categoryCode as string | null, + sortNo: sortNo as number, + }, + }), + ), + ); + const byCode = Object.fromEntries(depts.map((d) => [d.code, d])); + + const permIds: Record = {}; + const writeMenus = async (nodes: MenuNode[], parentId: string | null, root = true) => { + for (let i = 0; i < nodes.length; i++) { + const menu = nodes[i]; + const row = await tx.permission.create({ + data: { + parentId, + name: menu.name, + code: menu.code, + permType: 'MENU', + path: menu.path, + icon: menu.icon, + sortNo: root ? (i + 1) * 10 : i + 1, + }, + }); + permIds[menu.code] = row.id; + if (menu.children?.length) await writeMenus(menu.children, row.id, false); + } + }; + await writeMenus(MENUS, null); + + const roles = { + admin: await tx.role.create({ data: { name: '系统管理员', code: 'admin', dataScope: 'ALL' } }), + owner: await tx.role.create({ data: { name: '企业负责人', code: 'owner', dataScope: 'ALL' } }), + biz_director: await tx.role.create({ data: { name: '商务总监', code: 'biz_director', dataScope: 'DEPT' } }), + biz_staff: await tx.role.create({ data: { name: '商务专员', code: 'biz_staff', dataScope: 'SELF' } }), + tech_director: await tx.role.create({ data: { name: '技术总监', code: 'tech_director', dataScope: 'DEPT' } }), + finance: await tx.role.create({ data: { name: '财务人员', code: 'finance', dataScope: 'ALL' } }), + hr: await tx.role.create({ data: { name: '人事', code: 'hr', dataScope: 'ALL' } }), + employee: await tx.role.create({ data: { name: '员工', code: 'employee', dataScope: 'SELF' } }), + }; + + const allPerms = await tx.permission.findMany(); + const grant = async (roleId: string, codes: string[]) => { + const selected = + codes[0] === '*' + ? allPerms + : allPerms.filter((p) => codes.some((c) => p.code === c || p.code.startsWith(c))); + await tx.rolePermission.createMany({ + data: selected.map((p) => ({ roleId, permissionId: p.id })), + skipDuplicates: true, + }); + }; + await grant(roles.admin.id, ['*']); + await grant(roles.owner.id, ['*']); + await grant(roles.biz_director.id, ['office', 'party', 'seal', 'bid', 'contract', 'finance', 'report', 'hr:performance']); + await grant(roles.biz_staff.id, ['office', 'party', 'seal', 'bid', 'contract']); + await grant(roles.tech_director.id, ['office', 'bid', 'project', 'report', 'hr:performance']); + await grant(roles.finance.id, ['office', 'finance', 'contract', 'report', 'bid:result']); + await grant(roles.hr.id, ['office', 'hr', 'system:hr', 'system:org', 'system:attendance-rules']); + await grant(roles.employee.id, ['office', 'project:timesheets']); + + const mkEmp = ( + name: string, + no: string, + deptCode: string, + title: string, + rest: string, + baseSalary: number, + perfBase: number, + socialBase: number, + ) => + tx.employee.create({ + data: { + name, + employeeNo: no, + departmentId: byCode[deptCode].id, + title, + restSchedule: rest, + employmentStatus: 'REGULAR', + hiredAt: new Date('2022-01-01'), + baseSalary, + perfBase, + socialBase, + }, + }); + + const emps = { + admin: await mkEmp('系统管理员', 'E000', 'HR', '管理员', 'DOUBLE', 8000, 0, 8000), + owner: await mkEmp('陈总', 'E001', 'BIZ', '企业负责人', 'SINGLE', 25000, 10000, 20000), + bizdir: await mkEmp('王商务', 'E010', 'BIZ', '商务总监', 'SINGLE', 18000, 8000, 18000), + bizstaff: await mkEmp('李专员', 'E011', 'BIZ', '商务专员', 'SINGLE', 9000, 2500, 9000), + dir3d: await mkEmp('赵三维', 'E020', 'TECH_3D', '三维总监', 'SINGLE', 15000, 5000, 15000), + dirfilm: await mkEmp('孙摄制', 'E021', 'TECH_FILM', '摄制总监', 'SINGLE', 15000, 5000, 15000), + dirmaterial: await mkEmp('周物资', 'E022', 'TECH_MATERIAL', '物资总监', 'SINGLE', 15000, 5000, 15000), + dirsoft: await mkEmp('吴软件', 'E023', 'TECH_SOFTWARE', '软件总监', 'DOUBLE', 15000, 5000, 15000), + finance: await mkEmp('郑财务', 'E030', 'FIN', '会计', 'SINGLE', 10000, 2000, 10000), + hr: await mkEmp('冯人事', 'E040', 'HR', '人事专员', 'SINGLE', 9000, 2000, 9000), + employee: await mkEmp('钱员工', 'E100', 'TECH_SOFTWARE', '工程师', 'DOUBLE', 10000, 3000, 10000), + }; + + const softpm = await mkEmp('软件项目经理', 'E101', 'TECH_SOFTWARE', '项目经理', 'DOUBLE', 12000, 4000, 12000); + await tx.employee.update({ + where: { id: emps.employee.id }, + data: { managerId: softpm.id }, + }); + await tx.employee.update({ + where: { id: softpm.id }, + data: { managerId: emps.dirsoft.id }, + }); + + const mkUser = async ( + username: string, + hash: string, + emp: { id: string; name: string }, + roleId: string, + ) => { + const u = await tx.user.create({ + data: { + username, + passwordHash: hash, + displayName: emp.name, + employeeId: emp.id, + }, + }); + await tx.userRole.create({ data: { userId: u.id, roleId } }); + return u; + }; + + const admin = await mkUser('admin', adminHash, emps.admin, roles.admin.id); + await mkUser('owner', passwordHash, emps.owner, roles.owner.id); + await mkUser('bizdir', passwordHash, emps.bizdir, roles.biz_director.id); + await mkUser('bizstaff', passwordHash, emps.bizstaff, roles.biz_staff.id); + const dirsoft = await mkUser('dirsoft', passwordHash, emps.dirsoft, roles.tech_director.id); + const dir3d = await mkUser('dir3d', passwordHash, emps.dir3d, roles.tech_director.id); + await mkUser('dirfilm', passwordHash, emps.dirfilm, roles.tech_director.id); + await mkUser('dirmaterial', passwordHash, emps.dirmaterial, roles.tech_director.id); + await mkUser('finance', passwordHash, emps.finance, roles.finance.id); + await mkUser('hr', passwordHash, emps.hr, roles.hr.id); + await mkUser('employee', passwordHash, emps.employee, roles.employee.id); + await mkUser('softpm', passwordHash, softpm, roles.employee.id); + + const demoEntity = await tx.legalEntity.create({ + data: { name: '演示科技有限公司', shortName: '演示科技', creditNo: '91110000MA0000001X', isDefault: true }, + }); + const party = await tx.businessParty.create({ + data: { name: '某市城投集团', creditNo: '91110000MA9999999X', level: 'A' }, + }); + await tx.partyContact.create({ + data: { partyId: party.id, name: '招标办张工', phone: '13800001111', title: '项目联系人' }, + }); + await tx.qualification.createMany({ + data: [ + { + name: '电子与智能化工程专业承包', + code: 'QUAL_INTEL', + keeper: '行政', + expiresAt: new Date(Date.now() + 400 * 86400000), + }, + { + name: '涉密信息系统集成乙级', + code: 'SECRET_B', + keeper: '行政', + expiresAt: new Date(Date.now() + 20 * 86400000), + }, + { + name: '建筑装修装饰工程专业承包', + code: 'QUAL_DECOR', + keeper: '行政', + expiresAt: new Date(Date.now() - 10 * 86400000), + }, + ], + }); + + const dicts: [string, string, string, number][] = [ + ['projectType', '软件', '软件', 1], + ['projectType', '三维', '三维', 2], + ['projectType', '摄制', '摄制', 3], + ['projectType', '物资', '物资', 4], + ['projectType', '集成', '集成', 5], + ['tenderMethod', '公开招标', '公开招标', 1], + ['tenderMethod', '邀请招标', '邀请招标', 2], + ['tenderMethod', '询价', '询价', 3], + ['tenderMethod', '竞谈【综合评分】', '竞谈【综合评分】', 4], + ['tenderMethod', '竞谈【最低价中】', '竞谈【最低价中】', 5], + ['tenderMethod', '单一来源', '单一来源', 6], + ['tenderMethod', '其他', '其他', 7], + ['qualificationNeed', '无需资质', '无需资质', 1], + ['qualificationNeed', '军二', '军二', 2], + ['qualificationNeed', '涉乙', '涉乙', 3], + ['qualificationNeed', '其他', '其他', 4], + ['expenseCategory', '交通', '交通', 1], + ['expenseCategory', '餐饮', '餐饮', 2], + ['expenseCategory', '住宿', '住宿', 3], + ['expenseCategory', '办公', '办公', 4], + ['expenseCategory', '招待', '招待', 5], + ['expenseCategory', '其他', '其他', 6], + ['gender', '男', 'MALE', 1], + ['gender', '女', 'FEMALE', 2], + ['education', '高中及以下', 'HIGH_SCHOOL', 1], + ['education', '专科', 'COLLEGE', 2], + ['education', '本科', 'BACHELOR', 3], + ['education', '硕士', 'MASTER', 4], + ['education', '博士', 'DOCTOR', 5], + ]; + await tx.dictItem.createMany({ + data: dicts.map(([dictType, label, value, sortNo]) => ({ dictType, label, value, sortNo })), + }); + await tx.numberRule.createMany({ + data: [ + { code: 'BID', name: '投标编号', prefix: 'BID', padLength: 5 }, + { code: 'HT', name: '合同编号', prefix: 'HT', padLength: 5 }, + { code: 'PRJ', name: '项目编号', prefix: 'PRJ', padLength: 5 }, + { code: 'BX', name: '报销编号', prefix: 'BX', padLength: 5 }, + { code: 'JK', name: '借款编号', prefix: 'JK', padLength: 5 }, + { code: 'BZ', name: '保证金编号', prefix: 'BZ', padLength: 5 }, + ], + }); + await tx.hrSetting.create({ + data: { id: 'default', tenantId: '1', idMask: true, autoAccount: false, probationMonths: 3, defaultRest: 'SINGLE', workStart: '09:00', workEnd: '18:00' }, + }); + await tx.attendanceRule.createMany({ + data: [ + { name: '单休班次', restSchedule: 'SINGLE', workStart: '09:00', workEnd: '18:00', sortNo: 1, remark: '每周休一天,薪酬按 26.09 天核算' }, + { name: '双休班次', restSchedule: 'DOUBLE', workStart: '09:00', workEnd: '18:00', sortNo: 2, remark: '每周休两天,薪酬按 21.75 天核算' }, + ], + }); + const sealTypes = ['公章', '合同章', '法人章', '法人签名章', '项目负责人章', '其他章']; + await tx.sealItem.createMany({ + data: sealTypes.map((sealType) => ({ + name: `${demoEntity.shortName}${sealType}`, + sealType, + keeper: '行政', + legalEntityId: demoEntity.id, + status: 'IN_STOCK', + })), + }); + + const bid = await tx.bidCase.create({ + data: { + bidNo: 'BID-2026-00001', + name: '某市展厅数字化升级', + externalNo: 'ZFCG-2026-088', + projectType: '集成', + source: '招标公告', + qualificationNeed: '涉乙', + tenderMethod: '公开招标', + applyMethod: '网上申领', + openMethod: '线下开标', + openPlace: '市公共资源交易中心', + openAt: new Date(Date.now() + 7 * 86400000), + partyId: party.id, + status: 'TECH_INITIAL', + createdById: admin.id, + reviewers: { + create: [ + { userId: dirsoft.id, stage: 'TECH_INITIAL' }, + { userId: dir3d.id, stage: 'TECH_INITIAL' }, + ], + }, + }, + }); + await tx.bidActionLog.create({ + data: { + bidCaseId: bid.id, + actorId: admin.id, + action: 'CREATE', + toStatus: 'TECH_INITIAL', + comment: `发起筛选 ${bid.bidNo}`, + }, + }); + await tx.approvalTask.createMany({ + data: [ + { + title: `技术初审会签:${bid.name}`, + bizType: 'BID_TECH_INITIAL', + bizId: bid.id, + assigneeId: dirsoft.id, + }, + { + title: `技术初审会签:${bid.name}`, + bizType: 'BID_TECH_INITIAL', + bizId: bid.id, + assigneeId: dir3d.id, + }, + ], + }); + }); +} + +main() + .then(async () => { + await prisma.$disconnect(); + console.log('seed ok'); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/sync-menus.ts b/apps/api/prisma/sync-menus.ts new file mode 100644 index 0000000..5e6ab97 --- /dev/null +++ b/apps/api/prisma/sync-menus.ts @@ -0,0 +1,165 @@ +import { PrismaClient } from '@prisma/client'; +import { DIRECTOR_ROLES, WORK_ASSIGN_ROLES } from '../src/common/role-groups'; +import { MENUS, isHrSettingCode, type MenuNode } from './menus'; + +const prisma = new PrismaClient(); + +async function upsertNode(node: MenuNode, parentId: string | null, sortNo: number) { + const existing = await prisma.permission.findUnique({ where: { code: node.code } }); + const data = { + parentId, + name: node.name, + permType: 'MENU', + path: node.path, + icon: node.icon ?? null, + sortNo, + visible: true, + }; + const row = existing + ? await prisma.permission.update({ where: { id: existing.id }, data }) + : await prisma.permission.create({ data: { ...data, code: node.code } }); + return row; +} + +const ACTION_DEFS = [ + { name: '查看', suffix: 'view' }, + { name: '新增', suffix: 'create' }, + { name: '编辑', suffix: 'edit' }, + { name: '删除', suffix: 'delete' }, + { name: '审批', suffix: 'approve' }, +]; + +async function upsertActions(parentCode: string, parentId: string) { + const ids: string[] = []; + for (let i = 0; i < ACTION_DEFS.length; i++) { + const a = ACTION_DEFS[i]; + const code = `${parentCode}:${a.suffix}`; + const existing = await prisma.permission.findUnique({ where: { code } }); + const data = { + parentId, + name: a.name, + permType: 'ACTION', + path: null as string | null, + icon: null as string | null, + sortNo: i + 1, + visible: true, + }; + const row = existing + ? await prisma.permission.update({ where: { id: existing.id }, data }) + : await prisma.permission.create({ data: { ...data, code } }); + ids.push(row.id); + } + return ids; +} + +async function walk(nodes: MenuNode[], parentId: string | null, root = true) { + const ids: string[] = []; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + const row = await upsertNode(node, parentId, root ? (i + 1) * 10 : i + 1); + ids.push(row.id); + if (node.children?.length) ids.push(...(await walk(node.children, row.id, false))); + else ids.push(...(await upsertActions(node.code, row.id))); + } + return ids; +} + +async function main() { + const kept = await walk(MENUS, null); + const stale = await prisma.permission.findMany({ + where: { + permType: 'MENU', + OR: [ + { id: { notIn: kept }, code: { in: ['system:org'] } }, + { code: { in: ['office:loan-approve', 'finance:expense-approve', 'finance:loan-approve', 'finance:bonds', 'contract:milestones', 'office:attendance', 'office:policies', 'project:budgets', 'project:changes'] } }, + ], + }, + }); + if (stale.length) { + await prisma.permission.updateMany({ + where: { id: { in: stale.map((s) => s.id) } }, + data: { visible: false }, + }); + } + + const all = await prisma.permission.findMany({ where: { visible: true } }); + const grantByCodes = (codes: string[]) => { + if (codes[0] === '*') return all; + return all.filter((p) => + codes.some((c) => { + if (c.endsWith(':')) return p.code === c.slice(0, -1) || p.code.startsWith(c); + return p.code === c || p.code.startsWith(`${c}:`); + }), + ); + }; + const STAFF_OFFICE = [ + 'office:overview', + 'office', + 'office:todos', + 'office:approvals', + 'office:calendar', + 'office:reports', + 'office:expenses', + 'office:flow', + 'office:hr-apply', + 'office:seal-apply', + 'office:apply', + 'office:notices', + 'office:directory', + 'office:im', + ]; + const ROLE_GRANT: Record = { + admin: ['*'], + owner: ['*'], + biz_director: ['office:', 'party:', 'seal:', 'bid:', 'contract:', 'finance:', 'report:', 'hr:', 'asset:', 'system:hr', 'system:org', 'system:attendance-rules'], + biz_staff: ['office:', 'party:', 'seal:', 'bid:', 'contract:', 'finance:', 'asset:', 'hr:employees', 'hr:payroll', 'hr:performance', 'hr:attendance', 'hr:lifecycle'], + tech_director: ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'], + rd_director: ['*'], + '3d_director': ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'], + video_director: ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'], + material_director: ['office:', 'bid:', 'project:', 'report:', 'hr:performance', 'hr:notices'], + finance: ['office:', 'finance:', 'contract:', 'report:', 'bid:result', 'asset:'], + hr: ['office:', 'hr:', 'contract:hr', 'system:hr', 'system:org', 'system:attendance-rules', 'system:acl'], + employee: [...STAFF_OFFICE, 'project:timesheets'], + bid_screener: ['office', 'office:overview', 'bid', 'bid:screening'], + pm: [...STAFF_OFFICE, 'office:assign', 'project:'], + rd_staff: [...STAFF_OFFICE, 'project:timesheets'], + '3d_staff': [...STAFF_OFFICE, 'project:timesheets'], + video_staff: [...STAFF_OFFICE, 'project:timesheets'], + material_staff: [...STAFF_OFFICE, 'project:timesheets'], + tech: [...STAFF_OFFICE, 'project:timesheets'], + }; + const roles = await prisma.role.findMany(); + for (const role of roles) { + if (role.aclCustom) continue; + const codes = ROLE_GRANT[role.code]; + if (!codes) continue; + const wanted = grantByCodes(codes); + const extra = role.code === 'hr' ? all.filter((p) => isHrSettingCode(p.code)) : []; + const smsRoles = ['admin', 'owner', 'hr', ...DIRECTOR_ROLES]; + const merged = [...new Map([...wanted, ...extra].map((p) => [p.id, p])).values()].filter((p) => { + if (p.code === 'office:assign') return WORK_ASSIGN_ROLES.includes(role.code); + if (p.code === 'office:sms') return smsRoles.includes(role.code); + return true; + }); + const wantedIds = merged.map((p) => p.id); + await prisma.rolePermission.deleteMany({ + where: { roleId: role.id, permissionId: { notIn: wantedIds.length ? wantedIds : [''] } }, + }); + await prisma.rolePermission.createMany({ + data: merged.map((p) => ({ roleId: role.id, permissionId: p.id })), + skipDuplicates: true, + }); + } + console.log(`menus synced: ${kept.length} visible, hid ${stale.length}`); +} + +main() + .then(async () => { + await prisma.$disconnect(); + }) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/sync-oa-attachments.ts b/apps/api/prisma/sync-oa-attachments.ts new file mode 100644 index 0000000..36cfc1e --- /dev/null +++ b/apps/api/prisma/sync-oa-attachments.ts @@ -0,0 +1,329 @@ +import { PrismaClient } from '@prisma/client'; +import { createHash, randomUUID } from 'crypto'; +import * as fs from 'fs'; +import * as path from 'path'; +import { parseOaJson } from './oa-json'; + +const prisma = new PrismaClient(); +const CORE = '/opt/import/prod/oa_core.json'; +const FLOW = '/opt/import/prod/oa_flow_finance.json'; +const BID_FILES = '/opt/import/prod/oa_bid_files.json'; +const HR = '/opt/import/prod/oa_hr.json'; +const OA_FILES = '/opt/import/prod/oa-files'; +const UPLOAD = path.join(process.cwd(), 'uploads'); + +type Row = Record; + +const MIME: Record = { + pdf: 'application/pdf', + jpg: 'image/jpeg', + jpeg: 'image/jpeg', + png: 'image/png', + gif: 'image/gif', + webp: 'image/webp', + doc: 'application/msword', + docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + xls: 'application/vnd.ms-excel', + xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + zip: 'application/zip', + rar: 'application/x-rar-compressed', +}; + +const NAME_ALIAS: Record = { 张靖妍: '张婧妍' }; +const SKIP_DOC_ROLE = new Set(['draft', 'final', 'review1']); + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +function officialNo(raw: unknown) { + const n = str(raw).replace(/[))]+$/, '').trim(); + if (!n || n === '无' || n === '-' || n === '—') return ''; + return n; +} + +function num(v: unknown) { + if (v == null || v === '') return 0; + const n = Number(String(v).replace(/,/g, '')); + return Number.isFinite(n) ? n : 0; +} + +function profilePaths(v: unknown) { + const out: string[] = []; + if (!v) return out; + for (const chunk of str(v).split(/[,;]/)) { + const piece = chunk.trim(); + const first = piece.split('|')[0].trim(); + const idx = first.indexOf('/profile/upload/'); + const p = idx >= 0 ? first.slice(idx) : first; + if (p.startsWith('/profile/upload/')) out.push(p); + } + return out; +} + +function relOf(p: string) { + return p.replace(/^\/profile\/upload\//, ''); +} + +function displayName(p: string) { + const pipe = str(p).split('|')[1]; + if (pipe && !pipe.includes('/')) return path.basename(pipe); + return path.basename(relOf(p.split('|')[0])); +} + +function mimeOf(name: string) { + const ext = name.split('.').pop()?.toLowerCase() || 'bin'; + return MIME[ext] || 'application/octet-stream'; +} + +function prettyName(name: string) { + try { + return decodeURIComponent(name); + } catch { + return name; + } +} + +async function attach( + bizType: string, + bizId: string, + urls: unknown, + uploadedById: string, + seen: Set, +) { + let n = 0; + let missing = 0; + for (const raw of profilePaths(urls)) { + const rel = relOf(raw); + const src = path.join(OA_FILES, rel); + if (!fs.existsSync(src)) { + console.warn(`[missing-source] ${bizType} ${rel}`); + missing += 1; + continue; + } + const buf = fs.readFileSync(src); + const hash = createHash('sha1').update(buf).digest('hex').slice(0, 16); + const key = `${bizId}:${hash}`; + if (seen.has(key)) continue; + seen.add(key); + const id = randomUUID(); + const ext = (path.extname(rel).replace('.', '') || 'bin').replace(/[^a-zA-Z0-9]/g, '').slice(0, 8); + const storageKey = `OA/${bizType}/${bizId}/${id}.${ext || 'bin'}`; + const dest = path.join(UPLOAD, storageKey); + fs.mkdirSync(path.dirname(dest), { recursive: true }); + fs.copyFileSync(src, dest); + const name = prettyName(displayName(raw) || path.basename(rel)); + await prisma.fileAsset.create({ + data: { + id, + tenantId: '1', + bizType, + bizId, + fileName: name.slice(0, 180), + mimeType: mimeOf(name), + size: buf.length, + storageKey, + uploadedById, + }, + }); + n += 1; + } + return { n, missing }; +} + +async function main() { + const core = parseOaJson>(CORE); + const flow = parseOaJson>(FLOW); + const docFiles = JSON.parse(fs.readFileSync(BID_FILES, 'utf8')) as Row[]; + const hr = JSON.parse(fs.readFileSync(HR, 'utf8')) as { users: { id: number; userName: string; nickName: string }[] }; + const admin = await prisma.user.findFirst({ + where: { OR: [{ username: 'admin' }, { displayName: '系统管理员' }] }, + orderBy: { createdAt: 'asc' }, + }); + if (!admin) throw new Error('没有可用于迁移附件的系统管理员账号'); + fs.mkdirSync(UPLOAD, { recursive: true }); + + const users = await prisma.user.findMany(); + const byUsername = new Map(users.map((u) => [u.username, u])); + const byName = new Map(users.map((u) => [u.displayName, u])); + for (const [a, b] of Object.entries(NAME_ALIAS)) { + const u = byName.get(b); + if (u) byName.set(a, u); + } + const oaUser = new Map(hr.users.map((u) => [String(u.id), u.userName])); + const userFromOaId = (id: unknown) => { + const uname = oaUser.get(str(id)); + return uname ? byUsername.get(uname) : undefined; + }; + const userFromName = (name: unknown) => { + const n = NAME_ALIAS[str(name)] || str(name); + return byName.get(n); + }; + + const existing = await prisma.fileAsset.findMany({ select: { bizId: true, storageKey: true, size: true, fileName: true } }); + const seen = new Set(); + for (const f of existing) { + const src = path.join(process.cwd(), 'uploads', f.storageKey); + if (!fs.existsSync(src)) continue; + const hash = createHash('sha1').update(fs.readFileSync(src)).digest('hex').slice(0, 16); + seen.add(`${f.bizId}:${hash}`); + } + + const bids = await prisma.bidCase.findMany(); + const findBids = (nos: string[], name?: string) => { + const wanted = new Set(nos.filter(Boolean)); + const hits = bids.filter( + (b) => wanted.has(b.bidNo) || (b.externalNo && wanted.has(b.externalNo)), + ); + if (hits.length) return hits; + if (name) return bids.filter((b) => b.name === name); + return []; + }; + + const stats = { + bidAnnounce: 0, + bidFile: 0, + bidProof: 0, + contracts: 0, + expenses: 0, + projects: 0, + purchases: 0, + missing: 0, + unmatchedBids: 0, + }; + + for (const t of core.oa_tender || []) { + const localHits = findBids( + [officialNo(t.project_number), str(t.system_number)], + str(t.project_name), + ); + if (!localHits.length) continue; + const who = userFromOaId(t.create_by) || userFromName(t.bid_prepare_person) || admin; + for (const local of localHits) { + stats.bidAnnounce += (await attach('BID_ANNOUNCE', local.id, t.announcement_file, who.id, seen)).n; + const extra = await attach('BID_FILE', local.id, t.attachment, who.id, seen); + stats.bidFile += extra.n; + stats.missing += extra.missing; + stats.bidProof += (await attach('BID_PROOF', local.id, t.submit_proof_file, who.id, seen)).n; + } + } + + for (const p of flow.project_preinvest_apply || []) { + const nos = [str(p.apply_no), officialNo(p.project_number)]; + const localHits = findBids(nos, str(p.project_name)); + if (!localHits.length) { + if (profilePaths(p.attachment).length || profilePaths(p.business_attachment).length) stats.unmatchedBids += 1; + continue; + } + const who = userFromName(p.applicant_name) || userFromName(p.business_handler_name) || admin; + for (const local of localHits) { + const a = await attach('BID_ANNOUNCE', local.id, p.attachment, who.id, seen); + const b = await attach('BID_FILE', local.id, p.business_attachment, who.id, seen); + const c = await attach('BID_PROOF', local.id, p.material_decision_proof, who.id, seen); + const d = await attach('BID_FILE', local.id, p.business_prep_attachment, who.id, seen); + stats.bidAnnounce += a.n; + stats.bidFile += b.n + d.n; + stats.bidProof += c.n; + stats.missing += a.missing + b.missing + c.missing + d.missing; + } + } + + const taskById = new Map((flow.bid_document_task || []).map((t) => [String(t.id), t])); + for (const f of docFiles) { + if (SKIP_DOC_ROLE.has(str(f.file_role))) continue; + const task = taskById.get(str(f.task_id)); + const nos = task + ? [str(task.task_no), officialNo(task.project_number), str(task.preinvest_apply_no)] + : []; + const localHits = findBids(nos, str(task?.project_name)); + if (!localHits.length) { + stats.unmatchedBids += 1; + continue; + } + const who = userFromName(f.uploaded_by_name) || userFromName(task?.dispatcher_name) || admin; + const role = str(f.file_role); + const from = str(f.source_preinvest_field); + const bizType = + role === 'submit_proof' || from === 'material_decision_proof' + ? 'BID_PROOF' + : from === 'attachment' + ? 'BID_ANNOUNCE' + : 'BID_FILE'; + for (const local of localHits) { + const r = await attach(bizType, local.id, f.file_url, who.id, seen); + if (bizType === 'BID_ANNOUNCE') stats.bidAnnounce += r.n; + else if (bizType === 'BID_PROOF') stats.bidProof += r.n; + else stats.bidFile += r.n; + stats.missing += r.missing; + } + } + + const contracts = await prisma.contract.findMany(); + const contractByName = new Map(contracts.map((c) => [c.name, c])); + for (const c of core.oa_contract || []) { + const local = contractByName.get(str(c.contract_name)); + if (!local) continue; + const who = userFromOaId(c.create_by) || userFromOaId(c.creator_id) || admin; + stats.contracts += (await attach('CONTRACT', local.id, c.contract_original_url, who.id, seen)).n; + stats.contracts += (await attach('CONTRACT_ACCEPT', local.id, c.acceptance_url, who.id, seen)).n; + stats.contracts += (await attach('CONTRACT_AWARD', local.id, c.award_notice_url, who.id, seen)).n; + } + + const expenses = await prisma.expenseClaim.findMany(); + for (const r of flow.reimbursement || core.reimbursement || []) { + const amount = num(r.total_amount); + const name = str(r.employee_name); + const claimNo = str(r.reimburse_no); + const oaId = str(r.id); + if (!oaId) continue; + let hits = claimNo ? expenses.filter((e) => e.claimNo === claimNo) : []; + if (!hits.length) { + hits = expenses.filter((e) => Math.abs(Number(e.amount) - amount) < 0.009 && name && (e.remark || '').includes(name)); + } + const local = hits[0]; + if (!local) continue; + const who = userFromName(name) || users.find((u) => u.id === local.applicantId) || admin; + const items = (flow.reimbursement_item || core.reimbursement_item || []).filter( + (i) => str(i.reimbursement_id) && str(i.reimbursement_id) === oaId, + ); + for (const item of items) { + const x = await attach('EXPENSE', local.id, item.url, who.id, seen); + stats.expenses += x.n; + stats.missing += x.missing; + } + const x = await attach('EXPENSE', local.id, r.attachment_url, who.id, seen); + stats.expenses += x.n; + stats.missing += x.missing; + } + + const projects = await prisma.project.findMany(); + const projectByName = new Map(projects.map((p) => [p.name, p])); + for (const p of core.project_list || []) { + const local = + projects.find((x) => x.projectNo === str(p.system_number)) || projectByName.get(str(p.project_name)); + if (!local) continue; + stats.projects += (await attach('PROJECT', local.id, p.bzj_attachment, admin.id, seen)).n; + } + + const purchases = await prisma.purchaseRequest.findMany(); + for (const r of core.oa_purchasing || []) { + const titleBits = [str(r.contract_no), str(r.contract_name)].filter(Boolean); + const local = purchases.find((p) => titleBits.some((b) => p.title.includes(b))); + if (!local) continue; + const who = userFromOaId(r.create_by) || userFromOaId(r.creator_id) || admin; + stats.purchases += (await attach('PURCHASE', local.id, r.contract_original_url, who.id, seen)).n; + stats.purchases += (await attach('PURCHASE_ACCEPT', local.id, r.acceptance_url, who.id, seen)).n; + } + + const fileCount = await prisma.fileAsset.count(); + const byType = await prisma.fileAsset.groupBy({ by: ['bizType'], _count: true }); + console.log(JSON.stringify({ added: stats, fileAssetTotal: fileCount, byType }, null, 2)); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/sync-tender-methods.ts b/apps/api/prisma/sync-tender-methods.ts new file mode 100644 index 0000000..6fbf14e --- /dev/null +++ b/apps/api/prisma/sync-tender-methods.ts @@ -0,0 +1,35 @@ +import { PrismaClient } from '@prisma/client'; + +const ITEMS = [ + ['公开招标', 1], + ['邀请招标', 2], + ['询价', 3], + ['竞谈【综合评分】', 4], + ['竞谈【最低价中】', 5], + ['单一来源', 6], + ['其他', 7], +] as const; + +const prisma = new PrismaClient(); + +async function main() { + await prisma.dictItem.deleteMany({ where: { tenantId: '1', dictType: 'tenderMethod' } }); + await prisma.dictItem.createMany({ + data: ITEMS.map(([value, sortNo]) => ({ + tenantId: '1', + dictType: 'tenderMethod', + label: value, + value, + sortNo, + })), + }); + console.log('tenderMethod dict synced', ITEMS.length); +} + +main() + .then(() => prisma.$disconnect()) + .catch(async (e) => { + console.error(e); + await prisma.$disconnect(); + process.exit(1); + }); diff --git a/apps/api/prisma/wash-bid-buckets.ts b/apps/api/prisma/wash-bid-buckets.ts new file mode 100644 index 0000000..9030f21 --- /dev/null +++ b/apps/api/prisma/wash-bid-buckets.ts @@ -0,0 +1,131 @@ +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; + +const prisma = new PrismaClient(); +const DUMP = '/opt/import/prod/oa_core.json'; + +type Row = Record; + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +function officialNo(raw: unknown) { + const n = str(raw).replace(/[))]+$/, '').trim(); + if (!n || n === '无' || n === '-' || n === '—') return ''; + return n; +} + +function mapQual(raw: unknown) { + const s = str(raw); + if (!s) return ''; + try { + const arr = JSON.parse(s) as { item?: string }[]; + const item = str(arr?.[0]?.item); + if (!item || item === '无') return '无需资质'; + if (item.includes('军')) return '军二'; + if (item.includes('涉')) return '涉乙'; + return item.slice(0, 20); + } catch { + if (s.includes('军')) return '军二'; + if (s.includes('涉')) return '涉乙'; + return ''; + } +} + +function mapStatus(row: Row) { + const st = str(row.project_status); + const win = str(row.winning_status); + if (st === '中标' || (st === '' && win.includes('中标'))) return 'WON'; + if (st === '未中' || (st === '' && win.includes('未中'))) return 'LOST'; + if (st === '流标') return 'FAILED'; + if (st === '弃标' || win.includes('弃标')) return 'TERMINATED'; + return ''; +} + +function isRealTender(row: Row) { + return /^TB-\d/.test(str(row.system_number)) && Boolean(str(row.project_name)); +} + +async function main() { + const raw = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Record; + const tenders = (raw.oa_tender || []).filter(isRealTender); + const bids = await prisma.bidCase.findMany(); + const users = await prisma.user.findMany({ select: { id: true, displayName: true } }); + const userByName = new Map(users.map((u) => [u.displayName, u.id])); + userByName.set('张靖妍', userByName.get('张婧妍') || ''); + + const testNos = ['SP-TEST-1788068192', 'SP-FIX-1788068273']; + const tests = bids.filter((b) => testNos.includes(b.bidNo) || testNos.includes(b.externalNo || '')); + const testIds = tests.map((b) => b.id); + if (testIds.length) { + await prisma.todoTask.deleteMany({ where: { bizId: { in: testIds } } }); + await prisma.approvalTask.deleteMany({ where: { bizId: { in: testIds } } }); + await prisma.bidCase.deleteMany({ where: { id: { in: testIds } } }); + } + + const used = new Set(); + const remaining = await prisma.bidCase.findMany(); + let patched = 0; + const moves: { name: string; from: string; to: string; no: string }[] = []; + + for (const t of tenders) { + const name = str(t.project_name); + const sys = str(t.system_number); + const official = officialNo(t.project_number); + const row = + remaining.find((b) => b.name === name) || + remaining.find((b) => b.bidNo === sys || b.externalNo === official || b.externalNo === str(t.project_number)); + if (!row) continue; + + const status = mapStatus(t) || row.status; + const creatorName = str(t.bid_prepare_person) === '张靖妍' ? '张婧妍' : str(t.bid_prepare_person); + let bidNo = official || sys; + if (used.has(bidNo)) bidNo = `${official || sys}-${sys.slice(-3)}`; + used.add(bidNo); + + if (row.status !== status) { + moves.push({ name: name.slice(0, 24), from: row.status, to: status, no: official || sys }); + } + + const qual = mapQual(t.bid_doc_requirement); + await prisma.bidCase.update({ + where: { id: row.id }, + data: { + bidNo: `TMP-${row.id.replace(/-/g, '').slice(0, 18)}`, + }, + }); + await prisma.bidCase.update({ + where: { id: row.id }, + data: { + bidNo, + externalNo: official || null, + status, + sourceType: 'FLOW', + qualificationNeed: qual || row.qualificationNeed, + tenderMethod: str(t.tender_method) || row.tenderMethod, + applyMethod: row.applyMethod || (str(t.announcement_file) ? '网上申领' : row.applyMethod), + delivererName: str(t.bid_delivery_person) || row.delivererName, + createdById: userByName.get(creatorName) || row.createdById, + }, + }); + patched += 1; + } + + const after = await prisma.bidCase.groupBy({ by: ['status'], _count: true }); + console.log( + JSON.stringify( + { deletedTests: testIds.length, patched, moves, counts: after }, + null, + 2, + ), + ); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/wash-legal-entities.ts b/apps/api/prisma/wash-legal-entities.ts new file mode 100644 index 0000000..40ffbbf --- /dev/null +++ b/apps/api/prisma/wash-legal-entities.ts @@ -0,0 +1,142 @@ +import { PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; + +const prisma = new PrismaClient(); +const DUMP = '/opt/import/prod/oa_clean.json'; + +type Row = Record; + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +const CANON = [ + { name: '江苏风影随行科技有限公司', shortName: '随行科技', aliases: ['江苏风影随行科技有限公司', '江苏风影随行', '随行科技'] }, + { name: '江苏风影软件有限公司', shortName: '风影软件', aliases: ['江苏风影软件有限公司', '风影软件'] }, + { name: '江苏风影工程科技有限公司', shortName: '风影工程', aliases: ['江苏风影工程科技有限公司', '风影工程'] }, + { name: '山东鲁兵', shortName: '山东鲁兵', aliases: ['山东鲁兵', '鲁兵'] }, + { name: '隆创', shortName: '隆创', aliases: ['隆创'] }, +] as const; + +function canonOf(raw: string) { + const s = str(raw); + if (!s) return null; + if (/软件/.test(s) && /风影/.test(s)) return CANON[1]; + if (/工程/.test(s) && /风影/.test(s)) return CANON[2]; + if (/随行/.test(s) || s === '江苏风影' || (s.includes('风影') && !/软件|工程/.test(s))) return CANON[0]; + if (s.includes('鲁兵')) return CANON[3]; + if (s.includes('隆创')) return CANON[4]; + for (const c of CANON) { + if (c.aliases.some((a) => s === a || s.includes(a))) return c; + } + return null; +} + +async function main() { + const ids = new Map(); + for (const c of CANON) { + let row = + (await prisma.legalEntity.findFirst({ where: { tenantId: '1', name: c.name } })) || + (await prisma.legalEntity.findFirst({ + where: { tenantId: '1', OR: c.aliases.map((a) => ({ name: a })) }, + })); + if (row) { + row = await prisma.legalEntity.update({ + where: { id: row.id }, + data: { name: c.name, shortName: c.shortName, isDefault: c.name === '江苏风影随行科技有限公司' }, + }); + } else { + row = await prisma.legalEntity.create({ + data: { + name: c.name, + shortName: c.shortName, + isDefault: c.name === '江苏风影随行科技有限公司', + }, + }); + } + ids.set(c.name, row.id); + } + + const all = await prisma.legalEntity.findMany({ where: { tenantId: '1' } }); + const keep = new Set(ids.values()); + let merged = 0; + for (const extra of all) { + if (keep.has(extra.id)) continue; + const hit = canonOf(extra.name) || canonOf(extra.shortName); + const target = hit ? ids.get(hit.name) : ids.get('江苏风影随行科技有限公司'); + if (!target || target === extra.id) continue; + const n = await prisma.bidCase.updateMany({ where: { legalEntityId: extra.id }, data: { legalEntityId: target } }); + await prisma.contract.updateMany({ where: { legalEntityId: extra.id }, data: { legalEntityId: target } }).catch(() => null); + await prisma.sealItem.updateMany({ where: { legalEntityId: extra.id }, data: { legalEntityId: target } }).catch(() => null); + await prisma.registeredAsset.updateMany({ where: { legalEntityId: extra.id }, data: { legalEntityId: target } }).catch(() => null); + await prisma.legalEntity.delete({ where: { id: extra.id } }).catch(() => null); + merged += 1; + console.log('merged', extra.name, '->', hit?.name, 'bids', n.count); + } + + await prisma.legalEntity.updateMany({ + where: { name: { not: '江苏风影随行科技有限公司' } }, + data: { isDefault: false }, + }); + await prisma.legalEntity.updateMany({ + where: { name: '江苏风影随行科技有限公司' }, + data: { isDefault: true }, + }); + + let remapped = 0; + if (fs.existsSync(DUMP)) { + const raw = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Record; + const tenders = raw.oa_tender || []; + const bids = await prisma.bidCase.findMany({ select: { id: true, name: true, bidNo: true, externalNo: true } }); + for (const t of tenders) { + const name = str(t.project_name); + const sys = str(t.system_number); + const official = str(t.project_number); + const row = + bids.find((b) => b.name === name) || + bids.find((b) => b.bidNo === sys || b.externalNo === official || b.bidNo === official); + const hit = canonOf(str(t.participating_unit)); + const legalEntityId = hit ? ids.get(hit.name) : undefined; + const winner = str(t.winning_unit); + const amtRaw = str(t.winning_amount); + const status = str(t.winning_status); + if (!row) continue; + const data: { legalEntityId?: string; winnerName?: string; winnerAmount?: number } = {}; + if (legalEntityId) data.legalEntityId = legalEntityId; + if (winner && status && !/中标/.test(status)) { + data.winnerName = winner; + if (amtRaw && !Number.isNaN(Number(amtRaw))) data.winnerAmount = Number(amtRaw); + } + if (!Object.keys(data).length) continue; + await prisma.bidCase.update({ where: { id: row.id }, data }); + remapped += 1; + } + } + + const leftover = await prisma.legalEntity.findMany({ where: { tenantId: '1' }, orderBy: { name: 'asc' } }); + const grouped = await prisma.bidCase.groupBy({ by: ['legalEntityId'], _count: true }); + console.log( + JSON.stringify( + { + entities: leftover.map((e) => ({ name: e.name, shortName: e.shortName, isDefault: e.isDefault })), + bids: grouped.map((g) => ({ + id: g.legalEntityId, + count: g._count, + name: leftover.find((e) => e.id === g.legalEntityId)?.name || '(空)', + })), + merged, + remapped, + }, + null, + 2, + ), + ); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/wash-oa-flow-finance.ts b/apps/api/prisma/wash-oa-flow-finance.ts new file mode 100644 index 0000000..053cd07 --- /dev/null +++ b/apps/api/prisma/wash-oa-flow-finance.ts @@ -0,0 +1,466 @@ +import { Prisma, PrismaClient } from '@prisma/client'; +import { mapExpenseLineKind, parseOaJson } from './oa-json'; + +const prisma = new PrismaClient(); +const FLOW = '/opt/import/prod/oa_flow_finance.json'; +const FRESH = '/opt/import/prod/oa_fresh.json'; + +type Row = Record; + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +function num(v: unknown) { + if (v == null || v === '') return 0; + const n = Number(String(v).replace(/,/g, '')); + return Number.isFinite(n) ? n : 0; +} + +function when(v: unknown) { + const s = str(v); + if (!s || s.startsWith('0000')) return null; + const d = new Date(s.includes('T') ? s : s.replace(' ', 'T')); + return Number.isNaN(d.getTime()) ? null : d; +} + +function clip(v: unknown, n = 200) { + const s = str(v); + return s.length > n ? s.slice(0, n) : s; +} + +function compactName(name: string) { + return str(name) + .replace(/[()()\[\]【】\s\-—_.,,、]/g, '') + .replace(/招标公告|采购项目|技术服务合同|合同/g, ''); +} + +function namesOverlap(a: string, b: string) { + const na = compactName(a); + const nb = compactName(b); + if (!na || !nb) return false; + if (na === nb) return true; + const shorter = na.length <= nb.length ? na : nb; + const longer = na.length <= nb.length ? nb : na; + return shorter.length >= 10 && longer.includes(shorter); +} + +function mapProjectType(v: unknown, projectName?: unknown) { + const name = str(projectName); + if (name.includes('三维')) return '三维'; + if (/(视频.{0,12}(制作|摄制|拍摄)|摄制|拍摄|摄影|宣传片|专题片|纪录片|微电影|影视制作)/.test(name)) return '摄制'; + const s = str(v); + if (s.includes('三维')) return '三维'; + if (s.includes('摄制') || s.includes('视频')) return '摄制'; + if (s.includes('物资') && s.includes('软件')) return '集成'; + if (s.includes('物资')) return '物资'; + if (s.includes('集成')) return '集成'; + if (s === '软件') return '软件'; + return '软件'; +} + +function mapDocStatus(stage: number, approval: number) { + if (stage === 8 || approval === 3) return 'TERMINATED'; + if (stage === 1 || stage === 2 || stage === 3 || stage === 4 || stage === 5 || stage === 6) return 'DRAFTING'; + if (stage === 7 && approval === 2) return 'PENDING'; + return 'DRAFTING'; +} + +function mapPreinvestStatus(stage: number, approval: number) { + if (stage === 6 || approval === 3) return 'TERMINATED'; + if (stage === 3) return 'TECH_FINAL'; + if (stage === 1) return 'TECH_INITIAL'; + if (stage === 5 && approval === 2) return ''; + return 'FETCH'; +} + +function mapExpenseStatus(v: unknown) { + const s = str(v); + if (s === '2') return 'APPROVED'; + if (s === '3') return 'REJECTED'; + if (s === '4') return 'DRAFT'; + return 'PENDING'; +} + +function mapLoanStatus(row: Row) { + const st = str(row.approval_status); + const offset = num(row.offset_amount); + if (st === '2') return offset >= num(row.amount) && offset > 0 ? 'OFFSET' : 'OPEN'; + if (st === '1') return 'PENDING'; + if (st === '3') return 'REJECTED'; + return 'DRAFT'; +} + +function mapItemKind(itemType: string, remark = '') { + return mapExpenseLineKind(itemType, remark); +} + +function mapItemCategory(itemType: string) { + if (itemType.includes('交通') || itemType.includes('差旅') || itemType.includes('机票') || itemType.includes('高铁')) return '交通'; + if (itemType.includes('餐')) return '餐饮'; + if (itemType.includes('住宿')) return '住宿'; + if (itemType.includes('办公')) return '办公'; + if (itemType.includes('招待')) return '招待'; + return '其他'; +} + +function isBidExpense(itemType: string) { + return itemType.includes('投标') || itemType.includes('制标') || itemType.includes('取标') || itemType.includes('印'); +} + +function officialNo(raw: unknown) { + const n = str(raw).replace(/[))]+$/, '').trim(); + if (!n || n === '无' || n === '-' || n === '—') return ''; + return n; +} + +function inferTenderMethod(name: string, stored?: string) { + const s = str(stored); + if (s) return s; + const n = name || ''; + if (n.includes('询价')) return '询价'; + if (n.includes('邀请招标')) return '邀请招标'; + if (n.includes('单一来源')) return '单一来源'; + if (n.includes('竞谈最低') || n.includes('竞争性谈判(最低') || n.includes('竞争性谈判(最低')) return '竞谈最低价'; + if (n.includes('竞谈综合') || n.includes('综合评审')) return '竞谈综合评审'; + if (n.includes('竞争性谈判') || n.includes('竞谈')) return '竞争性谈判'; + if (n.includes('竞争性磋商') || n.includes('磋商')) return '竞争性磋商'; + if (n.includes('公开招标')) return '公开招标'; + return ''; +} + +async function main() { + const flow = parseOaJson>(FLOW); + const fresh = parseOaJson>(FRESH); + const admin = await prisma.user.findFirst({ where: { username: 'admin' } }); + if (!admin) throw new Error('本地没有 admin'); + const entity = + (await prisma.legalEntity.findFirst({ where: { tenantId: '1', isDefault: true } })) || + (await prisma.legalEntity.findFirst({ where: { tenantId: '1' } })); + if (!entity) throw new Error('本地没有主体'); + + const users = await prisma.user.findMany({ select: { id: true, displayName: true, username: true } }); + const userByName = new Map(users.map((u) => [u.displayName, u.id])); + userByName.set('张靖妍', userByName.get('张婧妍') || ''); + + const entities = await prisma.legalEntity.findMany(); + const entityByName = new Map(entities.map((e) => [e.name, e.id])); + for (const e of entities) if (e.shortName) entityByName.set(e.shortName, e.id); + + const projects = await prisma.project.findMany({ select: { id: true, name: true, projectNo: true } }); + const projectByName = new Map(projects.map((p) => [p.name, p.id])); + const projectByOaId = new Map(); + for (const p of fresh.project_list || []) { + const local = projects.find((x) => x.projectNo === str(p.system_number) || namesOverlap(x.name, str(p.project_name))); + if (local && p.project_id != null) projectByOaId.set(String(p.project_id), local.id); + } + + const tenders = (fresh.oa_tender || []).filter((t) => /^TB-\d/.test(str(t.system_number))); + const tenderById = new Map(tenders.map((t) => [String(t.id), t])); + + const allPreinvests = flow.project_preinvest_apply || []; + const preByTask = new Map(); + const preByTender = new Map(); + const preByName = new Map(); + for (const p of [...allPreinvests].sort((a, b) => num(a.deleted) - num(b.deleted))) { + if (p.bid_document_task_id != null) preByTask.set(String(p.bid_document_task_id), p); + if (p.tender_id != null) preByTender.set(String(p.tender_id), p); + if (str(p.project_name) && !preByName.has(str(p.project_name))) preByName.set(str(p.project_name), p); + } + + const bids = await prisma.bidCase.findMany(); + const usedNos = new Set(bids.map((b) => b.bidNo)); + + function findBid(_name: string, nos: string[]) { + const wanted = new Set(nos.filter(Boolean)); + if (!wanted.size) return undefined; + return bids.find((b) => wanted.has(b.bidNo) || (b.externalNo && wanted.has(b.externalNo))); + } + + let applyPatched = 0; + for (const t of tenders) { + const pre = preByTender.get(String(t.id)) || preByName.get(str(t.project_name)); + const start = when(pre?.file_apply_time_start) || when(t.bid_prepare_date) || when(t.create_time); + const end = when(pre?.file_apply_time_end) || when(t.file_apply_deadline); + const row = findBid(str(t.project_name), [officialNo(t.project_number), str(t.system_number)]); + if (!row || (!start && !end)) continue; + await prisma.bidCase.update({ + where: { id: row.id }, + data: { + applyStartAt: start || row.applyStartAt, + applyEndAt: end || row.applyEndAt, + applyMethod: clip(pre?.apply_method, 50) || row.applyMethod, + openMethod: clip(pre?.bid_open_method, 50) || row.openMethod, + }, + }); + applyPatched += 1; + } + + async function ensureBid(opts: { + bidNo: string; + name: string; + official?: string; + status: string; + openAt?: Date | null; + openPlace?: string; + applyStart?: Date | null; + applyEnd?: Date | null; + applyMethod?: string; + openMethod?: string; + projectType?: string; + source?: string; + qualification?: string; + entityName?: string; + createdAt?: Date | null; + createdByName?: string; + tenderMethod?: string; + }) { + if (!opts.name || /^test/i.test(opts.name)) return 'skip'; + const existing = findBid(opts.name, [opts.bidNo, opts.official || '']); + if (existing) { + const result = ['WON', 'LOST', 'FAILED'].includes(existing.status); + const sameNo = existing.bidNo === opts.bidNo; + if (opts.status === 'TERMINATED' && !sameNo && !result && existing.status !== 'TERMINATED') { + // 同名在途单不因回收站记录被误杀,下面另建终止单 + } else { + const nextStatus = result ? existing.status : opts.status === 'TERMINATED' || existing.status === 'TERMINATED' ? 'TERMINATED' : opts.status; + await prisma.bidCase.update({ + where: { id: existing.id }, + data: { + applyStartAt: opts.applyStart || existing.applyStartAt, + applyEndAt: opts.applyEnd || existing.applyEndAt, + applyMethod: opts.applyMethod || existing.applyMethod, + openMethod: opts.openMethod || existing.openMethod, + openAt: opts.openAt || existing.openAt, + openPlace: opts.openPlace || existing.openPlace, + projectType: mapProjectType(opts.projectType, opts.name) || existing.projectType, + tenderMethod: inferTenderMethod(opts.name, opts.tenderMethod) || existing.tenderMethod, + status: nextStatus, + }, + }); + return 'updated'; + } + } + let bidNo = opts.bidNo; + if (!bidNo || usedNos.has(bidNo)) bidNo = opts.official || opts.bidNo; + if (usedNos.has(bidNo)) bidNo = `${opts.bidNo}-${Date.now().toString().slice(-4)}`; + usedNos.add(bidNo); + const created = await prisma.bidCase.create({ + data: { + bidNo, + name: clip(opts.name, 200), + externalNo: opts.official || undefined, + projectType: mapProjectType(opts.projectType, opts.name), + sourceType: 'FLOW', + source: clip(opts.source, 50) || undefined, + qualificationNeed: clip(opts.qualification, 40) || undefined, + legalEntityId: (opts.entityName && entityByName.get(opts.entityName)) || entity.id, + applyStartAt: opts.applyStart || undefined, + applyEndAt: opts.applyEnd || undefined, + applyMethod: clip(opts.applyMethod, 50) || undefined, + openAt: opts.openAt || undefined, + openMethod: clip(opts.openMethod, 50) || undefined, + openPlace: clip(opts.openPlace, 200) || undefined, + tenderMethod: inferTenderMethod(opts.name, opts.tenderMethod) || undefined, + status: opts.status, + createdById: userByName.get(opts.createdByName || '') || admin.id, + createdAt: opts.createdAt || undefined, + }, + }); + bids.push(created); + return 'created'; + } + + const created = { drafting: 0, screening: 0, terminated: 0, other: 0, updated: 0, skipped: 0 }; + const tasks = (flow.bid_document_task || []).filter((t) => str(t.project_name)); + for (const t of tasks) { + const pre = preByTask.get(String(t.id)) || preByName.get(str(t.project_name)); + const tender = t.tender_id != null ? tenderById.get(String(t.tender_id)) : undefined; + const deleted = num(t.is_deleted) === 1; + const status = deleted + ? 'TERMINATED' + : tender + ? undefined + : mapDocStatus(num(t.workflow_stage), num(t.approval_status)); + if (!status && tender) { + const r = await ensureBid({ + bidNo: str(t.task_no), + name: str(t.project_name), + official: officialNo(t.project_number) || officialNo(tender.project_number), + status: 'PENDING', + openAt: when(t.bid_open_time) || when(tender.bid_open_date), + openPlace: clip(t.bid_open_location, 200), + applyStart: when(pre?.file_apply_time_start) || when(tender.bid_prepare_date), + applyEnd: when(pre?.file_apply_time_end) || when(t.file_apply_time_end) || when(tender.file_apply_deadline), + applyMethod: str(pre?.apply_method), + openMethod: str(pre?.bid_open_method), + projectType: str(pre?.screening_category || t.screening_category || t.project_type || tender.project_type), + source: str(t.project_source || pre?.project_source), + qualification: str(pre?.qualification_require), + entityName: str(pre?.bid_entity), + createdAt: when(t.created_time), + createdByName: str(t.dispatcher_name), + tenderMethod: str(t.bid_method || tender.tender_method), + }); + if (r === 'updated') created.updated += 1; + else if (r === 'skip') created.skipped += 1; + continue; + } + const next = status || 'DRAFTING'; + const r = await ensureBid({ + bidNo: str(t.task_no), + name: str(t.project_name), + official: officialNo(t.project_number), + status: next, + openAt: when(t.bid_open_time), + openPlace: clip(t.bid_open_location, 200), + applyStart: when(pre?.file_apply_time_start), + applyEnd: when(pre?.file_apply_time_end) || when(t.file_apply_time_end), + applyMethod: str(pre?.apply_method), + openMethod: str(pre?.bid_open_method), + projectType: str(pre?.screening_category || t.screening_category || t.project_type || pre?.project_type), + source: str(t.project_source || pre?.project_source), + qualification: str(pre?.qualification_require), + entityName: str(pre?.bid_entity), + createdAt: when(t.created_time), + createdByName: str(t.dispatcher_name), + tenderMethod: str(t.bid_method), + }); + if (r === 'created') { + if (next === 'DRAFTING') created.drafting += 1; + else if (next === 'TERMINATED') created.terminated += 1; + else created.other += 1; + } else if (r === 'updated') created.updated += 1; + else created.skipped += 1; + } + + for (const p of allPreinvests) { + if (!str(p.project_name) || !str(p.apply_no)) continue; + const deleted = num(p.deleted) === 1; + const status = deleted ? 'TERMINATED' : mapPreinvestStatus(num(p.workflow_stage), num(p.approval_status)); + if (!status) continue; + const r = await ensureBid({ + bidNo: str(p.apply_no), + name: str(p.project_name), + official: officialNo(p.project_number), + status, + openAt: when(p.bid_open_time), + openPlace: clip(p.bid_open_location, 200), + applyStart: when(p.file_apply_time_start), + applyEnd: when(p.file_apply_time_end), + applyMethod: str(p.apply_method), + openMethod: str(p.bid_open_method), + projectType: str(p.screening_category || p.project_type), + source: str(p.project_source || p.entry_source), + qualification: str(p.qualification_require), + entityName: str(p.bid_entity), + createdAt: when(p.created_time), + createdByName: str(p.applicant_name), + tenderMethod: str(p.bid_method), + }); + if (r === 'created') { + if (status === 'TERMINATED') created.terminated += 1; + else created.screening += 1; + } else if (r === 'updated') created.updated += 1; + else created.skipped += 1; + } + + const itemsByReimb = new Map(); + for (const item of flow.reimbursement_item || []) { + const key = String(item.reimbursement_id); + if (!itemsByReimb.has(key)) itemsByReimb.set(key, []); + itemsByReimb.get(key)!.push(item); + } + + let expensesUpserted = 0; + for (const r of flow.reimbursement || []) { + const claimNo = str(r.reimburse_no); + if (!claimNo) continue; + const lines = itemsByReimb.get(String(r.id)) || []; + const main = [...lines].sort((a, b) => num(b.amount) - num(a.amount))[0]; + const itemType = str(main?.item_type); + const applicantId = userByName.get(str(r.employee_name)) || admin.id; + const payerId = userByName.get(str(r.payer_name)) || undefined; + const projectId = r.project_id != null ? projectByOaId.get(String(r.project_id)) : projectByName.get(str(r.project_name)); + const bidHit = findBid(str(r.project_name), []); + const bidCaseId = isBidExpense(itemType) || (!projectId && bidHit) ? bidHit?.id : undefined; + const costType = bidCaseId ? 'BID' : projectId ? 'PROJECT' : 'DEPT'; + const data = { + applicantId, + costType, + budgetCategory: costType === 'PROJECT' ? 'TRAVEL' : undefined, + projectId: costType === 'PROJECT' ? projectId : undefined, + bidCaseId: costType === 'BID' ? bidCaseId : undefined, + expenseCategory: mapItemCategory(itemType), + purpose: clip(r.subject || r.project_name, 200) || undefined, + amount: new Prisma.Decimal(num(r.total_amount)), + status: mapExpenseStatus(r.approval_status), + remark: clip(r.remark, 1000) || undefined, + submittedAt: when(r.created_time) || undefined, + decidedAt: str(r.approval_status) === '2' ? when(r.updated_time) || when(r.created_time) : undefined, + decidedById: str(r.approval_status) === '2' ? admin.id : undefined, + prepaid: Boolean(payerId && payerId !== applicantId), + payerId: payerId && payerId !== applicantId ? payerId : undefined, + }; + const existed = await prisma.expenseClaim.findFirst({ where: { tenantId: '1', claimNo } }); + const row = existed + ? await prisma.expenseClaim.update({ where: { id: existed.id }, data }) + : await prisma.expenseClaim.create({ data: { claimNo, ...data } }); + await prisma.expenseLine.deleteMany({ where: { claimId: row.id } }); + for (const [i, line] of lines.entries()) { + await prisma.expenseLine.create({ + data: { + claimId: row.id, + kind: mapItemKind(str(line.item_type), str(line.remark)), + amount: new Prisma.Decimal(num(line.amount)), + invoiceNo: clip(line.invoice_no, 80) || undefined, + occurAt: when(line.expense_date) || undefined, + remark: clip(line.remark || line.item_type, 200) || undefined, + sortNo: num(line.sort_order) || i, + }, + }); + } + expensesUpserted += 1; + } + + let loansUpserted = 0; + for (const r of flow.borrow_advance || []) { + const loanNo = str(r.advance_no); + if (!loanNo) continue; + const applicantId = userByName.get(str(r.employee_name)) || admin.id; + const projectId = r.project_id != null ? projectByOaId.get(String(r.project_id)) : projectByName.get(str(r.project_name)); + const data = { + applicantId, + costType: projectId ? 'PROJECT' : 'DEPT', + projectId, + amount: new Prisma.Decimal(num(r.amount)), + offsetAmount: new Prisma.Decimal(num(r.offset_amount)), + purpose: clip(r.reason || r.subject, 200) || 'OA 借款', + status: mapLoanStatus(r), + submittedAt: when(r.apply_time) || when(r.created_time) || undefined, + }; + const existed = await prisma.loanRecord.findFirst({ where: { tenantId: '1', loanNo } }); + if (existed) await prisma.loanRecord.update({ where: { id: existed.id }, data }); + else await prisma.loanRecord.create({ data: { loanNo, ...data } }); + loansUpserted += 1; + } + + const bidCounts = await prisma.bidCase.groupBy({ by: ['status'], _count: true }); + const expCounts = await prisma.expenseClaim.groupBy({ by: ['status'], _count: true }); + const loanCounts = await prisma.loanRecord.groupBy({ by: ['status'], _count: true }); + console.log( + JSON.stringify( + { applyPatched, created, expensesUpserted, loansUpserted, bidCounts, expCounts, loanCounts }, + null, + 2, + ), + ); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/wash-oa-ledger.ts b/apps/api/prisma/wash-oa-ledger.ts new file mode 100644 index 0000000..192dd07 --- /dev/null +++ b/apps/api/prisma/wash-oa-ledger.ts @@ -0,0 +1,563 @@ +import { Prisma, PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; + +const prisma = new PrismaClient(); +const DUMP = '/opt/import/prod/oa_fresh.json'; + +type Row = Record; + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +function num(v: unknown) { + if (v == null || v === '') return 0; + const n = Number(String(v).replace(/,/g, '')); + return Number.isFinite(n) ? n : 0; +} + +function when(v: unknown) { + const s = str(v); + if (!s || s.startsWith('0000')) return null; + const d = new Date(s.includes('T') ? s : s.replace(' ', 'T')); + return Number.isNaN(d.getTime()) ? null : d; +} + +function clip(v: unknown, n = 200) { + const s = str(v); + return s.length > n ? s.slice(0, n) : s; +} + +function realPartyName(raw: unknown) { + let name = str(raw).replace(/\s+/g, ' '); + if (!name) return ''; + const alias: Record = { + '73047': '中国人民解放军73047部队保障部', + '中国人民解放军 32272 部队': '中国人民解放军32272部队', + }; + name = alias[name] || name; + if (/^未指定/.test(name)) return ''; + if (/^\d+$/.test(name)) return ''; + if (name.startsWith('/') || name.endsWith('.pdf')) return ''; + if (name.length < 2) return ''; + return name; +} + +function compactName(name: string) { + return str(name) + .replace(/[()()\[\]【】\s\-—_.,,、]/g, '') + .replace(/招标公告|采购项目|技术服务合同|合同/g, '') + .replace(/检查院/g, '检察院'); +} + +function namesOverlap(a: string, b: string) { + const na = compactName(a); + const nb = compactName(b); + if (!na || !nb) return false; + if (na === nb) return true; + const ka = groupKey(a); + const kb = groupKey(b); + if (ka && ka === kb && ka.length >= 3 && ka !== na && ka !== nb) return true; + const shorter = na.length <= nb.length ? na : nb; + const longer = na.length <= nb.length ? nb : na; + return shorter.length >= 8 && longer.includes(shorter); +} + +function groupKey(name: string) { + const anchors = [ + '信息化教材管理服务平台', + '教务管理系统国产化', + '中船建模', + '课程录播资源管理平台', + '九江市国动办', + '北京顺义', + '天长市', + '国防科技大学试验训练', + '银川低空', + '无锡联勤', + '南京军代', + '日常办公(江苏', + '日常办公(南京', + '数字人', + '沉浸式体验区', + '某演练', + '某单位陈列室', + 'OA网络教学', + '某部软件系统定制', + '院感及互联互通', + '财经管控', + '智能控制系统', + '某部门户网站', + ]; + const hit = anchors.find((a) => name.includes(a)); + return hit || compactName(name) || name; +} + +function mapBidStatus(row: Row) { + const st = str(row.project_status); + const win = str(row.winning_status); + if (st === '中标' || (st === '' && win.includes('中标'))) return 'WON'; + if (st === '未中' || (st === '' && win.includes('未中'))) return 'LOST'; + if (st === '流标') return 'FAILED'; + if (st === '弃标' || win.includes('弃标')) return 'TERMINATED'; + return ''; +} + +function mapContractStatus(v: unknown) { + const s = str(v); + if (s === '已通过') return 'APPROVED'; + if (s === '审批中') return 'PENDING'; + return 'DRAFT'; +} + +function mapProjectStatus(v: unknown) { + const s = str(v); + if (s === '验收通过') return 'ACCEPTED'; + if (s === '已完成') return 'DONE'; + if (s === '待验收') return 'INTERNAL_ACCEPTED'; + if (s === '未开始') return 'NOT_STARTED'; + return 'ACTIVE'; +} + +const STATUS_RANK: Record = { + 验收通过: 5, + 已完成: 4, + 待验收: 3, + 进行中: 2, + 未开始: 1, +}; + +function officialNo(raw: unknown) { + const n = str(raw).replace(/[))]+$/, '').trim(); + if (!n || n === '无' || n === '-' || n === '—') return ''; + return n; +} + +function mapProjectType(v: unknown, projectName?: unknown) { + const name = str(projectName); + if (name.includes('三维')) return '三维'; + if (/(视频.{0,12}(制作|摄制|拍摄)|摄制|拍摄|摄影|宣传片|专题片|纪录片|微电影|影视制作)/.test(name)) return '摄制'; + const s = str(v); + if (s.includes('三维')) return '三维'; + if (s.includes('摄制') || s.includes('视频')) return '摄制'; + if (s.includes('物资')) return '物资'; + if (s.includes('集成')) return '集成'; + return '软件'; +} + +function isRealTender(row: Row) { + return /^TB-\d/.test(str(row.system_number)) && Boolean(str(row.project_name)) && !str(row.project_name).startsWith('/profile/'); +} + +function isGenericContractName(name: string) { + return compactName(name) === '采购' || str(name) === '采购合同'; +} + +async function ensureParty(name: string, partyByName: Map, remark: string) { + const existing = partyByName.get(name); + if (existing) return existing; + const row = await prisma.businessParty.create({ data: { name, remark, level: 'B' } }); + partyByName.set(name, row.id); + return row.id; +} + +type MergedProject = { + systemNumber: string; + name: string; + status: string; + oaStatus: string; + contractProdId: string; + contractName: string; + budget: number; + createdAt: Date | null; +}; + +function mergeProjects(rows: Row[]): MergedProject[] { + const groups = new Map(); + for (const p of rows) { + const name = str(p.project_name); + if (!name || !str(p.system_number)) continue; + const key = groupKey(name); + if (!groups.has(key)) groups.set(key, []); + groups.get(key)!.push(p); + } + const out: MergedProject[] = []; + for (const items of groups.values()) { + const withContract = items.find((p) => p.contract_id != null && str(p.contract_id)); + const progressed = [...items].sort((a, b) => (STATUS_RANK[str(b.project_status)] || 0) - (STATUS_RANK[str(a.project_status)] || 0))[0]; + const pick = withContract || progressed; + out.push({ + systemNumber: str(pick.system_number), + name: str(progressed.project_name), + status: mapProjectStatus(progressed.project_status), + oaStatus: str(progressed.project_status), + contractProdId: withContract ? str(withContract.contract_id) : '', + contractName: str((withContract || pick).contract_name), + budget: Math.max(...items.map((p) => num(p.project_budget))), + createdAt: when(pick.create_time), + }); + } + return out; +} + +async function main() { + const raw = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Record; + const admin = await prisma.user.findFirst({ where: { username: 'admin' } }); + if (!admin) throw new Error('本地没有 admin'); + const users = await prisma.user.findMany({ select: { id: true, displayName: true, username: true } }); + const userByName = new Map(users.map((u) => [u.displayName, u.id])); + const xia = userByName.get('夏雨晴'); + const entity = + (await prisma.legalEntity.findFirst({ where: { tenantId: '1', isDefault: true } })) || + (await prisma.legalEntity.findFirst({ where: { tenantId: '1' } })); + if (!entity) throw new Error('本地没有主体'); + + const stale = await prisma.contract.findMany({ + where: { contractNo: { startsWith: 'HT-2026-00' } }, + include: { + _count: { + select: { + projects: true, + invoices: true, + bonds: true, + sealRequests: true, + milestones: true, + reviewers: true, + actionLogs: true, + }, + }, + }, + }); + const staleIds = stale.filter((c) => Object.values(c._count).every((n) => n === 0)).map((c) => c.id); + if (staleIds.length) await prisma.contract.deleteMany({ where: { id: { in: staleIds } } }); + + const tenders = (raw.oa_tender || []).filter(isRealTender); + const contracts = raw.oa_contract || []; + const mergedProjects = mergeProjects(raw.project_list || []); + + const parties = await prisma.businessParty.findMany(); + const partyByName = new Map(parties.map((p) => [p.name, p.id])); + + for (const t of tenders) { + const n = realPartyName(t.tender_dan_wei); + if (n) await ensureParty(n, partyByName, 'OA 招标单位'); + } + for (const c of contracts) { + const n = realPartyName(c.party_a_name); + if (n && n !== '江苏风影随行科技有限公司') await ensureParty(n, partyByName, 'OA 合同甲方'); + } + + const bids = await prisma.bidCase.findMany(); + const bidByNo = new Map(bids.map((b) => [b.bidNo, b])); + let bidsUpdated = 0; + let bidsCreated = 0; + const usedNos = new Set(bids.map((b) => b.bidNo)); + + function findExistingBid(t: Row) { + const sys = str(t.system_number); + const official = officialNo(t.project_number); + const suffix = sys.split('-').pop() || ''; + const name = str(t.project_name); + const all = [...bidByNo.values()]; + return ( + bidByNo.get(sys) || + all.find((b) => b.externalNo === sys) || + all.find((b) => b.bidNo === `${official}-${suffix}` || (official && b.bidNo.endsWith(`-${suffix}`) && b.name === name)) || + (official && all.filter((b) => b.bidNo === official || b.externalNo === official).length === 1 + ? all.find((b) => b.bidNo === official || b.externalNo === official) + : undefined) + ); + } + + const testNos = ['SP-TEST-1788068192', 'SP-FIX-1788068273']; + const tests = bids.filter((b) => testNos.includes(b.bidNo) || testNos.includes(b.externalNo || '')); + if (tests.length) { + const ids = tests.map((b) => b.id); + await prisma.todoTask.deleteMany({ where: { bizId: { in: ids } } }); + await prisma.approvalTask.deleteMany({ where: { bizId: { in: ids } } }); + await prisma.bidCase.deleteMany({ where: { id: { in: ids } } }); + } + + for (const t of tenders) { + const sys = str(t.system_number); + const official = officialNo(t.project_number); + const name = clip(t.project_name, 200); + const partyName = realPartyName(t.tender_dan_wei); + const status = mapBidStatus(t) || 'PENDING'; + const existing = findExistingBid(t); + const resultBits = [ + t.winning_unit ? `中标单位:${t.winning_unit}` : '', + num(t.winning_amount) ? `中标金额:${num(t.winning_amount)}` : '', + str(t.opening_remark), + str(t.bid_summary), + ].filter(Boolean); + + if (existing) { + let bidNo = official || sys; + if (usedNos.has(bidNo) && bidNo !== existing.bidNo) bidNo = `${official || sys}-${sys.slice(-3)}`; + usedNos.delete(existing.bidNo); + usedNos.add(bidNo); + await prisma.bidCase.update({ + where: { id: existing.id }, + data: { + bidNo, + name, + externalNo: official || null, + status, + sourceType: 'FLOW', + partyId: partyName ? partyByName.get(partyName) : existing.partyId, + tenderMethod: clip(t.tender_method, 50) || existing.tenderMethod, + openPlace: clip(t.tender_address, 200) || existing.openPlace, + priceCap: t.price_limit == null ? existing.priceCap : String(t.price_limit), + quoteAmount: t.bid_quotation == null ? existing.quoteAmount : String(t.bid_quotation), + resultNote: resultBits.join(';') || existing.resultNote, + openedResultAt: when(t.winning_date) || when(t.bid_open_date) || existing.openedResultAt, + }, + }); + existing.bidNo = bidNo; + existing.name = name; + existing.status = status; + existing.externalNo = official || existing.externalNo; + bidByNo.set(bidNo, existing); + bidByNo.set(sys, existing); + bidsUpdated += 1; + } else { + let bidNo = official || sys; + if (usedNos.has(bidNo)) bidNo = `${sys}`; + if (usedNos.has(bidNo)) bidNo = `${sys}-${Date.now().toString().slice(-4)}`; + usedNos.add(bidNo); + const row = await prisma.bidCase.create({ + data: { + bidNo, + name, + externalNo: official || undefined, + projectType: mapProjectType(t.project_type, t.project_name), + legalEntityId: entity.id, + partyId: partyName ? partyByName.get(partyName) : undefined, + tenderMethod: clip(t.tender_method, 50) || undefined, + applyEndAt: when(t.file_apply_deadline) || undefined, + openAt: when(t.bid_open_date) || undefined, + openPlace: clip(t.tender_address, 200) || undefined, + securityLevel: str(t.security_level) || 'INTERNAL', + projectNature: clip(t.project_nature, 50) || undefined, + contactName: clip(t.bid_contact_person, 64) || undefined, + contactPhone: clip(t.bid_contact_phone, 32) || undefined, + priceCap: t.price_limit == null ? undefined : String(t.price_limit), + quoteAmount: t.bid_quotation == null ? undefined : String(t.bid_quotation), + resultNote: resultBits.join(';') || undefined, + openedResultAt: when(t.winning_date) || when(t.bid_open_date) || undefined, + status, + createdById: admin.id, + createdAt: when(t.create_time) || undefined, + }, + }); + bidByNo.set(bidNo, row); + bidByNo.set(sys, row); + bidsCreated += 1; + } + } + + const localContracts = await prisma.contract.findMany(); + const contractByNo = new Map(localContracts.map((c) => [c.contractNo, c])); + const contractByProd = new Map(); + let contractsUpdated = 0; + let contractsCreated = 0; + + for (const c of contracts) { + const contractNo = str(c.contract_no); + if (!contractNo) continue; + const tb = contractNo.match(/TB-\d+-\d+/); + const bid = + (tb && (bidByNo.get(tb[0]) || [...bidByNo.values()].find((b) => b.bidNo === tb[0] || b.bidNo.startsWith(tb[0])))) || + [...bidByNo.values()].find((b) => namesOverlap(b.name, str(c.contract_name))); + if (str(c.approval_status) === 'pending_submit' && !c.sign_date && !num(c.amount)) { + continue; + } + const partyA = realPartyName(c.party_a_name); + const partyB = str(c.party_b_name); + const remark = [ + str(c.payment_method) ? `付款:${c.payment_method}` : '', + str(c.contract_lb) ? `类别:${c.contract_lb}` : '', + str(c.remark), + ] + .filter(Boolean) + .join(' / '); + const handlerId = Number(c.create_by) === 107 ? xia : bid?.createdById || xia || admin.id; + const data = { + name: clip(c.contract_name, 200), + bidCaseId: bid?.id, + legalEntityId: entity.id, + amount: new Prisma.Decimal(num(c.amount)), + status: mapContractStatus(c.approval_status), + partyA: partyA || undefined, + partyB: partyB || undefined, + remark: remark || undefined, + signedAt: when(c.sign_date) || undefined, + createdById: handlerId, + }; + const existing = + contractByNo.get(contractNo) || + localContracts.find( + (row) => row.name === clip(c.contract_name, 200) && /^HT-2026-00\d+$/.test(row.contractNo), + ); + if (existing) { + const nextNo = contractByNo.has(contractNo) && existing.contractNo !== contractNo ? existing.contractNo : contractNo; + await prisma.contract.update({ + where: { id: existing.id }, + data: { ...data, contractNo: nextNo }, + }); + existing.contractNo = nextNo; + contractByNo.set(nextNo, existing); + contractByProd.set(String(c.id), existing.id); + contractsUpdated += 1; + } else { + const row = await prisma.contract.create({ + data: { + contractNo, + createdAt: when(c.create_time) || undefined, + ...data, + }, + }); + contractByNo.set(contractNo, row); + contractByProd.set(String(c.id), row.id); + contractsCreated += 1; + } + } + + const localProjects = await prisma.project.findMany({ include: { budgets: true } }); + const usedProjectNos = new Set(localProjects.map((p) => p.projectNo)); + let projectsUpdated = 0; + let projectsCreated = 0; + const projectMoves: { name: string; from: string; to: string; no: string }[] = []; + + function findLocalProject(p: MergedProject) { + return ( + localProjects.find((row) => row.projectNo === p.systemNumber) || + localProjects.find((row) => namesOverlap(row.name, p.name) || groupKey(row.name) === groupKey(p.name)) + ); + } + + function findContractId(p: MergedProject) { + if (p.contractProdId && contractByProd.has(p.contractProdId)) return contractByProd.get(p.contractProdId); + if (p.contractName) { + const exact = [...contractByNo.values()].find((c) => c.name === p.contractName); + if (exact) return exact.id; + } + const fuzzy = [...contractByNo.values()].find( + (c) => !isGenericContractName(c.name) && namesOverlap(c.name, p.name), + ); + return fuzzy?.id; + } + + for (const p of mergedProjects) { + const existing = findLocalProject(p); + const contractId = findContractId(p); + if (existing) { + let projectNo = p.systemNumber; + if (usedProjectNos.has(projectNo) && projectNo !== existing.projectNo) { + projectNo = existing.projectNo; + } + usedProjectNos.delete(existing.projectNo); + usedProjectNos.add(projectNo); + if (existing.status !== p.status || existing.projectNo !== projectNo) { + projectMoves.push({ name: p.name.slice(0, 24), from: `${existing.projectNo}/${existing.status}`, to: `${projectNo}/${p.status}`, no: p.oaStatus }); + } + await prisma.project.update({ + where: { id: existing.id }, + data: { + projectNo, + name: clip(p.name, 200), + contractId, + status: p.status, + }, + }); + if (p.budget > 0) { + await prisma.projectBudget.upsert({ + where: { projectId_category: { projectId: existing.id, category: 'PURCHASE' } }, + update: { amount: new Prisma.Decimal(p.budget) }, + create: { projectId: existing.id, category: 'PURCHASE', amount: new Prisma.Decimal(p.budget) }, + }); + } + existing.projectNo = projectNo; + existing.name = p.name; + existing.status = p.status; + projectsUpdated += 1; + } else { + if (!contractId && /招标公告/.test(p.name) && p.status === 'NOT_STARTED') { + continue; + } + let projectNo = p.systemNumber; + if (usedProjectNos.has(projectNo)) projectNo = `${p.systemNumber}-2`; + usedProjectNos.add(projectNo); + const row = await prisma.project.create({ + data: { + projectNo, + name: clip(p.name, 200), + contractId, + status: p.status, + createdAt: p.createdAt || undefined, + }, + }); + if (p.budget > 0) { + await prisma.projectBudget.create({ + data: { projectId: row.id, category: 'PURCHASE', amount: new Prisma.Decimal(p.budget) }, + }); + } + localProjects.push({ ...row, budgets: [] }); + projectsCreated += 1; + } + } + + const caijing = await prisma.bidCase.findMany({ + where: { name: { contains: '财经管控' } }, + include: { _count: { select: { contracts: true, expenses: true, bonds: true, assignees: true, versions: true } } }, + }); + if (caijing.length) { + const won = caijing.find((b) => /196/.test(b.bidNo) || b._count.contracts > 0); + const extras = caijing.filter((b) => b.id !== won?.id); + const keepFailed = extras.find((b) => Object.values(b._count).some((n) => n > 0)) || extras[0]; + if (keepFailed) { + await prisma.bidCase.update({ + where: { id: keepFailed.id }, + data: { status: 'FAILED', bidNo: keepFailed.bidNo.includes('182') ? keepFailed.bidNo : '2026-JLDJAE-W3006-182' }, + }); + } + if (won && won.status !== 'WON') { + await prisma.bidCase.update({ where: { id: won.id }, data: { status: 'WON' } }); + } + const drop = extras.filter((b) => b.id !== keepFailed?.id && Object.values(b._count).every((n) => n === 0)).map((b) => b.id); + if (drop.length) await prisma.bidCase.deleteMany({ where: { id: { in: drop } } }); + } + + const bidCounts = await prisma.bidCase.groupBy({ by: ['status'], _count: true }); + const projectCounts = await prisma.project.groupBy({ by: ['status'], _count: true }); + const linked = await prisma.project.count({ where: { contractId: { not: null } } }); + console.log( + JSON.stringify( + { + tenders: tenders.length, + bidsUpdated, + bidsCreated, + contractsUpdated, + contractsCreated, + projectsMerged: mergedProjects.length, + projectsUpdated, + projectsCreated, + projectMoves, + bidCounts, + projectCounts, + projectsWithContract: linked, + }, + null, + 2, + ), + ); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/prisma/wash-terminated-winners.ts b/apps/api/prisma/wash-terminated-winners.ts new file mode 100644 index 0000000..f6863e3 --- /dev/null +++ b/apps/api/prisma/wash-terminated-winners.ts @@ -0,0 +1,186 @@ +import { Prisma, PrismaClient } from '@prisma/client'; +import * as fs from 'fs'; + +const prisma = new PrismaClient(); +const DUMP = '/opt/import/prod/oa_flow_finance.json'; +const APPLY = process.argv.includes('--apply'); + +type Row = Record; +type Bid = { + id: string; + bidNo: string; + name: string; + externalNo: string | null; + status: string; + winnerName: string | null; + winnerAmount: Prisma.Decimal | null; + winnerNote: string | null; +}; + +function str(v: unknown) { + if (v == null) return ''; + return String(v).trim(); +} + +function num(v: unknown) { + if (v == null || v === '') return null; + const n = Number(String(v).replace(/,/g, '')); + if (!Number.isFinite(n) || n === 0) return null; + return n; +} + +function compactName(name: string) { + return str(name) + .replace(/\s+/g, '') + .replace(/[((].*?[))]/g, '') + .replace(/招标公告|采购公告|询价公告|竞争性谈判公告|谈判公告|比价公告|需求公示|意向公开/g, ''); +} + +function isOwnWinner(name: string) { + return /风影|随行科技|山东鲁兵/.test(name) || name === '隆创'; +} + +function officialNo(raw: unknown) { + const n = str(raw).replace(/[))]+$/, '').trim(); + if (!n || n === '无' || n === '-' || n === '—') return ''; + return n; +} + +function pickUnused(cands: Bid[], used: Set) { + const open = cands.filter((b) => !used.has(b.id)); + if (!open.length) return null; + const yt = open.find((b) => b.bidNo.startsWith('YT-')); + return yt || open[0]; +} + +async function main() { + if (!fs.existsSync(DUMP)) { + throw new Error(`找不到 OA 备份 ${DUMP}`); + } + const raw = JSON.parse(fs.readFileSync(DUMP, 'utf8')) as Record; + const pres = raw.project_preinvest_apply || []; + const sources = pres.filter((p) => { + const deleted = str(p.deleted) === '1'; + const winner = str(p.winning_unit); + const cat = str(p.screening_category) || str(p.project_type); + return deleted && winner && cat.includes('软件'); + }); + + const bids = await prisma.bidCase.findMany({ + where: { tenantId: '1', status: 'TERMINATED' }, + select: { + id: true, + bidNo: true, + name: true, + externalNo: true, + status: true, + winnerName: true, + winnerAmount: true, + winnerNote: true, + }, + }); + + const byNo = new Map(); + const byName = new Map(); + const byCompact = new Map(); + const add = (map: Map, key: string, row: Bid) => { + if (!key) return; + const list = map.get(key) || []; + list.push(row); + map.set(key, list); + }; + for (const b of bids) { + add(byNo, b.bidNo, b); + add(byNo, str(b.externalNo), b); + add(byName, b.name, b); + add(byCompact, compactName(b.name), b); + } + + const used = new Set(); + const updated: { bidNo: string; name: string; winner: string; amount: number | null; via: string }[] = []; + const skippedSame: string[] = []; + const conflicts: { bidNo: string; have: string; want: string }[] = []; + const unmatched: { applyNo: string; name: string; winner: string }[] = []; + const ownSkipped: string[] = []; + + for (const p of sources) { + const applyNo = str(p.apply_no); + const name = str(p.project_name); + const official = officialNo(p.project_number); + const winner = str(p.winning_unit); + const amount = num(p.winning_amount); + if (isOwnWinner(winner)) { + ownSkipped.push(`${applyNo} ${winner}`); + continue; + } + + const hit = + pickUnused(byNo.get(applyNo) || [], used) || + pickUnused(byNo.get(official) || [], used) || + pickUnused(byName.get(name) || [], used) || + pickUnused(byCompact.get(compactName(name)) || [], used); + + if (!hit) { + unmatched.push({ applyNo, name, winner }); + continue; + } + used.add(hit.id); + + const haveName = str(hit.winnerName); + const haveAmt = hit.winnerAmount != null ? Number(hit.winnerAmount) : null; + if (haveName) { + const sameName = haveName === winner; + const sameAmt = amount == null || haveAmt === amount; + if (sameName && sameAmt) skippedSame.push(hit.bidNo); + else conflicts.push({ bidNo: hit.bidNo, have: `${haveName}/${haveAmt}`, want: `${winner}/${amount}` }); + continue; + } + + const note = str(hit.winnerNote) || 'OA 软件已删除迁入'; + if (APPLY) { + await prisma.bidCase.update({ + where: { id: hit.id }, + data: { + winnerName: winner, + winnerAmount: amount == null ? undefined : new Prisma.Decimal(amount), + winnerNote: note, + }, + }); + } + updated.push({ + bidNo: hit.bidNo, + name: hit.name, + winner, + amount, + via: hit.bidNo === applyNo ? 'bidNo' : str(hit.externalNo) === applyNo || hit.bidNo === official ? 'official' : 'name', + }); + } + + console.log( + JSON.stringify( + { + apply: APPLY, + dumpSources: sources.length, + terminated: bids.length, + filled: updated.length, + skippedSame: skippedSame.length, + conflicts: conflicts.length, + unmatched: unmatched.length, + ownSkipped: ownSkipped.length, + filledSample: updated.slice(0, 8), + conflictRows: conflicts, + unmatchedRows: unmatched.slice(0, 20), + tinyAmounts: updated.filter((u) => u.amount != null && u.amount < 100), + }, + null, + 2, + ), + ); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts new file mode 100644 index 0000000..7087371 --- /dev/null +++ b/apps/api/src/app.module.ts @@ -0,0 +1,93 @@ +import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core'; +import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler'; +import { loadEnv } from './config/env'; +import { PrismaModule } from './prisma/prisma.module'; +import { AuthModule } from './auth/auth.module'; +import { SmsModule } from './sms/sms.module'; +import { ImModule } from './im/im.module'; +import { PushModule } from './push/push.module'; +import { HealthController } from './health/health.controller'; +import { ReportController } from './report/report.controller'; +import { ReportService } from './report/report.service'; +import { SystemController } from './system/system.controller'; +import { SystemService } from './system/system.service'; +import { OfficeController } from './office/office.controller'; +import { MasterController } from './master/master.controller'; +import { BiddingController } from './bidding/bidding.controller'; +import { BiddingService } from './bidding/bidding.service'; +import { ContractController } from './contract/contract.controller'; +import { ContractService } from './contract/contract.service'; +import { ProjectController } from './project/project.controller'; +import { ProjectService } from './project/project.service'; +import { SealController } from './seal/seal.controller'; +import { SealService } from './seal/seal.service'; +import { FinanceController } from './finance/finance.controller'; +import { FinanceService } from './finance/finance.service'; +import { BusinessController } from './business/business.controller'; +import { HrController } from './hr/hr.controller'; +import { HrService } from './hr/hr.service'; +import { MasterService } from './master/master.service'; +import { FilesController } from './files/files.controller'; +import { FilesModule } from './files/files.module'; +import { JwtAuthGuard, PermissionsGuard } from './common/guards/auth.guards'; +import { ResponseInterceptor } from './common/interceptors/response.interceptor'; +import { HttpErrorFilter } from './common/filters/http-exception.filter'; +import { RequestIdMiddleware } from './common/middleware/request-id.middleware'; +import { LegalController } from './legal/legal.controller'; + +@Module({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + envFilePath: ['.env', '../../.env'], + validate: (raw) => loadEnv(raw), + }), + ThrottlerModule.forRoot({ + throttlers: [{ ttl: 60000, limit: 120 }], + }), + PrismaModule, + AuthModule, + SmsModule, + PushModule, + ImModule, + FilesModule, + ], + controllers: [ + HealthController, + SystemController, + OfficeController, + MasterController, + BiddingController, + ContractController, + ProjectController, + SealController, + FinanceController, + BusinessController, + HrController, + ReportController, + LegalController, + ], + providers: [ + BiddingService, + ContractService, + ProjectService, + SealService, + FinanceService, + HrService, + ReportService, + SystemService, + MasterService, + { provide: APP_GUARD, useClass: JwtAuthGuard }, + { provide: APP_GUARD, useClass: PermissionsGuard }, + { provide: APP_GUARD, useClass: ThrottlerGuard }, + { provide: APP_INTERCEPTOR, useClass: ResponseInterceptor }, + { provide: APP_FILTER, useClass: HttpErrorFilter }, + ], +}) +export class AppModule implements NestModule { + configure(consumer: MiddlewareConsumer) { + consumer.apply(RequestIdMiddleware).forRoutes('*'); + } +} diff --git a/apps/api/src/auth/auth.controller.ts b/apps/api/src/auth/auth.controller.ts new file mode 100644 index 0000000..55ba6af --- /dev/null +++ b/apps/api/src/auth/auth.controller.ts @@ -0,0 +1,199 @@ +import { BadRequestException, Body, Controller, Get, Patch, Post, Query, Req } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { Equals, IsOptional, IsString, MinLength } from 'class-validator'; +import type { Request } from 'express'; +import { AuthService } from './auth.service'; +import { Public } from '../common/decorators/auth.decorators'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { PrismaService } from '../prisma/prisma.service'; +import { SmsService } from '../sms/sms.service'; +import { LOG_ACTION, writeOpLog } from '../system/op-log'; +import { REQUEST_ID_HEADER } from '../common/middleware/request-id.middleware'; + +class LoginDto { + @IsString() + username!: string; + + @IsString() + @MinLength(6) + password!: string; + + @IsOptional() + @IsString() + smsCode?: string; + + @Transform(({ value }) => value === true || value === 'true' || value === 1 || value === '1') + @Equals(true, { message: '请先阅读并同意用户协议和隐私政策' }) + agreeTerms!: boolean; + + @IsOptional() + @IsString() + clientKind?: string; +} + +class RefreshDto { + @IsString() + refreshToken!: string; + + @IsOptional() + @IsString() + clientKind?: string; +} + +class ProfileDto { + @IsOptional() + @IsString() + displayName?: string; +} + +class PasswordDto { + @IsString() + oldPassword!: string; + @IsString() + @MinLength(6, { message: '新密码至少 6 位' }) + newPassword!: string; +} + +class OnboardProfileDto { + @IsOptional() @IsString() name?: string; + @IsOptional() @IsString() gender?: string; + @IsOptional() @IsString() email?: string; + @IsOptional() @IsString() mobile?: string; + @IsOptional() @IsString() birthday?: string; + @IsOptional() @IsString() ethnicity?: string; + @IsOptional() @IsString() maritalStatus?: string; + @IsOptional() @IsString() hukouAddress?: string; + @IsOptional() @IsString() liveAddress?: string; + @IsOptional() @IsString() emergencyName?: string; + @IsOptional() @IsString() emergencyPhone?: string; + @IsOptional() @IsString() education?: string; + @IsOptional() @IsString() school?: string; + @IsOptional() @IsString() major?: string; + @IsOptional() @IsString() graduatedAt?: string; + @IsOptional() @IsString() bankName?: string; + @IsOptional() @IsString() bankAccount?: string; + @IsOptional() @IsString() idType?: string; + @IsOptional() @IsString() idNo?: string; + @IsOptional() @IsString() idIssuer?: string; + @IsOptional() @IsString() idValidFrom?: string; + @IsOptional() @IsString() idValidTo?: string; +} + +@ApiTags('auth') +@Controller('auth') +export class AuthController { + constructor( + private readonly auth: AuthService, + private readonly prisma: PrismaService, + private readonly sms: SmsService, + ) {} + + @Public() + @Get('login-options') + loginOptions() { + return this.sms.loginOptions(); + } + + @Public() + @Post('sms-code') + smsCode(@Body() body: { username?: string }) { + return this.sms.sendLoginCode(body.username || ''); + } + + @Public() + @Post('login') + @ApiOperation({ summary: '登录' }) + async login(@Body() dto: LoginDto, @Req() req: Request) { + const requestId = (req.headers[REQUEST_ID_HEADER] as string) || ''; + try { + if (!dto.agreeTerms) { + throw new BadRequestException('请先阅读并同意用户协议和隐私政策'); + } + const result = await this.auth.login(dto.username, dto.password, dto.smsCode, dto.clientKind); + await writeOpLog(this.prisma, { + userId: result.user.id, + username: result.user.username, + action: LOG_ACTION.LOGIN, + path: '/auth/login', + detail: '登录成功', + requestId, + ip: req.ip, + }); + return result; + } catch (e) { + await writeOpLog(this.prisma, { + username: dto.username, + action: LOG_ACTION.LOGIN_FAIL, + path: '/auth/login', + detail: '用户名或密码错误', + success: false, + requestId, + ip: req.ip, + }); + throw e; + } + } + + @Public() + @Post('refresh') + @ApiOperation({ summary: '刷新令牌' }) + refresh(@Body() dto: RefreshDto) { + return this.auth.refresh(dto.refreshToken, dto.clientKind); + } + + @Post('logout') + @ApiBearerAuth() + logout(@CurrentUser() user: AuthUser, @Body() dto: Partial) { + return this.auth.logout(user.id, dto.refreshToken); + } + + @Public() + @Post('qr/start') + qrStart() { + return this.auth.startQrLogin(); + } + + @Public() + @Get('qr/status') + qrStatus(@Query('ticket') ticket?: string) { + return this.auth.qrStatus(ticket || ''); + } + + @Public() + @Post('qr/complete') + qrComplete(@Body() body: { ticket?: string; code?: string }) { + return this.auth.completeQrLogin(body.ticket || '', body.code || ''); + } + + @Post('qr/confirm') + @ApiBearerAuth() + qrConfirm(@CurrentUser() user: AuthUser, @Body() body: { ticket?: string }) { + return this.auth.confirmQrLogin(user.id, body.ticket || ''); + } + + @Get('me') + @ApiBearerAuth() + me(@CurrentUser() user: AuthUser) { + return this.auth.me(user.id); + } + + @Patch('profile') + @ApiBearerAuth() + updateProfile(@CurrentUser() user: AuthUser, @Body() dto: ProfileDto) { + return this.auth.updateProfile(user.id, dto); + } + + @Patch('password') + @ApiBearerAuth() + updatePassword(@CurrentUser() user: AuthUser, @Body() dto: PasswordDto) { + return this.auth.updatePassword(user.id, dto); + } + + @Patch('onboard-profile') + @ApiBearerAuth() + onboardProfile(@CurrentUser() user: AuthUser, @Body() dto: OnboardProfileDto) { + return this.auth.onboardProfile(user.id, dto); + } +} diff --git a/apps/api/src/auth/auth.module.ts b/apps/api/src/auth/auth.module.ts new file mode 100644 index 0000000..cc56e5e --- /dev/null +++ b/apps/api/src/auth/auth.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { JwtModule } from '@nestjs/jwt'; +import { PassportModule } from '@nestjs/passport'; +import { AuthController } from './auth.controller'; +import { AuthService } from './auth.service'; +import { JwtStrategy } from './jwt.strategy'; + +@Module({ + imports: [PassportModule, JwtModule.register({})], + controllers: [AuthController], + providers: [AuthService, JwtStrategy], + exports: [AuthService], +}) +export class AuthModule {} diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts new file mode 100644 index 0000000..11ac34a --- /dev/null +++ b/apps/api/src/auth/auth.service.ts @@ -0,0 +1,532 @@ +import { + BadRequestException, + Injectable, + UnauthorizedException, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { ConfigService } from '@nestjs/config'; +import * as argon2 from 'argon2'; +import { createHash, randomInt, randomUUID } from 'crypto'; +import { PrismaService } from '../prisma/prisma.service'; +import { Prisma } from '@prisma/client'; +import { SmsService } from '../sms/sms.service'; +import { elevateToAdmin } from '../common/role-groups'; + +function sha256(value: string) { + return createHash('sha256').update(value).digest('hex'); +} + +function needsOnboardGate(input: { + isAdmin: boolean; + mustChangePassword?: boolean | null; + profileCompletedAt?: Date | string | null; + employeeId?: string | null; + idPhotoFileId?: string | null; + idFrontFileId?: string | null; + idBackFileId?: string | null; + employee?: { idNo?: string | null; idType?: string | null } | null; +}) { + if (input.isAdmin) return false; + if (input.mustChangePassword) return true; + if (!input.profileCompletedAt) return true; + if (!input.employeeId) return false; + if (!input.idPhotoFileId || !input.idFrontFileId || !input.idBackFileId) return true; + if (input.employee && (!input.employee.idNo || !input.employee.idType)) return true; + return false; +} + +@Injectable() +export class AuthService { + constructor( + private readonly prisma: PrismaService, + private readonly jwt: JwtService, + private readonly config: ConfigService, + private readonly sms: SmsService, + ) {} + + async login(username: string, password: string, smsCode?: string, clientKind = 'web') { + const account = username.trim(); + const kind = normalizeClientKind(clientKind); + if (kind === 'desktop') { + throw new BadRequestException('电脑端请用手机扫码登录'); + } + const user = await this.prisma.user.findFirst({ + where: { + tenantId: '1', + OR: [{ username: account }, { employee: { is: { mobile: account } } }], + }, + include: { + roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } }, + employee: { include: { department: true } }, + }, + }); + if (!user || user.status !== 'ACTIVE') { + throw new UnauthorizedException('手机号或密码错误'); + } + const ok = await argon2.verify(user.passwordHash, password); + if (!ok) { + throw new UnauthorizedException('用户名或密码错误'); + } + await this.sms.assertLoginCode(account, smsCode); + return this.issueTokens(user, kind); + } + + async refresh(refreshToken: string, clientKindHint?: string) { + if (!refreshToken) throw new BadRequestException('缺少刷新令牌'); + let payload: { sub: string; typ: string; ck?: string }; + try { + payload = await this.jwt.verifyAsync(refreshToken, { + secret: this.config.get('JWT_REFRESH_SECRET'), + }); + } catch { + throw new UnauthorizedException('刷新令牌无效'); + } + if (payload.typ !== 'refresh') { + throw new UnauthorizedException('刷新令牌无效'); + } + const hash = sha256(refreshToken); + const stored = await this.prisma.refreshToken.findFirst({ + where: { userId: payload.sub, tokenHash: hash, revokedAt: null }, + }); + if (!stored || stored.expiresAt < new Date()) { + throw new UnauthorizedException('刷新令牌已失效'); + } + const user = await this.prisma.user.findUnique({ + where: { id: payload.sub }, + include: { + roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } }, + employee: { include: { department: true } }, + }, + }); + if (!user) throw new UnauthorizedException('用户不存在'); + const kind = normalizeClientKind(clientKindHint || stored.clientKind || payload.ck); + if (stored.clientKind !== kind) { + await this.prisma.refreshToken.update({ where: { id: stored.id }, data: { clientKind: kind } }); + } + if (kind === 'mobile') { + return this.issueTokens(user, kind, { keepRefresh: stored }); + } + await this.prisma.refreshToken.update({ + where: { id: stored.id }, + data: { revokedAt: new Date() }, + }); + return this.issueTokens(user, kind); + } + + async logout(userId: string, refreshToken?: string) { + if (refreshToken) { + await this.prisma.refreshToken.updateMany({ + where: { userId, tokenHash: sha256(refreshToken), revokedAt: null }, + data: { revokedAt: new Date() }, + }); + } else { + await this.prisma.refreshToken.updateMany({ + where: { userId, clientKind: { in: ['web', 'desktop'] }, revokedAt: null }, + data: { revokedAt: new Date() }, + }); + } + return { ok: true }; + } + + async updateProfile(userId: string, dto: { displayName?: string }) { + const name = dto.displayName?.trim(); + if (!name) throw new BadRequestException('请填写姓名'); + const user = await this.prisma.user.update({ + where: { id: userId }, + data: { displayName: name }, + include: { employee: true }, + }); + if (user.employeeId) { + await this.prisma.employee.update({ + where: { id: user.employeeId }, + data: { name }, + }); + } + return this.me(userId); + } + + async updatePassword(userId: string, dto: { oldPassword: string; newPassword: string }) { + if (!dto.newPassword || dto.newPassword.length < 6) { + throw new BadRequestException('新密码至少 6 位'); + } + const user = await this.prisma.user.findFirst({ where: { id: userId, tenantId: '1' } }); + if (!user) throw new UnauthorizedException(); + const ok = await argon2.verify(user.passwordHash, dto.oldPassword || ''); + if (!ok) throw new BadRequestException('当前密码不正确'); + await this.prisma.user.update({ + where: { id: userId }, + data: { passwordHash: await argon2.hash(dto.newPassword), mustChangePassword: false }, + }); + return { ok: true }; + } + + async onboardProfile( + userId: string, + dto: { + name?: string; + gender?: string; + email?: string; + mobile?: string; + birthday?: string; + ethnicity?: string; + maritalStatus?: string; + hukouAddress?: string; + liveAddress?: string; + emergencyName?: string; + emergencyPhone?: string; + education?: string; + school?: string; + major?: string; + graduatedAt?: string; + bankName?: string; + bankAccount?: string; + idType?: string; + idNo?: string; + idIssuer?: string; + idValidFrom?: string; + idValidTo?: string; + }, + ) { + const user = await this.prisma.user.findFirst({ + where: { id: userId, tenantId: '1' }, + include: { roles: { include: { role: true } }, employee: true }, + }); + if (!user) throw new UnauthorizedException(); + const isAdmin = user.roles.some((r) => r.role.code === 'admin'); + if (isAdmin) return this.me(userId); + if (user.employeeId && user.employee) { + const name = dto.name?.trim() || user.employee.name; + if (!name) throw new BadRequestException('请填写姓名'); + if (!dto.mobile?.trim() && !user.employee.mobile) throw new BadRequestException('请填写手机号码'); + if (!dto.idType?.trim() && !user.employee.idType) throw new BadRequestException('请选择证件类型'); + if (!dto.idNo?.trim() && !user.employee.idNo) throw new BadRequestException('请填写证件号码'); + if (!dto.idIssuer?.trim() && !user.employee.idIssuer) throw new BadRequestException('请填写发证机关'); + if (!dto.idValidFrom && !user.employee.idValidFrom) throw new BadRequestException('请选择证件有效期起'); + if (!dto.idValidTo && !user.employee.idValidTo) throw new BadRequestException('请选择证件有效期止'); + if (!dto.education?.trim() && !user.employee.education) throw new BadRequestException('请填写学历'); + if (!dto.bankName?.trim() && !user.employee.bankName) throw new BadRequestException('请填写南京银行卡开户行'); + if (!dto.bankAccount?.trim() && !user.employee.bankAccount) throw new BadRequestException('请填写南京银行卡号'); + const scans = await this.prisma.fileAsset.findMany({ + where: { + tenantId: '1', + OR: [ + { bizType: 'USER_ID_PHOTO', bizId: userId }, + { bizType: { in: ['EMPLOYEE_ID_PHOTO', 'EMPLOYEE_ID_FRONT', 'EMPLOYEE_ID_BACK'] }, bizId: user.employeeId }, + ], + }, + }); + const hasPhoto = scans.some((f) => f.bizType === 'USER_ID_PHOTO' || f.bizType === 'EMPLOYEE_ID_PHOTO'); + const hasFront = scans.some((f) => f.bizType === 'EMPLOYEE_ID_FRONT'); + const hasBack = scans.some((f) => f.bizType === 'EMPLOYEE_ID_BACK'); + if (!hasPhoto) throw new BadRequestException('请上传证件照'); + if (!hasFront) throw new BadRequestException('请上传身份证正面'); + if (!hasBack) throw new BadRequestException('请上传身份证反面'); + await this.prisma.employee.update({ + where: { id: user.employeeId }, + data: { + name, + gender: dto.gender?.trim() || user.employee.gender, + email: dto.email?.trim() || user.employee.email, + mobile: dto.mobile?.trim() || user.employee.mobile, + birthday: dto.birthday ? new Date(dto.birthday) : user.employee.birthday, + ethnicity: dto.ethnicity?.trim() || user.employee.ethnicity, + maritalStatus: dto.maritalStatus?.trim() || user.employee.maritalStatus, + hukouAddress: dto.hukouAddress?.trim() || user.employee.hukouAddress, + liveAddress: dto.liveAddress?.trim() || user.employee.liveAddress, + emergencyName: dto.emergencyName?.trim() || user.employee.emergencyName, + emergencyPhone: dto.emergencyPhone?.trim() || user.employee.emergencyPhone, + idType: dto.idType?.trim() || user.employee.idType, + idNo: dto.idNo?.trim() || user.employee.idNo, + idIssuer: dto.idIssuer?.trim() || user.employee.idIssuer, + idValidFrom: dto.idValidFrom ? new Date(dto.idValidFrom) : user.employee.idValidFrom, + idValidTo: dto.idValidTo ? new Date(dto.idValidTo) : user.employee.idValidTo, + education: dto.education?.trim() || user.employee.education, + school: dto.school?.trim() || user.employee.school, + major: dto.major?.trim() || user.employee.major, + graduatedAt: dto.graduatedAt ? new Date(dto.graduatedAt) : user.employee.graduatedAt, + bankName: dto.bankName?.trim() || user.employee.bankName, + bankAccount: dto.bankAccount?.trim() || user.employee.bankAccount, + }, + }); + if (name !== user.displayName) { + await this.prisma.user.update({ where: { id: userId }, data: { displayName: name } }); + } + } + await this.prisma.user.update({ + where: { id: userId }, + data: { profileCompletedAt: new Date() }, + }); + return this.me(userId); + } + + async me(userId: string) { + const user = await this.prisma.user.findUnique({ + where: { id: userId }, + include: { + roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } }, + employee: { include: { department: true } }, + }, + }); + if (!user) throw new UnauthorizedException(); + const roles = elevateToAdmin(user.roles.map((r) => r.role.code)); + const permissions = [ + ...new Set(user.roles.flatMap((r) => r.role.permissions.map((p) => p.permission.code))), + ]; + const photos = await this.prisma.fileAsset.findMany({ + where: { + tenantId: '1', + OR: [ + { bizId: user.id, bizType: { in: ['USER_AVATAR', 'USER_ID_PHOTO'] } }, + user.employeeId + ? { bizId: user.employeeId, bizType: { in: ['EMPLOYEE_ID_PHOTO', 'EMPLOYEE_ID_FRONT', 'EMPLOYEE_ID_BACK'] } } + : undefined, + ].filter(Boolean) as Prisma.FileAssetWhereInput[], + }, + orderBy: { createdAt: 'desc' }, + }); + const isAdmin = roles.includes('admin'); + const idPhotoFileId = + photos.find((f) => f.bizType === 'USER_ID_PHOTO' || f.bizType === 'EMPLOYEE_ID_PHOTO')?.id || null; + const idFrontFileId = photos.find((f) => f.bizType === 'EMPLOYEE_ID_FRONT')?.id || null; + const idBackFileId = photos.find((f) => f.bizType === 'EMPLOYEE_ID_BACK')?.id || null; + return { + id: user.id, + username: user.username, + displayName: user.displayName, + avatarFileId: photos.find((f) => f.bizType === 'USER_AVATAR')?.id || null, + idPhotoFileId, + idFrontFileId, + idBackFileId, + roles, + permissions, + employeeId: user.employeeId, + mustChangePassword: isAdmin ? false : user.mustChangePassword, + profileCompletedAt: user.profileCompletedAt, + needsOnboard: needsOnboardGate({ + isAdmin, + mustChangePassword: user.mustChangePassword, + profileCompletedAt: user.profileCompletedAt, + employeeId: user.employeeId, + idPhotoFileId, + idFrontFileId, + idBackFileId, + employee: user.employee, + }), + dataScope: isAdmin || roles.includes('owner') + ? 'ALL' + : user.roles.reduce( + (best, r) => { + const rank = { SELF: 0, DEPT: 1, ALL: 2 } as Record; + return (rank[r.role.dataScope] || 0) > (rank[best] || 0) ? r.role.dataScope : best; + }, + 'SELF', + ), + employee: user.employee, + }; + } + + private async issueTokens( + user: { + id: string; + username: string; + displayName: string; + employeeId?: string | null; + mustChangePassword?: boolean; + profileCompletedAt?: Date | null; + roles: { role: { code: string; permissions: { permission: { code: string; permType: string; path: string | null; name: string; icon: string | null; parentId: string | null; sortNo: number; visible: boolean; id: string } }[] } }[]; + employee: unknown; + }, + clientKind: ClientKind, + opts?: { keepRefresh?: { id: string; tokenHash: string; expiresAt: Date } }, + ) { + if (clientKind === 'web' || clientKind === 'desktop') { + await this.prisma.refreshToken.updateMany({ + where: { userId: user.id, clientKind, revokedAt: null }, + data: { revokedAt: new Date() }, + }); + } + const accessTtl = clientKind === 'mobile' ? '7d' : '12h'; + const refreshTtl = clientKind === 'mobile' ? '3650d' : '2d'; + const sessionId = opts?.keepRefresh?.id || randomUUID(); + const accessToken = await this.jwt.signAsync( + { sub: user.id, username: user.username, typ: 'access', sid: sessionId, ck: clientKind }, + { + secret: this.config.get('JWT_ACCESS_SECRET'), + expiresIn: accessTtl, + }, + ); + let refreshToken = ''; + if (opts?.keepRefresh) { + refreshToken = ''; + } else { + refreshToken = await this.jwt.signAsync( + { sub: user.id, typ: 'refresh', jti: randomUUID(), ck: clientKind }, + { + secret: this.config.get('JWT_REFRESH_SECRET'), + expiresIn: refreshTtl, + }, + ); + const days = clientKind === 'mobile' ? 3650 : 2; + await this.prisma.refreshToken.create({ + data: { + id: sessionId, + userId: user.id, + tokenHash: sha256(refreshToken), + clientKind, + expiresAt: new Date(Date.now() + days * 24 * 3600 * 1000), + }, + }); + } + const permissions = [ + ...new Set( + user.roles.flatMap((r) => r.role.permissions.map((p) => p.permission)), + ), + ]; + const photos = await this.prisma.fileAsset.findMany({ + where: { + tenantId: '1', + OR: [ + { bizId: user.id, bizType: { in: ['USER_AVATAR', 'USER_ID_PHOTO'] } }, + user.employeeId + ? { bizId: user.employeeId, bizType: { in: ['EMPLOYEE_ID_PHOTO', 'EMPLOYEE_ID_FRONT', 'EMPLOYEE_ID_BACK'] } } + : undefined, + ].filter(Boolean) as Prisma.FileAssetWhereInput[], + }, + orderBy: { createdAt: 'desc' }, + }); + const roles = elevateToAdmin(user.roles.map((r) => r.role.code)); + const isAdmin = roles.includes('admin'); + const idPhotoFileId = + photos.find((f) => f.bizType === 'USER_ID_PHOTO' || f.bizType === 'EMPLOYEE_ID_PHOTO')?.id || null; + const idFrontFileId = photos.find((f) => f.bizType === 'EMPLOYEE_ID_FRONT')?.id || null; + const idBackFileId = photos.find((f) => f.bizType === 'EMPLOYEE_ID_BACK')?.id || null; + return { + accessToken, + refreshToken: refreshToken || undefined, + clientKind, + user: { + id: user.id, + username: user.username, + displayName: user.displayName, + avatarFileId: photos.find((f) => f.bizType === 'USER_AVATAR')?.id || null, + idPhotoFileId, + idFrontFileId, + idBackFileId, + roles, + employeeId: user.employeeId, + mustChangePassword: isAdmin ? false : Boolean(user.mustChangePassword), + profileCompletedAt: user.profileCompletedAt, + needsOnboard: needsOnboardGate({ + isAdmin, + mustChangePassword: user.mustChangePassword, + profileCompletedAt: user.profileCompletedAt, + employeeId: user.employeeId, + idPhotoFileId, + idFrontFileId, + idBackFileId, + employee: user.employee as { idNo?: string | null; idType?: string | null } | null, + }), + employee: user.employee, + permissions: permissions.map((p) => p.code), + }, + }; + } + + async startQrLogin() { + const ticket = randomUUID().replace(/-/g, '').slice(0, 16); + const expiresAt = new Date(Date.now() + 2 * 60 * 1000); + await this.prisma.authQrTicket.create({ + data: { ticket, expiresAt }, + }); + return { + ticket, + expiresAt, + payload: `fengyingoa://qr?ticket=${ticket}`, + }; + } + + async qrStatus(ticket: string) { + const row = await this.prisma.authQrTicket.findFirst({ where: { ticket: ticket.trim() } }); + if (!row) throw new UnauthorizedException('二维码无效'); + if (row.consumedAt || row.expiresAt < new Date()) { + if (row.status === 'pending') { + await this.prisma.authQrTicket.update({ where: { id: row.id }, data: { status: 'expired' } }); + } + return { status: row.consumedAt ? 'consumed' : 'expired' }; + } + if (row.status !== 'confirmed' || !row.userId) { + return { status: row.status }; + } + // 扫码只证明是本人的手机;还需把手机屏幕上的一次性六码填到电脑, + // 才签发桌面端令牌,避免旁人代扫后直接登录电脑。 + return { status: 'code_required', expiresAt: row.expiresAt }; + } + + async completeQrLogin(ticket: string, code: string) { + const rawTicket = ticket.trim(); + const rawCode = code.trim(); + if (!/^\d{6}$/.test(rawCode)) throw new BadRequestException('请输入手机显示的六位登录码'); + const row = await this.prisma.authQrTicket.findFirst({ where: { ticket: rawTicket } }); + if (!row || row.expiresAt < new Date()) { + throw new BadRequestException('二维码已过期,请刷新后重新扫码'); + } + if (row.status !== 'confirmed' || !row.userId || !row.confirmCodeHash) { + throw new BadRequestException('请先在手机端扫码确认'); + } + if (row.confirmCodeAttempts >= 5) { + await this.prisma.authQrTicket.update({ + where: { id: row.id }, + data: { status: 'expired' }, + }); + throw new BadRequestException('登录码错误次数过多,请刷新二维码'); + } + if (sha256(`${rawTicket}:${rawCode}`) !== row.confirmCodeHash) { + await this.prisma.authQrTicket.update({ + where: { id: row.id }, + data: { confirmCodeAttempts: { increment: 1 } }, + }); + throw new BadRequestException('六位登录码不正确'); + } + const user = await this.prisma.user.findUnique({ + where: { id: row.userId }, + include: { + roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } }, + employee: { include: { department: true } }, + }, + }); + if (!user || user.status !== 'ACTIVE') throw new UnauthorizedException('账号不可用'); + await this.prisma.authQrTicket.update({ + where: { id: row.id }, + data: { status: 'consumed', consumedAt: new Date() }, + }); + const tokens = await this.issueTokens(user, 'desktop'); + return { status: 'ok', ...tokens }; + } + + async confirmQrLogin(userId: string, ticket: string) { + const row = await this.prisma.authQrTicket.findFirst({ where: { ticket: ticket.trim() } }); + if (!row || row.expiresAt < new Date()) throw new BadRequestException('二维码已过期,请在电脑上刷新'); + if (row.status !== 'pending') throw new BadRequestException('这个码已经用过了'); + const code = String(randomInt(0, 1_000_000)).padStart(6, '0'); + await this.prisma.authQrTicket.update({ + where: { id: row.id }, + data: { + status: 'confirmed', + userId, + confirmCodeHash: sha256(`${row.ticket}:${code}`), + confirmCodeIssuedAt: new Date(), + confirmCodeAttempts: 0, + }, + }); + return { ok: true, code, expiresAt: row.expiresAt }; + } +} + +type ClientKind = 'web' | 'mobile' | 'desktop'; + +function normalizeClientKind(raw?: string | null): ClientKind { + const v = (raw || 'web').toLowerCase(); + if (v === 'mobile' || v === 'android' || v === 'ios' || v === 'app') return 'mobile'; + if (v === 'desktop' || v === 'windows' || v === 'pc') return 'desktop'; + return 'web'; +} diff --git a/apps/api/src/auth/jwt.strategy.ts b/apps/api/src/auth/jwt.strategy.ts new file mode 100644 index 0000000..b726232 --- /dev/null +++ b/apps/api/src/auth/jwt.strategy.ts @@ -0,0 +1,71 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ExtractJwt, Strategy } from 'passport-jwt'; +import { ConfigService } from '@nestjs/config'; +import { PrismaService } from '../prisma/prisma.service'; +import { AuthUser } from '../common/decorators/current-user.decorator'; +import { mergeDataScope } from '../common/data-scope'; +import { elevateToAdmin } from '../common/role-groups'; + +export interface JwtPayload { + sub: string; + username: string; + typ: 'access'; + sid?: string; + ck?: string; +} + +@Injectable() +export class JwtStrategy extends PassportStrategy(Strategy) { + constructor( + config: ConfigService, + private readonly prisma: PrismaService, + ) { + super({ + jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), + secretOrKey: config.get('JWT_ACCESS_SECRET')!, + }); + } + + async validate(payload: JwtPayload): Promise { + const user = await this.prisma.user.findUnique({ + where: { id: payload.sub }, + include: { + roles: { include: { role: { include: { permissions: { include: { permission: true } } } } } }, + employee: true, + }, + }); + if (!user || user.status !== 'ACTIVE') { + throw new UnauthorizedException('登录已失效'); + } + if (payload.sid) { + const session = await this.prisma.refreshToken.findFirst({ + where: { id: payload.sid, userId: user.id, revokedAt: null }, + }); + if (!session || session.expiresAt < new Date()) { + throw new UnauthorizedException('登录已失效'); + } + } + const roles = elevateToAdmin(user.roles.map((r: { role: { code: string } }) => r.role.code)); + const permissions = [ + ...new Set( + user.roles.flatMap((r: { role: { permissions: { permission: { code: string } }[] } }) => + r.role.permissions.map((p) => p.permission.code), + ), + ), + ]; + const dataScope = roles.includes('admin') + ? 'ALL' + : mergeDataScope(user.roles.map((r: { role: { dataScope: string } }) => r.role.dataScope)); + return { + id: user.id, + username: user.username, + displayName: user.displayName, + roles, + permissions, + employeeId: user.employeeId, + departmentId: user.employee?.departmentId ?? null, + dataScope, + }; + } +} diff --git a/apps/api/src/bidding/bid-workflow.spec.ts b/apps/api/src/bidding/bid-workflow.spec.ts new file mode 100644 index 0000000..8c5a2f6 --- /dev/null +++ b/apps/api/src/bidding/bid-workflow.spec.ts @@ -0,0 +1,161 @@ +import { + ASSIGN_ROLE, + canAccessBidModule, + canAssignFetcher, + canCreateBid, + canCreateSpecialBid, + canFetchBid, + canFetchThisBid, + canManageDraft, + canManageThisDraft, + canSeeAllBids, + canSeeTerminated, + canTerminateBid, + canTerminateFrom, + currentHandlerNames, + isCommercialStage, + isSpecialBid, + isSpecialCoordinatorUser, + stageForStatus, + previousBidStatus, + inferLegacyRestoreStatus, +} from './bid-workflow'; + +describe('bid-workflow', () => { + it('maps status to review stage', () => { + expect(stageForStatus('TECH_INITIAL')).toBe('TECH_INITIAL'); + expect(stageForStatus('TECH_FINAL')).toBe('TECH_FINAL'); + expect(stageForStatus('REVIEW1')).toBe('REVIEW1'); + expect(stageForStatus('REVIEW2')).toBe('REVIEW2'); + expect(stageForStatus('FETCH')).toBeNull(); + expect(stageForStatus('DRAFTING')).toBeNull(); + }); + + it('treats commercial reject as bounce, not terminate', () => { + expect(isCommercialStage('REVIEW1')).toBe(true); + expect(isCommercialStage('REVIEW2')).toBe(true); + expect(isCommercialStage('TECH_INITIAL')).toBe(false); + }); + + it('lets every director see all bids', () => { + expect(canSeeAllBids(['rd_director'])).toBe(true); + expect(canSeeAllBids(['3d_director'])).toBe(true); + expect(canSeeAllBids(['video_director'])).toBe(true); + expect(canSeeAllBids(['material_director'])).toBe(true); + expect(canSeeAllBids(['biz_director'])).toBe(true); + expect(canSeeAllBids(['tech_director'])).toBe(true); + expect(canSeeAllBids(['owner'])).toBe(true); + expect(canSeeAllBids(['admin'])).toBe(true); + expect(canSeeAllBids(['biz_staff'])).toBe(true); + expect(canSeeAllBids(['employee'])).toBe(false); + expect(canSeeAllBids(['pm'])).toBe(false); + }); + + it('hides terminated bids from ordinary employees', () => { + expect(canSeeTerminated(['employee'], ['office'])).toBe(false); + expect(canSeeTerminated(['hr'], ['office', 'hr'])).toBe(false); + expect(canSeeTerminated(['finance'], ['bid:result'])).toBe(false); + expect(canSeeTerminated(['biz_staff'], ['bid:screening'])).toBe(false); + expect(canSeeTerminated(['biz_director'], ['bid:terminated'])).toBe(true); + expect(canSeeTerminated(['tech_director'], ['bid'])).toBe(true); + expect(canSeeTerminated(['owner'], [])).toBe(true); + expect(canSeeTerminated(['admin'], [])).toBe(true); + }); + + it('restricts create / fetch / terminate by role', () => { + expect(canCreateBid(['owner'])).toBe(true); + expect(canCreateBid(['tech_director'])).toBe(false); + expect(canCreateBid(['employee'], ['bid:screening'])).toBe(true); + expect(canFetchBid(['biz_staff'])).toBe(true); + expect(canFetchBid(['tech_director'])).toBe(false); + expect(canTerminateBid(['biz_director'])).toBe(true); + expect(canTerminateBid(['biz_staff'])).toBe(false); + expect(canManageDraft(['biz_director'])).toBe(true); + expect(canManageDraft(['biz_staff'])).toBe(false); + }); + + it('keeps special screening on named coordinators', () => { + expect(isSpecialBid('SPECIAL')).toBe(true); + expect(isSpecialBid('FLOW')).toBe(false); + expect(canCreateSpecialBid(['owner'])).toBe(true); + expect(canCreateSpecialBid(['biz_director'])).toBe(false); + expect(isSpecialCoordinatorUser({ username: 'liuyunze', displayName: '刘运泽' })).toBe(true); + expect(isSpecialCoordinatorUser({ username: 'chencheng', displayName: '陈铖' })).toBe(true); + expect(isSpecialCoordinatorUser({ username: 'other', displayName: '其他人' })).toBe(false); + const coordinators = [{ userId: 'liu', role: ASSIGN_ROLE.COORDINATOR }]; + const fetchers = [{ userId: 'staff', role: ASSIGN_ROLE.FETCHER }]; + expect(canAssignFetcher(['3d_director'], 'SPECIAL', coordinators, 'liu', 'FETCH')).toBe(true); + expect(canAssignFetcher(['3d_director'], 'SPECIAL', coordinators, 'other', 'FETCH')).toBe(false); + expect(canAssignFetcher(['biz_director'], 'FLOW', [], 'x', 'FETCH')).toBe(true); + expect(canAssignFetcher(['biz_staff'], 'FLOW', [], 'x', 'FETCH')).toBe(false); + expect(canFetchThisBid(['employee'], 'SPECIAL', fetchers, 'staff')).toBe(true); + expect(canFetchThisBid(['biz_staff'], 'SPECIAL', fetchers, 'other')).toBe(false); + expect(canFetchThisBid(['biz_staff'], 'FLOW', [], 'other')).toBe(false); + expect(canFetchThisBid(['employee'], 'FLOW', fetchers, 'staff')).toBe(true); + expect(canManageThisDraft(['3d_director'], 'SPECIAL', coordinators, 'liu')).toBe(true); + expect(canManageThisDraft(['biz_director'], 'SPECIAL', coordinators, 'biz')).toBe(false); + expect(canManageThisDraft(['biz_director'], 'FLOW', [], 'biz')).toBe(true); + }); + + it('allows terminate until execution, not after open', () => { + expect(canTerminateFrom('TECH_INITIAL')).toBe(true); + expect(canTerminateFrom('DRAFTING')).toBe(true); + expect(canTerminateFrom('LIST')).toBe(true); + expect(canTerminateFrom('PENDING')).toBe(false); + expect(canTerminateFrom('WON')).toBe(false); + }); + + it('shows the person who should act on the current step', () => { + expect( + currentHandlerNames({ + status: 'TECH_INITIAL', + reviewCycle: 1, + createdBy: { displayName: '潘兴辉' }, + reviewers: [ + { stage: 'TECH_INITIAL', result: 'PENDING', cycle: 1, user: { displayName: '戴永康' } }, + ], + }), + ).toBe('戴永康'); + expect( + currentHandlerNames({ + status: 'FETCH', + assignees: [{ role: 'FETCHER', user: { displayName: '何烨' } }], + createdBy: { displayName: '潘兴辉' }, + }), + ).toBe('何烨'); + expect( + currentHandlerNames({ + status: 'DRAFTING', + assignees: [{ role: 'DRAFTER', user: { displayName: '张婧妍' } }], + }), + ).toBe('张婧妍'); + expect(currentHandlerNames({ status: 'PRINT', createdBy: { displayName: '潘兴辉' } })).toBe('商务部'); + expect(currentHandlerNames({ status: 'FETCH', createdBy: { displayName: '潘兴辉' } })).toBe('待派取标人'); + }); + + it('rolls back one process step, except special fetch', () => { + expect(previousBidStatus('FETCH', 'FLOW')).toBe('TECH_INITIAL'); + expect(previousBidStatus('FETCH', 'SPECIAL')).toBeNull(); + expect(previousBidStatus('TECH_FINAL')).toBe('FETCH'); + expect(previousBidStatus('DRAFTING')).toBe('TECH_FINAL'); + expect(previousBidStatus('REVIEW1')).toBe('DRAFTING'); + expect(previousBidStatus('REVIEW2')).toBe('REVIEW1'); + expect(previousBidStatus('PRINT')).toBe('REVIEW2'); + expect(previousBidStatus('LIST')).toBe('PRINT'); + expect(previousBidStatus('PENDING')).toBe('LIST'); + expect(previousBidStatus('TECH_INITIAL')).toBeNull(); + expect(previousBidStatus('WON')).toBeNull(); + }); + + it('infers the restore step for legacy terminated records without an action log', () => { + expect(inferLegacyRestoreStatus([], [])).toBe('DRAFTING'); + expect( + inferLegacyRestoreStatus( + [{ stage: 'TECH_INITIAL', result: 'APPROVED' }], + [{ role: 'FETCHER' }, { role: 'DRAFTER' }], + ), + ).toBe('DRAFTING'); + expect(inferLegacyRestoreStatus([], [{ role: 'BIDDER' }])).toBe('PENDING'); + expect(inferLegacyRestoreStatus([{ stage: 'REVIEW2', result: 'PENDING' }], [])).toBe('REVIEW2'); + }); +}); diff --git a/apps/api/src/bidding/bid-workflow.ts b/apps/api/src/bidding/bid-workflow.ts new file mode 100644 index 0000000..63a95dd --- /dev/null +++ b/apps/api/src/bidding/bid-workflow.ts @@ -0,0 +1,336 @@ +import { DIRECTOR_ROLES, hasAnyRole, isTechLead } from '../common/role-groups'; + +export const BID_STATUS = { + TECH_INITIAL: 'TECH_INITIAL', + FETCH: 'FETCH', + TECH_FINAL: 'TECH_FINAL', + DRAFTING: 'DRAFTING', + REVIEW1: 'REVIEW1', + REVIEW2: 'REVIEW2', + PRINT: 'PRINT', + LIST: 'LIST', + PENDING: 'PENDING', + WON: 'WON', + LOST: 'LOST', + FAILED: 'FAILED', + TERMINATED: 'TERMINATED', +} as const; + +export type BidStatus = (typeof BID_STATUS)[keyof typeof BID_STATUS]; + +export const STATUS_BUCKET: Record = { + screening: [BID_STATUS.TECH_INITIAL, BID_STATUS.FETCH, BID_STATUS.TECH_FINAL], + drafting: [BID_STATUS.DRAFTING, BID_STATUS.REVIEW1, BID_STATUS.REVIEW2, BID_STATUS.PRINT], + execution: [BID_STATUS.LIST, BID_STATUS.PENDING], + result: [BID_STATUS.WON, BID_STATUS.LOST, BID_STATUS.FAILED], + terminated: [BID_STATUS.TERMINATED], +}; + +export const REVIEW_STAGE = { + TECH_INITIAL: 'TECH_INITIAL', + TECH_FINAL: 'TECH_FINAL', + REVIEW1: 'REVIEW1', + REVIEW2: 'REVIEW2', +} as const; + +export type ReviewStage = (typeof REVIEW_STAGE)[keyof typeof REVIEW_STAGE]; + +export const BID_SOURCE = { + FLOW: 'FLOW', + SPECIAL: 'SPECIAL', +} as const; + +export const ASSIGN_ROLE = { + DRAFTER: 'DRAFTER', + REVIEW1: 'REVIEW1', + BIDDER: 'BIDDER', + FETCHER: 'FETCHER', + COORDINATOR: 'COORDINATOR', +} as const; + +/** 特殊筛选只发给这两人,由他们派取标人 / 制标人。 */ +export const SPECIAL_COORDINATORS = [ + { username: 'liuyunze', name: '刘运泽' }, + { username: 'chencheng', name: '陈铖' }, +] as const; + +export const BIZ_TYPE = { + TECH_INITIAL: 'BID_TECH_INITIAL', + TECH_FINAL: 'BID_TECH_FINAL', + FETCH: 'BID_FETCH', + ASSIGN_FETCH: 'BID_ASSIGN_FETCH', + ASSIGN_DRAFT: 'BID_ASSIGN_DRAFT', + DRAFT: 'BID_DRAFT', + REVIEW1: 'BID_REVIEW1', + REVIEW2: 'BID_REVIEW2', + PRINT: 'BID_PRINT', + COMPLETE: 'BID_COMPLETE', + OPEN: 'BID_OPEN', + CONVERT_CONTRACT: 'CONTRACT_DRAFT', +} as const; + +export const OPEN_RESULT = { + WON: 'WON', + LOST: 'LOST', + FAILED: 'FAILED', +} as const; + +export const COST_DISPOSITION = { + DEPT: 'DEPT', + MARKETING: 'MARKETING', +} as const; + +export function stageForStatus(status: string): ReviewStage | null { + if (status === BID_STATUS.TECH_INITIAL) return REVIEW_STAGE.TECH_INITIAL; + if (status === BID_STATUS.TECH_FINAL) return REVIEW_STAGE.TECH_FINAL; + if (status === BID_STATUS.REVIEW1) return REVIEW_STAGE.REVIEW1; + if (status === BID_STATUS.REVIEW2) return REVIEW_STAGE.REVIEW2; + return null; +} + +/** + * Old OA imports did not always contain the TERMINATED action log. Infer the + * furthest assigned workflow step so those records can still be restored. + */ +export function inferLegacyRestoreStatus( + reviewers: { stage: string; result?: string | null }[], + assignees: { role: string }[], +): BidStatus { + const candidates: BidStatus[] = []; + for (const reviewer of reviewers) { + if (reviewer.result && reviewer.result !== 'PENDING') continue; + if (reviewer.stage === REVIEW_STAGE.TECH_INITIAL) candidates.push(BID_STATUS.TECH_INITIAL); + if (reviewer.stage === REVIEW_STAGE.TECH_FINAL) candidates.push(BID_STATUS.TECH_FINAL); + if (reviewer.stage === REVIEW_STAGE.REVIEW1) candidates.push(BID_STATUS.REVIEW1); + if (reviewer.stage === REVIEW_STAGE.REVIEW2) candidates.push(BID_STATUS.REVIEW2); + } + for (const assignee of assignees) { + if (assignee.role === ASSIGN_ROLE.FETCHER) candidates.push(BID_STATUS.FETCH); + if (assignee.role === ASSIGN_ROLE.DRAFTER) candidates.push(BID_STATUS.DRAFTING); + if (assignee.role === ASSIGN_ROLE.REVIEW1) candidates.push(BID_STATUS.REVIEW1); + if (assignee.role === ASSIGN_ROLE.BIDDER) candidates.push(BID_STATUS.PENDING); + } + const order: BidStatus[] = [ + BID_STATUS.TECH_INITIAL, + BID_STATUS.FETCH, + BID_STATUS.TECH_FINAL, + BID_STATUS.DRAFTING, + BID_STATUS.REVIEW1, + BID_STATUS.REVIEW2, + BID_STATUS.PRINT, + BID_STATUS.LIST, + BID_STATUS.PENDING, + ]; + return candidates.sort((a, b) => order.indexOf(b) - order.indexOf(a))[0] ?? BID_STATUS.DRAFTING; +} + +export function isCommercialStage(stage: string) { + return stage === REVIEW_STAGE.REVIEW1 || stage === REVIEW_STAGE.REVIEW2; +} + +export function approvalBizType(stage: string) { + if (stage === REVIEW_STAGE.TECH_FINAL) return BIZ_TYPE.TECH_FINAL; + if (stage === REVIEW_STAGE.REVIEW1) return BIZ_TYPE.REVIEW1; + if (stage === REVIEW_STAGE.REVIEW2) return BIZ_TYPE.REVIEW2; + return BIZ_TYPE.TECH_INITIAL; +} + +export function canAccessBidModule(roles: string[], permissions: string[]) { + if (roles.includes('admin')) return true; + return permissions.some((p) => p === 'bid' || p.startsWith('bid:')); +} + +/** 管理员、负责人、商务部(总监/专员)、全体技术总监可看全部投标。 */ +export function canSeeAllBids(roles: string[]) { + return hasAnyRole(roles, ['admin', 'owner', 'biz_staff', ...DIRECTOR_ROLES]); +} + +export function canSeeTerminated(roles: string[], permissions: string[]) { + if (roles.includes('admin') || roles.includes('owner') || roles.includes('biz_director')) { + return true; + } + if (isTechLead(roles)) return true; + return permissions.includes('bid:terminated'); +} + +export function canCreateBid(roles: string[], permissions: string[] = []) { + if (['admin', 'owner', 'biz_director', 'biz_staff'].some((r) => roles.includes(r))) return true; + return permissions.includes('bid:screening'); +} + +export function canCreateSpecialBid(roles: string[]) { + return ['admin', 'owner'].some((r) => roles.includes(r)); +} + +export function isSpecialBid(sourceType?: string | null) { + return sourceType === BID_SOURCE.SPECIAL; +} + +export function isSpecialCoordinatorUser(user: { username?: string | null; displayName?: string | null }) { + const username = (user.username || '').trim().toLowerCase(); + const name = (user.displayName || '').trim(); + return SPECIAL_COORDINATORS.some( + (c) => c.username === username || c.name === name, + ); +} + +export function hasAssigneeRole( + assignees: { userId: string; role: string }[], + userId: string, + role: string, +) { + return assignees.some((a) => a.userId === userId && a.role === role); +} + +export function canFetchBid(roles: string[]) { + return ['admin', 'owner', 'biz_director', 'biz_staff'].some((r) => roles.includes(r)); +} + +export function canFetchThisBid( + roles: string[], + sourceType: string | null | undefined, + assignees: { userId: string; role: string }[], + userId: string, +) { + // 普通/特殊筛选都必须由被派的取标人办理。过去普通筛选把整个商务部 + // 当成取标人,既会群发短信,也会让任意人越权上传或完成取标。 + return hasAssigneeRole(assignees, userId, ASSIGN_ROLE.FETCHER) || roles.includes('admin'); +} + +export function canManageDraft(roles: string[]) { + return ['admin', 'owner', 'biz_director'].some((r) => roles.includes(r)); +} + +export function canAssignFetcher( + roles: string[], + sourceType: string | null | undefined, + assignees: { userId: string; role: string }[], + userId: string, + status: string, +) { + if (status !== BID_STATUS.FETCH) return false; + if (isSpecialBid(sourceType)) { + return roles.includes('admin') || hasAssigneeRole(assignees, userId, ASSIGN_ROLE.COORDINATOR); + } + return ['admin', 'owner', 'biz_director'].some((role) => roles.includes(role)); +} + +export function canManageThisDraft( + roles: string[], + sourceType: string | null | undefined, + assignees: { userId: string; role: string }[], + userId: string, +) { + if (isSpecialBid(sourceType)) { + return roles.includes('admin') || hasAssigneeRole(assignees, userId, ASSIGN_ROLE.COORDINATOR); + } + return canManageDraft(roles); +} + +export function canBizOperate(roles: string[]) { + return ['admin', 'owner', 'biz_director', 'biz_staff'].some((r) => roles.includes(r)); +} + +export function canTerminateBid(roles: string[]) { + return ['admin', 'owner', 'biz_director'].some((r) => roles.includes(r)); +} + +const TERMINABLE_STATUSES: string[] = [ + BID_STATUS.TECH_INITIAL, + BID_STATUS.FETCH, + BID_STATUS.TECH_FINAL, + BID_STATUS.DRAFTING, + BID_STATUS.REVIEW1, + BID_STATUS.REVIEW2, + BID_STATUS.PRINT, + BID_STATUS.LIST, +]; + +export function canTerminateFrom(status: string) { + return TERMINABLE_STATUSES.includes(status); +} + +const PREVIOUS_STATUS: Partial> = { + [BID_STATUS.FETCH]: BID_STATUS.TECH_INITIAL, + [BID_STATUS.TECH_FINAL]: BID_STATUS.FETCH, + [BID_STATUS.DRAFTING]: BID_STATUS.TECH_FINAL, + [BID_STATUS.REVIEW1]: BID_STATUS.DRAFTING, + [BID_STATUS.REVIEW2]: BID_STATUS.REVIEW1, + [BID_STATUS.PRINT]: BID_STATUS.REVIEW2, + [BID_STATUS.LIST]: BID_STATUS.PRINT, + [BID_STATUS.PENDING]: BID_STATUS.LIST, +}; + +/** 当前步骤办错了,退回上一步重做。特殊筛选没有技术初审,取标不能再往回退。 */ +export function previousBidStatus(status: string, sourceType?: string | null): BidStatus | null { + if (status === BID_STATUS.FETCH && isSpecialBid(sourceType)) return null; + return PREVIOUS_STATUS[status as BidStatus] || null; +} + +export function canRollbackBid(roles: string[]) { + return ['admin', 'owner', 'biz_director'].some((r) => roles.includes(r)); +} + +type Named = { displayName?: string | null } | null | undefined; + +function uniqNames(names: (string | null | undefined)[]) { + return [...new Set(names.map((n) => (n || '').trim()).filter(Boolean))]; +} + +/** 列表「经办」:当前这一步该谁办,就显示谁。 */ +export function currentHandlerNames(bid: { + status: string; + sourceType?: string | null; + reviewCycle?: number; + createdBy?: Named; + assignees?: { role: string; user?: Named }[]; + reviewers?: { stage: string; result: string; cycle: number; user?: Named }[]; +}) { + const cycle = bid.reviewCycle ?? 1; + const byRole = (role: string) => + uniqNames((bid.assignees || []).filter((a) => a.role === role).map((a) => a.user?.displayName)); + const pending = (stage: string) => { + const rows = bid.reviewers || []; + const atCycle = rows.filter((r) => r.stage === stage && r.result === 'PENDING' && r.cycle === cycle); + const anyPending = rows.filter((r) => r.stage === stage && r.result === 'PENDING'); + const pick = atCycle.length ? atCycle : anyPending; + return uniqNames(pick.map((r) => r.user?.displayName)); + }; + const show = (names: string[], fallback = '') => names.join('、') || fallback; + + switch (bid.status) { + case BID_STATUS.TECH_INITIAL: + return show(pending(REVIEW_STAGE.TECH_INITIAL), bid.createdBy?.displayName || '—'); + case BID_STATUS.FETCH: { + const fetchers = byRole(ASSIGN_ROLE.FETCHER); + if (fetchers.length) return show(fetchers); + if (isSpecialBid(bid.sourceType)) return show(byRole(ASSIGN_ROLE.COORDINATOR), '待派取标人'); + return '待派取标人'; + } + case BID_STATUS.TECH_FINAL: + return show(pending(REVIEW_STAGE.TECH_FINAL), '待终审'); + case BID_STATUS.DRAFTING: { + const drafters = byRole(ASSIGN_ROLE.DRAFTER); + if (drafters.length) return show(drafters); + if (isSpecialBid(bid.sourceType)) return show(byRole(ASSIGN_ROLE.COORDINATOR), '待派制标人'); + return '待派制标人'; + } + case BID_STATUS.REVIEW1: + return show([...pending(REVIEW_STAGE.REVIEW1), ...byRole(ASSIGN_ROLE.REVIEW1)], '商务一审'); + case BID_STATUS.REVIEW2: + return show(pending(REVIEW_STAGE.REVIEW2), '商务二审'); + case BID_STATUS.PRINT: + return '商务部'; + case BID_STATUS.LIST: + return show(byRole(ASSIGN_ROLE.BIDDER), '待派投标人'); + case BID_STATUS.PENDING: + case BID_STATUS.WON: + case BID_STATUS.LOST: + case BID_STATUS.FAILED: + return show(byRole(ASSIGN_ROLE.BIDDER), bid.createdBy?.displayName || '—'); + case BID_STATUS.TERMINATED: + return bid.createdBy?.displayName || '—'; + default: + return bid.createdBy?.displayName || '—'; + } +} diff --git a/apps/api/src/bidding/bidding.controller.ts b/apps/api/src/bidding/bidding.controller.ts new file mode 100644 index 0000000..cca8ee0 --- /dev/null +++ b/apps/api/src/bidding/bidding.controller.ts @@ -0,0 +1,389 @@ +import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; +import { + ArrayNotEmpty, + IsArray, + IsBoolean, + IsDefined, + IsIn, + MinLength, + IsOptional, + IsString, +} from 'class-validator'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { PageQueryDto } from '../common/dto/page-query.dto'; +import { BiddingService } from './bidding.service'; + +class CreateBidDto { + @IsString() + name!: string; + @IsString() + externalNo!: string; + @IsString() + projectType!: string; + @IsString() + source!: string; + @IsString() + qualificationNeed!: string; + @IsString() + legalEntityId!: string; + @IsOptional() + @IsString() + partyId?: string; + @IsOptional() + @IsString() + tenderMethod?: string; + @IsOptional() + applyStartAt?: string; + @IsOptional() + applyEndAt?: string; + @IsOptional() + @IsString() + applyMethod?: string; + @IsOptional() + openAt?: string; + @IsOptional() + @IsString() + openMethod?: string; + @IsOptional() + @IsString() + openPlace?: string; + @IsOptional() + @IsString() + remark?: string; + @IsOptional() + @IsString() + summary?: string; + @IsOptional() + @IsString() + nasPath?: string; + @IsOptional() + @IsString() + bondMethod?: string; + @IsOptional() + @IsString() + quoteAmount?: string; + @IsOptional() + @IsString() + priceCap?: string; + @IsArray() + @ArrayNotEmpty({ message: '请选择项目初审人或终审总监' }) + @IsString({ each: true }) + reviewerIds!: string[]; + @IsOptional() + @IsIn(['FLOW', 'SPECIAL']) + sourceType?: string; + @IsOptional() + @IsArray() + @IsString({ each: true }) + coordinatorIds?: string[]; + @IsOptional() + @Transform(({ value }) => value === true || value === 'true') + @IsBoolean() + needSurvey?: boolean; +} + +class ReviewDto { + @IsIn(['APPROVED', 'REJECTED']) + result!: 'APPROVED' | 'REJECTED'; + @IsOptional() + @IsString() + comment?: string; +} + +class FetchDto { + @IsOptional() + @IsString() + nasPath?: string; + @IsOptional() + @IsString() + remark?: string; + @IsOptional() + openAt?: string; + @IsOptional() + @IsString() + openMethod?: string; + @IsOptional() + @IsString() + openPlace?: string; + @IsOptional() + @IsString() + applyMethod?: string; +} + +class TerminateDto { + @IsString() + @MinLength(1, { message: '终止请填写原因' }) + comment!: string; +} + +class RollbackDto { + @IsString() + @MinLength(1, { message: '退回请填写原因' }) + comment!: string; +} + +class AssignFetcherDto { + @IsString() + fetcherId!: string; +} + +class AssignDrafterDto { + @IsString() + drafterId!: string; + @IsOptional() + @IsString() + review1Id?: string; +} + +class SubmitDraftDto { + @IsString() + @MinLength(1, { message: '请填写标书 NAS 路径' }) + nasPath!: string; + @IsOptional() + @IsString() + note?: string; +} + +class PrintDto { + @IsOptional() + amount?: number; + @IsOptional() + @IsString() + note?: string; + @IsOptional() + @IsString() + nasPath?: string; +} + +class CompleteDto { + @IsOptional() + @IsString() + bondMethod?: string; + @IsOptional() + @IsString() + bondStatus?: string; + @IsOptional() + bondAmount?: number; + @IsOptional() + @IsString() + delivererName?: string; + @IsOptional() + @IsString() + priceCap?: string; + @IsOptional() + @IsString() + quoteAmount?: string; + @IsOptional() + @IsString() + securityLevel?: string; + @IsOptional() + @IsString() + contactName?: string; + @IsOptional() + @IsString() + contactPhone?: string; + @IsArray() + @ArrayNotEmpty({ message: '请派投标人' }) + @IsString({ each: true }) + bidderIds!: string[]; + @IsOptional() + openAt?: string; + @IsOptional() + @IsString() + openPlace?: string; + @IsOptional() + @IsString() + openMethod?: string; +} + +class OpenResultDto { + @IsIn(['WON', 'LOST', 'FAILED']) + result!: 'WON' | 'LOST' | 'FAILED'; + @IsOptional() + @IsString() + comment?: string; + @IsOptional() + @IsIn(['DEPT', 'MARKETING']) + costDisposition?: 'DEPT' | 'MARKETING'; + @IsOptional() + @IsString() + winnerName?: string; + @IsOptional() + winnerAmount?: number | string; +} + +class CompetitorDto { + @IsString() + @MinLength(1, { message: '请填写中标单位' }) + winnerName!: string; + @IsDefined({ message: '请填写中标金额' }) + winnerAmount!: number | string; + @IsOptional() + @IsString() + winnerNote?: string; +} + +class UpdateLegalEntityDto { + @IsString() + legalEntityId!: string; +} + +@ApiTags('bidding') +@ApiBearerAuth() +@Controller('bid-cases') +export class BiddingController { + constructor(private readonly bidding: BiddingService) {} + + @Get() + list( + @CurrentUser() user: AuthUser, + @Query() q: PageQueryDto, + @Query('bucket') bucket?: string, + @Query('sourceType') sourceType?: string, + @Query('projectType') projectType?: string, + @Query('status') status?: string, + @Query('legalEntityId') legalEntityId?: string, + ) { + return this.bidding.list(user, q.page, q.pageSize, q.keyword, bucket, sourceType, projectType, status, legalEntityId); + } + + @Get('special-coordinators') + specialCoordinators(@CurrentUser() user: AuthUser) { + return this.bidding.specialCoordinators(user); + } + + @Get(':id') + get(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: AuthUser) { + return this.bidding.get(id, user); + } + + @Post() + create(@Body() dto: CreateBidDto, @CurrentUser() user: AuthUser) { + return this.bidding.create(dto, user); + } + + @Patch(':id/survey') + setSurvey( + @Param('id', ParseUUIDPipe) id: string, + @Body() body: { needSurvey?: boolean }, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.setNeedSurvey(id, user, Boolean(body.needSurvey)); + } + + @Post(':id/reviews') + review( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: ReviewDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.review(id, user, dto); + } + + @Post(':id/fetch') + fetch( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: FetchDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.fetch(id, user, dto); + } + + @Post(':id/terminate') + terminate( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: TerminateDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.terminate(id, user, dto.comment); + } + + @Post(':id/rollback') + rollback( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: RollbackDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.rollback(id, user, dto.comment); + } + + @Post(':id/restore') + restore(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: AuthUser) { + return this.bidding.restoreTerminated(id, user); + } + + @Post(':id/assign-fetcher') + assignFetcher( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: AssignFetcherDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.assignFetcher(id, user, dto); + } + + @Post(':id/assign-drafter') + assignDrafter( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: AssignDrafterDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.assignDrafter(id, user, dto); + } + + @Post(':id/submit-draft') + submitDraft( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: SubmitDraftDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.submitDraft(id, user, dto); + } + + @Post(':id/print') + print( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: PrintDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.print(id, user, dto); + } + + @Post(':id/complete') + complete( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: CompleteDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.completeExecution(id, user, dto); + } + + @Post(':id/open-result') + openResult( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: OpenResultDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.openResult(id, user, dto); + } + + @Patch(':id/competitor') + competitor( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: CompetitorDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.recordCompetitor(id, user, dto); + } + + @Patch(':id/legal-entity') + updateLegalEntity( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: UpdateLegalEntityDto, + @CurrentUser() user: AuthUser, + ) { + return this.bidding.updateLegalEntity(id, user, dto.legalEntityId); + } +} diff --git a/apps/api/src/bidding/bidding.service.ts b/apps/api/src/bidding/bidding.service.ts new file mode 100644 index 0000000..57392d0 --- /dev/null +++ b/apps/api/src/bidding/bidding.service.ts @@ -0,0 +1,1810 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { emitSms } from '../sms/sms-bus'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { assertCanSeeOwner, ownerScope, withAnd } from '../common/data-scope'; +import { canSeeSubmittedFinance, EXPENSE_STATUS } from '../finance/finance-workflow'; +import { nextBondNo, nextExpenseClaimNo } from '../common/doc-no'; +import { decodeUploadFileName } from '../files/files.service'; +import { ensureWonLedger } from '../contract/won-ledger'; +import { findBizDirectors } from '../common/org-chain'; +import { + ASSIGN_ROLE, + BID_SOURCE, + BID_STATUS, + BIZ_TYPE, + COST_DISPOSITION, + OPEN_RESULT, + REVIEW_STAGE, + STATUS_BUCKET, + approvalBizType, + canAccessBidModule, + canAssignFetcher, + canBizOperate, + canCreateBid, + canCreateSpecialBid, + canFetchThisBid, + canManageThisDraft, + canSeeAllBids, + canSeeTerminated, + canTerminateBid, + canTerminateFrom, + currentHandlerNames, + isCommercialStage, + isSpecialBid, + inferLegacyRestoreStatus, + previousBidStatus, + canRollbackBid, + stageForStatus, +} from './bid-workflow'; + +const USER_SELECT = { id: true, displayName: true, username: true }; + +function fmtSmsTime(d?: Date | null) { + if (!d) return ''; + const x = new Date(d); + const p = (n: number) => String(n).padStart(2, '0'); + return `${x.getFullYear()}-${p(x.getMonth() + 1)}-${p(x.getDate())} ${p(x.getHours())}:${p(x.getMinutes())}`; +} + +function bidSmsParams( + bid: { + bidNo?: string | null; + name?: string | null; + openAt?: Date | null; + openPlace?: string | null; + contactName?: string | null; + contactPhone?: string | null; + }, + extra: Record = {}, +) { + return { + apply_no: bid.bidNo || '', + project_name: bid.name || '', + date: fmtSmsTime(bid.openAt), + location: bid.openPlace || '', + contact_person: bid.contactName || '', + contact_phone: bid.contactPhone || '', + content: bid.name || '', + ...extra, + }; +} + +const DETAIL_INCLUDE = { + party: true, + legalEntity: true, + createdBy: { select: USER_SELECT }, + actionLogs: { + include: { actor: { select: USER_SELECT } }, + orderBy: { createdAt: 'asc' as const }, + }, + assignees: { + include: { user: { select: USER_SELECT } }, + orderBy: { createdAt: 'asc' as const }, + }, + versions: { + include: { submittedBy: { select: USER_SELECT } }, + orderBy: { versionNo: 'desc' as const }, + }, + bonds: true, + expenses: true, + contracts: { + select: { + id: true, + contractNo: true, + name: true, + status: true, + amount: true, + projects: { select: { id: true, projectNo: true, name: true, status: true } }, + }, + orderBy: { createdAt: 'desc' as const }, + }, +}; + +type Tx = Prisma.TransactionClient; + +export type CreateBidInput = { + name: string; + externalNo: string; + projectType: string; + source?: string; + qualificationNeed?: string; + legalEntityId?: string; + partyId?: string; + tenderMethod?: string; + applyStartAt?: string; + applyEndAt?: string; + applyMethod?: string; + openAt?: string; + openMethod?: string; + openPlace?: string; + remark?: string; + summary?: string; + nasPath?: string; + bondMethod?: string; + quoteAmount?: string; + priceCap?: string; + reviewerIds: string[]; + sourceType?: string; + coordinatorIds?: string[]; + needSurvey?: boolean; +}; + +export type ReviewInput = { + result: 'APPROVED' | 'REJECTED'; + comment?: string; +}; + +export type FetchInput = { + nasPath?: string; + remark?: string; + openAt?: string; + openMethod?: string; + openPlace?: string; + applyMethod?: string; +}; + +export type AssignDrafterInput = { + drafterId: string; + review1Id?: string; +}; + +export type SubmitDraftInput = { + nasPath: string; + note?: string; +}; + +export type PrintInput = { + amount?: number; + note?: string; + nasPath?: string; +}; + +export type CompleteInput = { + bondMethod?: string; + bondStatus?: string; + bondAmount?: number; + delivererName?: string; + priceCap?: string; + quoteAmount?: string; + securityLevel?: string; + contactName?: string; + contactPhone?: string; + bidderIds: string[]; + openAt?: string; + openPlace?: string; + openMethod?: string; +}; + +export type OpenResultInput = { + result: 'WON' | 'LOST' | 'FAILED'; + comment?: string; + costDisposition?: 'DEPT' | 'MARKETING'; + winnerName?: string; + winnerAmount?: number | string; +}; + +@Injectable() +export class BiddingService { + constructor(private readonly prisma: PrismaService) {} + + assertModuleAccess(user: AuthUser) { + if (!canAccessBidModule(user.roles, user.permissions)) { + throw new ForbiddenException('无权访问投标管理'); + } + } + + async list( + user: AuthUser, + page: number, + pageSize: number, + keyword?: string, + bucket?: string, + sourceType?: string, + projectType?: string, + status?: string, + legalEntityId?: string, + ) { + this.assertModuleAccess(user); + const seeTerminated = canSeeTerminated(user.roles, user.permissions); + if (bucket === 'terminated' && !seeTerminated) { + throw new ForbiddenException('无权查看终止投标'); + } + const bucketStatuses = bucket ? STATUS_BUCKET[bucket] : undefined; + if (bucket && !bucketStatuses) { + throw new BadRequestException('未知的列表分桶'); + } + + const where: Prisma.BidCaseWhereInput = { tenantId: '1' }; + if (legalEntityId) where.legalEntityId = legalEntityId; + if (status) { + if (bucketStatuses && !bucketStatuses.includes(status)) { + throw new BadRequestException('该状态不在当前列表'); + } + where.status = status; + } else if (bucket === 'terminated') { + where.status = BID_STATUS.TERMINATED; + } else if (bucketStatuses) { + where.status = { in: bucketStatuses }; + } else if (!seeTerminated) { + where.status = { not: BID_STATUS.TERMINATED }; + } + if (sourceType === BID_SOURCE.SPECIAL || sourceType === BID_SOURCE.FLOW) { + where.sourceType = sourceType; + } else if (bucket !== 'execution' && bucket !== 'result' && bucket !== 'terminated') { + where.sourceType = { not: BID_SOURCE.SPECIAL }; + } + if (projectType) where.projectType = projectType; + if (keyword) { + where.AND = [ + { + OR: [ + { name: { contains: keyword } }, + { bidNo: { contains: keyword } }, + { externalNo: { contains: keyword } }, + ], + }, + ]; + } + const scoped = canSeeAllBids(user.roles) ? {} : await ownerScope(this.prisma, user, 'createdById'); + const visibility = Object.keys(scoped).length + ? { + OR: [ + scoped, + { assignees: { some: { userId: user.id } } }, + { reviewers: { some: { userId: user.id } } }, + ], + } + : {}; + const filtered = withAnd(where, visibility as Record); + + const [items, total] = await this.prisma.$transaction([ + this.prisma.bidCase.findMany({ + where: filtered, + include: { + party: true, + legalEntity: true, + createdBy: { select: USER_SELECT }, + assignees: { include: { user: { select: USER_SELECT } } }, + reviewers: { include: { user: { select: USER_SELECT } } }, + }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.bidCase.count({ where: filtered }), + ]); + return { + items: items.map((r) => ({ ...r, handlerName: currentHandlerNames(r) })), + page, + pageSize, + total, + }; + } + + async get(id: string, user: AuthUser) { + const bid = await this.prisma.bidCase.findFirst({ + where: { id, tenantId: '1' }, + include: { + ...DETAIL_INCLUDE, + reviewers: { + include: { user: { select: USER_SELECT } }, + orderBy: [{ cycle: 'asc' }, { stage: 'asc' }, { decidedAt: 'asc' }], + }, + }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + const assigned = bid.assignees.some((a) => a.userId === user.id); + const reviewing = bid.reviewers.some((r) => r.userId === user.id); + const involved = assigned || reviewing; + if (!canAccessBidModule(user.roles, user.permissions) && !involved) { + throw new ForbiddenException('无权访问投标管理'); + } + if (!involved && !canSeeAllBids(user.roles)) await assertCanSeeOwner(this.prisma, user, bid.createdById); + if ( + bid.status === BID_STATUS.TERMINATED && + !canSeeTerminated(user.roles, user.permissions) && + !assigned + ) { + throw new ForbiddenException('无权查看终止投标'); + } + const files = ( + await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizId: id, bizType: { startsWith: 'BID' } }, + orderBy: { createdAt: 'asc' }, + }) + ).map((f) => ({ ...f, fileName: decodeUploadFileName(f.fileName) })); + const expenses = (bid.expenses || []).filter((e) => { + if (e.applicantId === user.id) return true; + if (e.status === EXPENSE_STATUS.DRAFT) return false; + if (e.status === EXPENSE_STATUS.APPROVED) return true; + return canSeeSubmittedFinance(user.roles, user.permissions); + }); + return { ...bid, expenses, files, myActions: this.myActions(bid, user) }; + } + + async setNeedSurvey(id: string, user: AuthUser, needSurvey: boolean) { + this.assertModuleAccess(user); + const row = await this.prisma.bidCase.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('投标事项不存在'); + return this.prisma.bidCase.update({ + where: { id }, + data: { needSurvey: Boolean(needSurvey) }, + }); + } + + async specialCoordinators(user: AuthUser) { + this.assertModuleAccess(user); + return this.loadSpecialCoordinators(this.prisma); + } + + async create(dto: CreateBidInput, user: AuthUser) { + this.assertModuleAccess(user); + const special = isSpecialBid(dto.sourceType); + if (special) { + if (!canCreateSpecialBid(user.roles)) { + throw new ForbiddenException('特殊筛选只能由企业负责人发起'); + } + } else if (!canCreateBid(user.roles, user.permissions)) { + throw new ForbiddenException('无权发起项目筛选'); + } + const reviewerIds = [...new Set(dto.reviewerIds.filter(Boolean))]; + if (reviewerIds.length === 0) { + throw new BadRequestException(special ? '请选择终审总监' : '请选择项目初审人'); + } + const reviewers = await this.prisma.user.findMany({ + where: { id: { in: reviewerIds }, tenantId: '1', status: 'ACTIVE' }, + }); + if (reviewers.length !== reviewerIds.length) { + throw new BadRequestException(special ? '终审总监无效' : '初审人无效'); + } + const allowed = await this.loadSpecialCoordinators(this.prisma); + const coordinatorIds = special + ? [...new Set((dto.coordinatorIds || []).filter(Boolean))] + : []; + if (special) { + if (!coordinatorIds.length) { + throw new BadRequestException('请选择特殊筛选办理人'); + } + const allowedIds = new Set(allowed.map((u) => u.id)); + if (coordinatorIds.some((id) => !allowedIds.has(id))) { + throw new BadRequestException('特殊筛选办理人无效'); + } + } + + const bid = await this.prisma.$transaction(async (tx) => { + const tenderNo = (dto.externalNo || '').trim(); + if (!tenderNo) throw new BadRequestException('请填写招标编号(公告上的项目编号)'); + // 项目筛选只使用人工填写的公告编号。相同公告可能二次发布、 + // 分包或重复筛选,不能把它当成系统唯一编号拦截。 + const created = await tx.bidCase.create({ + data: { + tenantId: '1', + bidNo: tenderNo, + name: dto.name, + externalNo: tenderNo, + projectType: dto.projectType, + sourceType: special ? BID_SOURCE.SPECIAL : BID_SOURCE.FLOW, + source: dto.source, + qualificationNeed: dto.qualificationNeed, + legalEntityId: dto.legalEntityId, + partyId: dto.partyId || undefined, + tenderMethod: dto.tenderMethod, + applyStartAt: dto.applyStartAt ? new Date(dto.applyStartAt) : undefined, + applyEndAt: dto.applyEndAt ? new Date(dto.applyEndAt) : undefined, + applyMethod: dto.applyMethod, + openAt: dto.openAt ? new Date(dto.openAt) : undefined, + openMethod: dto.openMethod, + openPlace: dto.openPlace, + remark: dto.remark, + summary: dto.summary, + nasPath: dto.nasPath, + bondMethod: dto.bondMethod, + quoteAmount: dto.quoteAmount, + priceCap: dto.priceCap, + needSurvey: Boolean(dto.needSurvey), + status: special ? BID_STATUS.FETCH : BID_STATUS.TECH_INITIAL, + createdById: user.id, + reviewers: { + create: reviewerIds.map((userId) => ({ + userId, + stage: REVIEW_STAGE.TECH_INITIAL, + })), + }, + assignees: special + ? { + create: coordinatorIds.map((userId) => ({ + userId, + role: ASSIGN_ROLE.COORDINATOR, + })), + } + : undefined, + }, + }); + if (special) { + await this.createTodos( + tx, + created.id, + `特殊筛选派取标:${created.name}`, + BIZ_TYPE.ASSIGN_FETCH, + coordinatorIds.map((id) => ({ id })), + created.applyEndAt, + ); + await this.log( + tx, + created.id, + user.id, + 'CREATE_SPECIAL', + undefined, + BID_STATUS.FETCH, + `发起特殊筛选 ${created.externalNo || created.bidNo},发给 ${allowed + .filter((u) => coordinatorIds.includes(u.id)) + .map((u) => u.displayName) + .join('、')}`, + ); + } else { + await tx.approvalTask.createMany({ + data: reviewerIds.map((assigneeId) => ({ + tenantId: '1', + title: `技术初审会签:${created.name}`, + bizType: BIZ_TYPE.TECH_INITIAL, + bizId: created.id, + assigneeId, + })), + }); + emitSms('BID_TECH_INITIAL', { + userIds: reviewerIds, + params: bidSmsParams(created), + }); + await this.log(tx, created.id, user.id, 'CREATE', undefined, BID_STATUS.TECH_INITIAL, `发起筛选 ${created.externalNo || created.bidNo}`); + } + return created; + }); + return this.get(bid.id, user); + } + + async review(bidId: string, user: AuthUser, dto: ReviewInput) { + this.assertModuleAccess(user); + if (dto.result === 'REJECTED' && !dto.comment?.trim()) { + throw new BadRequestException('驳回请填写意见'); + } + await this.prisma.$transaction(async (tx) => { + const bid = await tx.bidCase.findFirst({ + where: { id: bidId, tenantId: '1' }, + include: { reviewers: true, assignees: true }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + const stage = stageForStatus(bid.status); + if (!stage) { + throw new BadRequestException('当前状态不可会签'); + } + const row = bid.reviewers.find( + (r) => + r.userId === user.id && + r.stage === stage && + r.result === 'PENDING' && + r.cycle === bid.reviewCycle, + ); + if (!row) { + throw new ForbiddenException('没有待办的会签'); + } + await tx.bidReviewer.update({ + where: { id: row.id }, + data: { + result: dto.result, + comment: dto.comment?.trim() || null, + decidedAt: new Date(), + }, + }); + await tx.approvalTask.updateMany({ + where: { + bizId: bid.id, + assigneeId: user.id, + status: 'PENDING', + bizType: approvalBizType(stage), + }, + data: { status: dto.result === 'APPROVED' ? 'APPROVED' : 'REJECTED' }, + }); + + if (dto.result === 'REJECTED') { + if (isCommercialStage(stage)) { + await this.bounceToDrafting(tx, bid, user, dto.comment!.trim(), stage); + return; + } + await this.closeAndTerminate( + tx, + bid, + user, + dto.comment!.trim(), + stage === REVIEW_STAGE.TECH_FINAL ? 'FINAL_REJECT' : 'INITIAL_REJECT', + ); + return; + } + + await this.log( + tx, + bid.id, + user.id, + this.approveAction(stage), + bid.status, + undefined, + dto.comment?.trim() || '同意', + ); + + const pending = await tx.bidReviewer.count({ + where: { + bidCaseId: bid.id, + stage, + cycle: bid.reviewCycle, + result: 'PENDING', + }, + }); + if (pending > 0) return; + + if (stage === REVIEW_STAGE.TECH_INITIAL) { + await tx.bidCase.update({ + where: { id: bid.id }, + data: { status: BID_STATUS.FETCH }, + }); + await this.log(tx, bid.id, user.id, 'INITIAL_PASSED', BID_STATUS.TECH_INITIAL, BID_STATUS.FETCH, '初审会签全部通过,进入取标'); + await this.createTodos( + tx, + bid.id, + `派取标人:${bid.name}`, + BIZ_TYPE.ASSIGN_FETCH, + await this.bizDeptDirectors(tx), + bid.applyEndAt, + ); + return; + } + + if (stage === REVIEW_STAGE.TECH_FINAL) { + await tx.bidCase.update({ + where: { id: bid.id }, + data: { status: BID_STATUS.DRAFTING }, + }); + await this.log(tx, bid.id, user.id, 'FINAL_PASSED', BID_STATUS.TECH_FINAL, BID_STATUS.DRAFTING, '技术终审通过,进入制标'); + const draftOwners = isSpecialBid(bid.sourceType) + ? bid.assignees + .filter((a) => a.role === ASSIGN_ROLE.COORDINATOR) + .map((a) => ({ id: a.userId })) + : [...(await this.bizDeptDirectors(tx)), ...(await this.usersByRoles(tx, ['owner']))]; + await this.createTodos( + tx, + bid.id, + isSpecialBid(bid.sourceType) ? `特殊制标派制标人:${bid.name}` : `派制标:${bid.name}`, + BIZ_TYPE.ASSIGN_DRAFT, + draftOwners, + ); + // 终审通过后的短信由 createTodos 仅发给本步骤指定的派制标人; + // 不再额外抄送固定人员,避免无关短信。 + return; + } + + if (stage === REVIEW_STAGE.REVIEW1) { + const directors = await this.bizDeptDirectors(tx); + const review2Ids = directors.length + ? directors.map((u) => u.id) + : (await this.usersByRoles(tx, ['owner'])).map((u) => u.id); + if (!review2Ids.length) { + throw new BadRequestException('没有可办理二审的商务总监'); + } + await tx.bidCase.update({ + where: { id: bid.id }, + data: { status: BID_STATUS.REVIEW2 }, + }); + await tx.bidReviewer.createMany({ + data: review2Ids.map((userId) => ({ + bidCaseId: bid.id, + userId, + stage: REVIEW_STAGE.REVIEW2, + cycle: bid.reviewCycle, + })), + }); + await tx.approvalTask.createMany({ + data: review2Ids.map((assigneeId) => ({ + tenantId: '1', + title: `商务二审:${bid.name}`, + bizType: BIZ_TYPE.REVIEW2, + bizId: bid.id, + assigneeId, + })), + }); + emitSms('BID_REVIEW', { userIds: review2Ids, params: bidSmsParams(bid) }); + await this.log(tx, bid.id, user.id, 'REVIEW1_PASSED', BID_STATUS.REVIEW1, BID_STATUS.REVIEW2, '一审通过,进入商务总监二审'); + return; + } + + await tx.bidCase.update({ + where: { id: bid.id }, + data: { status: BID_STATUS.PRINT }, + }); + await this.log(tx, bid.id, user.id, 'REVIEW2_PASSED', BID_STATUS.REVIEW2, BID_STATUS.PRINT, '二审通过,进入印封'); + await this.createTodos( + tx, + bid.id, + `印封:${bid.name}`, + BIZ_TYPE.PRINT, + [{ id: user.id }], + ); + }); + return this.get(bidId, user); + } + + async fetch(bidId: string, user: AuthUser, dto: FetchInput) { + await this.prisma.$transaction(async (tx) => { + const bid = await tx.bidCase.findFirst({ + where: { id: bidId, tenantId: '1' }, + include: { reviewers: true, assignees: true }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + if (bid.status !== BID_STATUS.FETCH) { + throw new BadRequestException('当前不是取标状态'); + } + if (!canFetchThisBid(user.roles, bid.sourceType, bid.assignees, user.id)) { + throw new ForbiddenException('只有被指定的取标人可以办理取标'); + } + if (!bid.assignees.some((a) => a.role === ASSIGN_ROLE.FETCHER)) { + throw new BadRequestException('请先由负责人指定取标人'); + } + await tx.bidCase.update({ + where: { id: bid.id }, + data: { + status: BID_STATUS.TECH_FINAL, + remark: dto.remark?.trim() || bid.remark, + applyMethod: dto.applyMethod || bid.applyMethod, + openAt: dto.openAt ? new Date(dto.openAt) : bid.openAt, + openMethod: dto.openMethod || bid.openMethod, + openPlace: dto.openPlace || bid.openPlace, + fetchedAt: new Date(), + }, + }); + await tx.todoTask.updateMany({ + where: { bizId: bid.id, bizType: { in: [BIZ_TYPE.FETCH, BIZ_TYPE.ASSIGN_FETCH] }, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + const initialReviewers = [ + ...new Set( + bid.reviewers + .filter((r) => r.stage === REVIEW_STAGE.TECH_INITIAL) + .map((r) => r.userId), + ), + ]; + for (const userId of initialReviewers) { + await tx.bidReviewer.create({ + data: { + bidCaseId: bid.id, + userId, + stage: REVIEW_STAGE.TECH_FINAL, + }, + }); + } + if (initialReviewers.length) { + await tx.approvalTask.createMany({ + data: initialReviewers.map((assigneeId) => ({ + tenantId: '1', + title: `技术终审:${bid.name}`, + bizType: BIZ_TYPE.TECH_FINAL, + bizId: bid.id, + assigneeId, + })), + }); + emitSms('BID_TECH_FINAL', { + userIds: initialReviewers, + params: bidSmsParams(bid), + }); + } + await this.log( + tx, + bid.id, + user.id, + 'FETCH_DONE', + BID_STATUS.FETCH, + BID_STATUS.TECH_FINAL, + '取标完成,进入技术终审', + ); + }); + return this.get(bidId, user); + } + + async assignFetcher(bidId: string, user: AuthUser, dto: { fetcherId: string }) { + await this.prisma.$transaction(async (tx) => { + const bid = await tx.bidCase.findFirst({ + where: { id: bidId, tenantId: '1' }, + include: { assignees: true }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + if (!canAssignFetcher(user.roles, bid.sourceType, bid.assignees, user.id, bid.status)) { + throw new ForbiddenException( + isSpecialBid(bid.sourceType) ? '只有被指定的特殊流程办理人可以指定取标人' : '只有商务总监可以指定取标人', + ); + } + const fetcher = await tx.user.findFirst({ + where: { id: dto.fetcherId, tenantId: '1', status: 'ACTIVE' }, + }); + if (!fetcher) throw new BadRequestException('取标人无效'); + await tx.bidAssignee.deleteMany({ + where: { bidCaseId: bid.id, role: ASSIGN_ROLE.FETCHER }, + }); + await tx.bidAssignee.create({ + data: { bidCaseId: bid.id, userId: fetcher.id, role: ASSIGN_ROLE.FETCHER }, + }); + await tx.todoTask.updateMany({ + where: { bizId: bid.id, bizType: { in: [BIZ_TYPE.ASSIGN_FETCH, BIZ_TYPE.FETCH] }, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + await tx.todoTask.create({ + data: { + tenantId: '1', + title: `${isSpecialBid(bid.sourceType) ? '特殊筛选' : ''}取标:${bid.name}`, + bizType: BIZ_TYPE.FETCH, + bizId: bid.id, + assigneeId: fetcher.id, + }, + }); + emitSms('BID_FETCH', { userIds: [fetcher.id], params: bidSmsParams(bid) }); + await this.log( + tx, + bid.id, + user.id, + 'ASSIGN_FETCHER', + bid.status, + bid.status, + `派取标人 ${fetcher.displayName}`, + ); + }); + return this.get(bidId, user); + } + + async assignDrafter(bidId: string, user: AuthUser, dto: AssignDrafterInput) { + this.assertModuleAccess(user); + await this.prisma.$transaction(async (tx) => { + const bid = await tx.bidCase.findFirst({ + where: { id: bidId, tenantId: '1' }, + include: { assignees: true }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + if (bid.status !== BID_STATUS.DRAFTING) { + throw new BadRequestException('当前不是制标状态'); + } + if (!canManageThisDraft(user.roles, bid.sourceType, bid.assignees, user.id)) { + throw new ForbiddenException( + isSpecialBid(bid.sourceType) ? '只有被指定的特殊流程办理人可以派特殊制标人' : '无权派制标人', + ); + } + const drafter = await tx.user.findFirst({ + where: { id: dto.drafterId, tenantId: '1', status: 'ACTIVE' }, + }); + if (!drafter) throw new BadRequestException('制标人无效'); + if (dto.review1Id) { + const r1 = await tx.user.findFirst({ + where: { id: dto.review1Id, tenantId: '1', status: 'ACTIVE' }, + }); + if (!r1) throw new BadRequestException('一审人无效'); + } + await tx.bidAssignee.deleteMany({ + where: { bidCaseId: bid.id, role: { in: [ASSIGN_ROLE.DRAFTER, ASSIGN_ROLE.REVIEW1] } }, + }); + await tx.bidAssignee.create({ + data: { bidCaseId: bid.id, userId: drafter.id, role: ASSIGN_ROLE.DRAFTER }, + }); + if (dto.review1Id) { + await tx.bidAssignee.create({ + data: { bidCaseId: bid.id, userId: dto.review1Id, role: ASSIGN_ROLE.REVIEW1 }, + }); + } + await tx.todoTask.updateMany({ + where: { bizId: bid.id, bizType: BIZ_TYPE.ASSIGN_DRAFT, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + await tx.todoTask.updateMany({ + where: { bizId: bid.id, bizType: BIZ_TYPE.DRAFT, status: 'OPEN' }, + data: { status: 'CANCELLED' }, + }); + await tx.todoTask.create({ + data: { + tenantId: '1', + title: `制标:${bid.name}`, + bizType: BIZ_TYPE.DRAFT, + bizId: bid.id, + assigneeId: drafter.id, + }, + }); + emitSms('BID_DRAFT', { userIds: [drafter.id], params: bidSmsParams(bid) }); + await this.log( + tx, + bid.id, + user.id, + 'ASSIGN_DRAFTER', + bid.status, + bid.status, + `派制标人 ${drafter.displayName}`, + ); + }); + return this.get(bidId, user); + } + + async submitDraft(bidId: string, user: AuthUser, dto: SubmitDraftInput) { + if (!dto.nasPath?.trim()) { + throw new BadRequestException('请填写标书 NAS 路径'); + } + await this.prisma.$transaction(async (tx) => { + const bid = await tx.bidCase.findFirst({ + where: { id: bidId, tenantId: '1' }, + include: { assignees: true }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + if (bid.status !== BID_STATUS.DRAFTING) { + throw new BadRequestException('当前不是制标状态'); + } + const isDrafter = bid.assignees.some( + (a) => a.role === ASSIGN_ROLE.DRAFTER && a.userId === user.id, + ); + const canManage = canManageThisDraft(user.roles, bid.sourceType, bid.assignees, user.id); + if (!isDrafter && !canManage && (isSpecialBid(bid.sourceType) || !canBizOperate(user.roles))) { + throw new ForbiddenException('无权提交标书'); + } + const latest = await tx.bidDocVersion.aggregate({ + where: { bidCaseId: bid.id }, + _max: { versionNo: true }, + }); + const versionNo = (latest._max.versionNo ?? 0) + 1; + await tx.bidDocVersion.create({ + data: { + bidCaseId: bid.id, + versionNo, + nasPath: dto.nasPath.trim(), + note: dto.note?.trim() || null, + submittedById: user.id, + }, + }); + const review1Ids = await this.review1Ids(tx, bid); + if (!review1Ids.length) { + throw new BadRequestException('请先指定一审人,或确保有商务专员'); + } + await tx.bidCase.update({ + where: { id: bid.id }, + data: { status: BID_STATUS.REVIEW1, nasPath: dto.nasPath.trim() }, + }); + await tx.bidReviewer.createMany({ + data: review1Ids.map((userId) => ({ + bidCaseId: bid.id, + userId, + stage: REVIEW_STAGE.REVIEW1, + cycle: bid.reviewCycle, + })), + }); + await tx.approvalTask.createMany({ + data: review1Ids.map((assigneeId) => ({ + tenantId: '1', + title: `商务一审:${bid.name}(第${versionNo}版)`, + bizType: BIZ_TYPE.REVIEW1, + bizId: bid.id, + assigneeId, + })), + }); + emitSms('BID_REVIEW', { userIds: review1Ids, params: bidSmsParams(bid) }); + // 一审待办短信仅发送给实际的一审人,不再抄送固定人员。 + await tx.todoTask.updateMany({ + where: { bizId: bid.id, bizType: BIZ_TYPE.DRAFT, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + await this.log( + tx, + bid.id, + user.id, + 'SUBMIT_DRAFT', + BID_STATUS.DRAFTING, + BID_STATUS.REVIEW1, + `提交第${versionNo}版 ${dto.nasPath.trim()}`, + ); + }); + return this.get(bidId, user); + } + + async print(bidId: string, user: AuthUser, dto: PrintInput) { + this.assertModuleAccess(user); + if (!canBizOperate(user.roles)) { + throw new ForbiddenException('无权办理印封'); + } + await this.prisma.$transaction(async (tx) => { + const bid = await this.requireBid(tx, bidId); + if (bid.status !== BID_STATUS.PRINT) { + throw new BadRequestException('当前不是印封状态'); + } + const amount = dto.amount ?? 0; + if (amount < 0) throw new BadRequestException('印封费用不能为负'); + if (amount > 0) { + await tx.expenseClaim.create({ + data: { + tenantId: '1', + claimNo: await nextExpenseClaimNo(tx), + applicantId: user.id, + costType: 'BID', + bidCaseId: bid.id, + amount, + status: 'PENDING', + remark: dto.note?.trim() || '印封费用', + }, + }); + } + await tx.bidCase.update({ + where: { id: bid.id }, + data: { + status: BID_STATUS.LIST, + printCost: amount, + printNote: dto.note?.trim() || null, + printedAt: new Date(), + nasPath: dto.nasPath?.trim() || bid.nasPath, + }, + }); + await tx.todoTask.updateMany({ + where: { bizId: bid.id, bizType: BIZ_TYPE.PRINT, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + await this.createTodos( + tx, + bid.id, + `补全投标:${bid.name}`, + BIZ_TYPE.COMPLETE, + [{ id: user.id }], + ); + await this.log( + tx, + bid.id, + user.id, + 'PRINT_DONE', + BID_STATUS.PRINT, + BID_STATUS.LIST, + amount > 0 ? `印封费用 ${amount}` : '印封完成,进入投标执行', + ); + }); + return this.get(bidId, user); + } + + async completeExecution(bidId: string, user: AuthUser, dto: CompleteInput) { + this.assertModuleAccess(user); + if (!canBizOperate(user.roles)) { + throw new ForbiddenException('无权补全投标执行'); + } + const bidderIds = [...new Set((dto.bidderIds ?? []).filter(Boolean))]; + if (!bidderIds.length) { + throw new BadRequestException('请派投标人'); + } + await this.prisma.$transaction(async (tx) => { + const bid = await this.requireBid(tx, bidId); + if (bid.status !== BID_STATUS.LIST) { + throw new BadRequestException('当前不是投标执行状态'); + } + const bidders = await tx.user.findMany({ + where: { id: { in: bidderIds }, tenantId: '1', status: 'ACTIVE' }, + }); + if (bidders.length !== bidderIds.length) { + throw new BadRequestException('投标人无效'); + } + await tx.bidCase.update({ + where: { id: bid.id }, + data: { + status: BID_STATUS.PENDING, + bondMethod: dto.bondMethod || bid.bondMethod, + bondStatus: dto.bondStatus || bid.bondStatus, + delivererName: dto.delivererName || bid.delivererName, + priceCap: dto.priceCap || bid.priceCap, + quoteAmount: dto.quoteAmount || bid.quoteAmount, + securityLevel: dto.securityLevel || bid.securityLevel, + contactName: dto.contactName || bid.contactName, + contactPhone: dto.contactPhone || bid.contactPhone, + openAt: dto.openAt ? new Date(dto.openAt) : bid.openAt, + openPlace: dto.openPlace || bid.openPlace, + openMethod: dto.openMethod || bid.openMethod, + }, + }); + if (dto.bondAmount && dto.bondAmount > 0) { + await tx.bondRecord.create({ + data: { + tenantId: '1', + bondNo: await nextBondNo(tx), + bidCaseId: bid.id, + bondType: 'TENDER', + amount: dto.bondAmount, + status: dto.bondStatus === '已缴纳' ? 'PAID' : 'UNPAID', + paidAt: dto.bondStatus === '已缴纳' ? new Date() : null, + }, + }); + } + await tx.bidAssignee.deleteMany({ + where: { bidCaseId: bid.id, role: ASSIGN_ROLE.BIDDER }, + }); + await tx.bidAssignee.createMany({ + data: bidderIds.map((userId) => ({ + bidCaseId: bid.id, + userId, + role: ASSIGN_ROLE.BIDDER, + })), + }); + await tx.todoTask.updateMany({ + where: { bizId: bid.id, bizType: BIZ_TYPE.COMPLETE, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + const openAt = dto.openAt ? new Date(dto.openAt) : bid.openAt; + await this.createTodos( + tx, + bid.id, + `开标反馈:${bid.name}`, + BIZ_TYPE.OPEN, + bidders, + openAt, + ); + await this.log( + tx, + bid.id, + user.id, + 'EXECUTION_DONE', + BID_STATUS.LIST, + BID_STATUS.PENDING, + `派投标人 ${bidders.map((b) => b.displayName).join('、')}`, + ); + }); + return this.get(bidId, user); + } + + async openResult(bidId: string, user: AuthUser, dto: OpenResultInput) { + if (!['WON', 'LOST', 'FAILED'].includes(dto.result)) { + throw new BadRequestException('开标结果无效'); + } + if (dto.result !== OPEN_RESULT.WON && !dto.costDisposition) { + throw new BadRequestException('未中标请选择售前费用归属:部门或营销'); + } + if ( + dto.costDisposition && + dto.costDisposition !== COST_DISPOSITION.DEPT && + dto.costDisposition !== COST_DISPOSITION.MARKETING + ) { + throw new BadRequestException('费用归属无效'); + } + await this.prisma.$transaction(async (tx) => { + const bid = await tx.bidCase.findFirst({ + where: { id: bidId, tenantId: '1' }, + include: { assignees: true }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + if (bid.status !== BID_STATUS.PENDING) { + throw new BadRequestException('当前不是待开标状态'); + } + const isBidder = bid.assignees.some( + (a) => a.role === ASSIGN_ROLE.BIDDER && a.userId === user.id, + ); + if (!isBidder) { + throw new ForbiddenException('请由被派的投标人在个人办公待办里登记开标结果'); + } + const disposition = + dto.result === OPEN_RESULT.WON ? null : dto.costDisposition || COST_DISPOSITION.DEPT; + await tx.bidCase.update({ + where: { id: bid.id }, + data: { + status: dto.result, + resultNote: dto.comment?.trim() || null, + costDisposition: disposition, + openedResultAt: new Date(), + winnerName: dto.result === 'WON' ? null : dto.winnerName?.trim() || null, + winnerAmount: dto.result === 'WON' ? null : parseMoney(dto.winnerAmount), + }, + }); + if (disposition) { + await tx.expenseClaim.updateMany({ + where: { bidCaseId: bid.id }, + data: { costType: disposition }, + }); + } + await tx.todoTask.updateMany({ + where: { bizId: bid.id, bizType: BIZ_TYPE.OPEN, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + const label = + dto.result === 'WON' ? '中标' : dto.result === 'LOST' ? '未中标' : '流标/废标'; + const extra = + disposition === 'DEPT' ? '售前费用归部门' : disposition === 'MARKETING' ? '售前费用归营销' : ''; + await this.log( + tx, + bid.id, + user.id, + 'OPEN_RESULT', + BID_STATUS.PENDING, + dto.result, + [label, extra, dto.comment?.trim()].filter(Boolean).join(';'), + ); + emitSms('BID_DONE', { + userIds: bid.createdById ? [bid.createdById] : [], + params: bidSmsParams(bid, { content: `${bid.name}已登记开标结果:${label}` }), + }); + if (dto.result === OPEN_RESULT.WON) { + const full = await tx.bidCase.findFirst({ + where: { id: bid.id }, + include: { party: true, legalEntity: true, expenses: { select: { amount: true } } }, + }); + if (!full) throw new NotFoundException('投标事项不存在'); + const spawned = await ensureWonLedger(tx, full, user.id); + await this.createTodos( + tx, + spawned.contract.id, + `完善合同草稿:${bid.name}`, + BIZ_TYPE.CONVERT_CONTRACT, + await this.bizDeptDirectors(tx), + ); + } + }); + return this.get(bidId, user); + } + + async terminate(bidId: string, user: AuthUser, comment?: string) { + this.assertModuleAccess(user); + if (!canTerminateBid(user.roles)) { + throw new ForbiddenException('无权终止投标'); + } + if (!comment?.trim()) { + throw new BadRequestException('终止请填写原因'); + } + await this.prisma.$transaction(async (tx) => { + const bid = await this.requireBid(tx, bidId); + if (!canTerminateFrom(bid.status)) { + throw new BadRequestException('当前状态不能终止'); + } + await this.closeAndTerminate(tx, bid, user, comment.trim(), 'MANUAL_TERMINATE'); + }); + if (!canSeeTerminated(user.roles, user.permissions)) { + return { id: bidId, status: BID_STATUS.TERMINATED }; + } + return this.get(bidId, user); + } + + async decideApproval(approvalId: string, user: AuthUser, dto: ReviewInput) { + const task = await this.prisma.approvalTask.findFirst({ + where: { id: approvalId, tenantId: '1' }, + }); + if (!task) throw new NotFoundException('审批任务不存在'); + if (task.assigneeId !== user.id) { + throw new ForbiddenException('这不是你的审批'); + } + if (task.status !== 'PENDING') { + throw new BadRequestException('该审批已办理'); + } + const allowed = [ + BIZ_TYPE.TECH_INITIAL, + BIZ_TYPE.TECH_FINAL, + BIZ_TYPE.REVIEW1, + BIZ_TYPE.REVIEW2, + ]; + if (!task.bizId || !allowed.includes(task.bizType as (typeof allowed)[number])) { + throw new BadRequestException('暂不支持该类型审批'); + } + return this.review(task.bizId, user, dto); + } + + private async bounceToDrafting( + tx: Tx, + bid: { id: string; status: string; name: string; reviewCycle: number }, + user: AuthUser, + comment: string, + stage: string, + ) { + const nextCycle = bid.reviewCycle + 1; + await tx.bidCase.update({ + where: { id: bid.id }, + data: { status: BID_STATUS.DRAFTING, reviewCycle: nextCycle }, + }); + await tx.approvalTask.updateMany({ + where: { bizId: bid.id, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + await tx.bidReviewer.updateMany({ + where: { bidCaseId: bid.id, result: 'PENDING' }, + data: { result: 'CANCELLED' }, + }); + const drafters = await tx.bidAssignee.findMany({ + where: { bidCaseId: bid.id, role: ASSIGN_ROLE.DRAFTER }, + }); + if (drafters.length) { + await tx.todoTask.createMany({ + data: drafters.map((d) => ({ + tenantId: '1', + title: `改标书:${bid.name}`, + bizType: BIZ_TYPE.DRAFT, + bizId: bid.id, + assigneeId: d.userId, + })), + }); + emitSms('BID_DRAFT', { + userIds: drafters.map((d) => d.userId), + params: bidSmsParams(bid), + }); + } + await this.log( + tx, + bid.id, + user.id, + stage === REVIEW_STAGE.REVIEW2 ? 'REVIEW2_REJECT' : 'REVIEW1_REJECT', + bid.status, + BID_STATUS.DRAFTING, + comment, + ); + } + + private async closeAndTerminate( + tx: Tx, + bid: { + id: string; + status: string; + bidNo?: string; + name?: string; + createdById?: string | null; + }, + user: AuthUser, + comment: string, + action: string, + ) { + await tx.bidCase.update({ + where: { id: bid.id }, + data: { status: BID_STATUS.TERMINATED }, + }); + await tx.approvalTask.updateMany({ + where: { bizId: bid.id, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + await tx.todoTask.updateMany({ + where: { bizId: bid.id, status: 'OPEN' }, + data: { status: 'CANCELLED' }, + }); + await tx.bidReviewer.updateMany({ + where: { bidCaseId: bid.id, result: 'PENDING' }, + data: { result: 'CANCELLED' }, + }); + await this.log(tx, bid.id, user.id, action, bid.status, BID_STATUS.TERMINATED, comment); + emitSms('BID_TERMINATED', { + userIds: await this.panXinghuiIds(tx), + params: bidSmsParams(bid, { content: `${bid.name}项目终止,终止原因:${comment}` }), + }); + } + + private async requireBid(tx: Tx, id: string) { + const bid = await tx.bidCase.findFirst({ where: { id, tenantId: '1' } }); + if (!bid) throw new NotFoundException('投标事项不存在'); + return bid; + } + + private async usersByRoles(tx: Tx, roles: string[]) { + return tx.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + roles: { some: { role: { code: { in: roles } } } }, + }, + }); + } + + private async bizDeptDirectors(tx: Tx) { + return findBizDirectors(tx); + } + + /** 投标终止按业务规则知会潘兴辉;其他环节不做固定抄送。 */ + private async panXinghuiIds(tx: Tx) { + const users = await tx.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + OR: [{ username: 'panxinghui' }, { displayName: '潘兴辉' }], + }, + select: { id: true }, + }); + return users.map((u) => u.id); + } + + private async createTodos( + tx: Tx, + bidId: string, + title: string, + bizType: string, + users: { id: string }[], + dueAt?: Date | null, + ) { + const seen = new Set(); + const assignees = users.filter((u) => u.id && !seen.has(u.id) && seen.add(u.id)); + if (!assignees.length) return; + await tx.todoTask.createMany({ + data: assignees.map((u) => ({ + tenantId: '1', + title, + bizType, + bizId: bidId, + assigneeId: u.id, + dueAt: dueAt ?? undefined, + })), + }); + const smsKey = + bizType === BIZ_TYPE.TECH_INITIAL + ? 'BID_TECH_INITIAL' + : bizType === BIZ_TYPE.FETCH + ? 'BID_FETCH' + : bizType === BIZ_TYPE.ASSIGN_FETCH + ? 'BID_ASSIGN_FETCH' + : bizType === BIZ_TYPE.TECH_FINAL + ? 'BID_TECH_FINAL' + : bizType === BIZ_TYPE.DRAFT + ? 'BID_DRAFT' + : bizType === BIZ_TYPE.ASSIGN_DRAFT + ? 'BID_ASSIGN_DRAFT' + : bizType === BIZ_TYPE.PRINT + ? 'BID_PRINT' + : bizType === BIZ_TYPE.COMPLETE + ? 'BID_EXECUTION' + : bizType === BIZ_TYPE.OPEN + ? 'BID_ASSIGN' + : bizType === BIZ_TYPE.REVIEW1 || bizType === BIZ_TYPE.REVIEW2 + ? 'BID_REVIEW' + : null; + if (smsKey) { + const bid = await tx.bidCase.findFirst({ + where: { id: bidId }, + select: { + bidNo: true, + name: true, + openAt: true, + openPlace: true, + contactName: true, + contactPhone: true, + }, + }); + emitSms(smsKey, { + userIds: assignees.map((u) => u.id), + params: bidSmsParams(bid || {}, { content: title }), + }); + } + } + + private async review1Ids( + tx: Tx, + bid: { id: string; assignees: { role: string; userId: string }[] }, + ) { + const assigned = bid.assignees + .filter((a) => a.role === ASSIGN_ROLE.REVIEW1) + .map((a) => a.userId); + if (assigned.length) return [...new Set(assigned)]; + const staff = await this.bizDeptDirectors(tx); + return staff.map((u) => u.id); + } + + private async log( + tx: Tx, + bidCaseId: string, + actorId: string, + action: string, + fromStatus?: string, + toStatus?: string, + comment?: string, + ) { + await tx.bidActionLog.create({ + data: { bidCaseId, actorId, action, fromStatus, toStatus, comment }, + }); + } + + private approveAction(stage: string) { + if (stage === REVIEW_STAGE.TECH_FINAL) return 'FINAL_APPROVE'; + if (stage === REVIEW_STAGE.REVIEW1) return 'REVIEW1_APPROVE'; + if (stage === REVIEW_STAGE.REVIEW2) return 'REVIEW2_APPROVE'; + return 'INITIAL_APPROVE'; + } + + private async loadSpecialCoordinators(db: PrismaService | Tx) { + // 特殊筛选/特殊制标由发起人从全体在职人员中指定办理人,不再写死两位协调人。 + return db.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + }, + select: { id: true, displayName: true, username: true, employee: { select: { department: { select: { name: true } } } } }, + orderBy: { displayName: 'asc' }, + }); + } + + private myActions( + bid: { + createdById?: string | null; + status: string; + sourceType?: string | null; + reviewCycle: number; + reviewers: { userId: string; stage: string; result: string; cycle: number }[]; + assignees: { userId: string; role: string }[]; + contracts?: { status: string }[]; + }, + user: AuthUser, + ) { + const stage = stageForStatus(bid.status); + const canReview = Boolean( + stage && + bid.reviewers.some( + (r) => + r.userId === user.id && + r.stage === stage && + r.result === 'PENDING' && + r.cycle === bid.reviewCycle, + ), + ); + const isDrafter = bid.assignees.some( + (a) => a.role === ASSIGN_ROLE.DRAFTER && a.userId === user.id, + ); + const isBidder = bid.assignees.some( + (a) => a.role === ASSIGN_ROLE.BIDDER && a.userId === user.id, + ); + const manageDraft = canManageThisDraft(user.roles, bid.sourceType, bid.assignees, user.id); + const hasActiveContract = (bid.contracts ?? []).some((c) => + ['DRAFT', 'PENDING', 'APPROVED'].includes(c.status), + ); + return { + canReview, + reviewStage: canReview ? stage : null, + reviewKind: canReview && stage && isCommercialStage(stage) ? 'commercial' : 'tech', + canAssignFetcher: canAssignFetcher( + user.roles, + bid.sourceType, + bid.assignees, + user.id, + bid.status, + ), + canFetch: + bid.status === BID_STATUS.FETCH && + canFetchThisBid(user.roles, bid.sourceType, bid.assignees, user.id) && + (!isSpecialBid(bid.sourceType) || + bid.assignees.some((a) => a.role === ASSIGN_ROLE.FETCHER)), + canUploadFetchFile: + bid.status === BID_STATUS.FETCH && + (user.roles.includes('admin') || + bid.assignees.some((a) => a.role === ASSIGN_ROLE.FETCHER && a.userId === user.id)), + canAssignDrafter: bid.status === BID_STATUS.DRAFTING && manageDraft, + canSubmitDraft: bid.status === BID_STATUS.DRAFTING && (isDrafter || manageDraft || (!isSpecialBid(bid.sourceType) && canBizOperate(user.roles))), + canPrint: bid.status === BID_STATUS.PRINT && canBizOperate(user.roles), + canCompleteExecution: bid.status === BID_STATUS.LIST && canBizOperate(user.roles), + canOpenResult: bid.status === BID_STATUS.PENDING && isBidder, + canEditCompetitor: + ['TERMINATED', 'LOST', 'FAILED'].includes(bid.status) && + (canSeeTerminated(user.roles, user.permissions) || canBizOperate(user.roles) || bid.assignees.some((a) => a.userId === user.id)), + canEditLegalEntity: + user.username.trim().toLowerCase() === 'panxinghui' || + user.displayName.trim() === '潘兴辉' || + user.roles.includes('biz_director') || + user.roles.includes('rd_director') || + user.roles.includes('admin') || + user.roles.includes('owner'), + canConvertToContract: + bid.status === BID_STATUS.WON && canBizOperate(user.roles) && !hasActiveContract, + canTerminate: canTerminateFrom(bid.status) && canTerminateBid(user.roles), + canRestore: bid.status === BID_STATUS.TERMINATED && canTerminateBid(user.roles), + canRollback: Boolean(previousBidStatus(bid.status, bid.sourceType)) && ( + canRollbackBid(user.roles) || + canReview || + (bid.status === BID_STATUS.FETCH && + canFetchThisBid(user.roles, bid.sourceType, bid.assignees, user.id)) || + (bid.status === BID_STATUS.DRAFTING && (isDrafter || manageDraft)) || + (bid.status === BID_STATUS.PRINT && canBizOperate(user.roles)) || + (bid.status === BID_STATUS.LIST && canBizOperate(user.roles)) || + (bid.status === BID_STATUS.PENDING && isBidder) + ), + rollbackTarget: previousBidStatus(bid.status, bid.sourceType), + }; + } + + async recordCompetitor( + bidId: string, + user: AuthUser, + dto: { winnerName: string; winnerAmount: number | string; winnerNote?: string }, + ) { + this.assertModuleAccess(user); + const bid = await this.prisma.bidCase.findFirst({ + where: { id: bidId, tenantId: '1' }, + include: { assignees: true }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + if (!['TERMINATED', 'LOST', 'FAILED'].includes(bid.status)) { + throw new BadRequestException('只有未中、流标或已终止的事项才能登记对方中标信息'); + } + const assigned = bid.assignees.some((a) => a.userId === user.id); + if (!assigned && !canSeeTerminated(user.roles, user.permissions) && !canBizOperate(user.roles)) { + throw new ForbiddenException('无权登记该项目的中标信息'); + } + const winnerName = dto.winnerName.trim(); + const winnerAmount = parseMoney(dto.winnerAmount); + if (!winnerName) throw new BadRequestException('请填写中标单位'); + if (winnerAmount == null) throw new BadRequestException('请填写中标金额'); + await this.prisma.$transaction(async (tx) => { + await tx.bidCase.update({ + where: { id: bid.id }, + data: { + winnerName, + winnerAmount, + winnerNote: dto.winnerNote?.trim() || null, + }, + }); + await this.log( + tx, + bid.id, + user.id, + 'UPDATE_COMPETITOR_WIN', + bid.status, + bid.status, + `登记中标单位:${winnerName},中标金额:${winnerAmount.toString()}`, + ); + }); + return this.get(bidId, user); + } + + async updateLegalEntity(bidId: string, user: AuthUser, legalEntityId: string) { + this.assertModuleAccess(user); + const allowed = + user.username.trim().toLowerCase() === 'panxinghui' || + user.displayName.trim() === '潘兴辉' || + user.roles.includes('biz_director') || + user.roles.includes('rd_director') || + user.roles.includes('admin') || + user.roles.includes('owner'); + if (!allowed) throw new ForbiddenException('只有潘兴辉、商务总监或研发总监可以修改参与主体'); + + const [bid, entity] = await Promise.all([ + this.prisma.bidCase.findFirst({ where: { id: bidId, tenantId: '1' } }), + this.prisma.legalEntity.findFirst({ where: { id: legalEntityId, tenantId: '1' } }), + ]); + if (!bid) throw new NotFoundException('投标事项不存在'); + if (!entity) throw new BadRequestException('参与主体不存在'); + await this.prisma.$transaction(async (tx) => { + await tx.bidCase.update({ where: { id: bid.id }, data: { legalEntityId: entity.id } }); + await this.log(tx, bid.id, user.id, 'UPDATE_LEGAL_ENTITY', bid.status, bid.status, `修改参与主体为 ${entity.name}`); + }); + return this.get(bidId, user); + } + + async rollback(bidId: string, user: AuthUser, comment: string) { + this.assertModuleAccess(user); + const note = comment.trim(); + if (!note) throw new BadRequestException('退回请填写原因'); + await this.prisma.$transaction(async (tx) => { + const bid = await tx.bidCase.findFirst({ + where: { id: bidId, tenantId: '1' }, + include: { reviewers: true, assignees: true }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + const target = previousBidStatus(bid.status, bid.sourceType); + if (!target) throw new BadRequestException('当前步骤不能再退回'); + const stage = stageForStatus(bid.status); + const isReviewer = Boolean( + stage && + bid.reviewers.some( + (r) => + r.userId === user.id && + r.stage === stage && + r.result === 'PENDING' && + r.cycle === bid.reviewCycle, + ), + ); + const isDrafter = bid.assignees.some((a) => a.role === ASSIGN_ROLE.DRAFTER && a.userId === user.id); + const manageDraft = canManageThisDraft(user.roles, bid.sourceType, bid.assignees, user.id); + const allowed = + canRollbackBid(user.roles) || + isReviewer || + (bid.status === BID_STATUS.FETCH && + canFetchThisBid(user.roles, bid.sourceType, bid.assignees, user.id)) || + (bid.status === BID_STATUS.DRAFTING && (isDrafter || manageDraft)) || + (bid.status === BID_STATUS.PRINT && canBizOperate(user.roles)) || + (bid.status === BID_STATUS.LIST && canBizOperate(user.roles)) || + (bid.status === BID_STATUS.PENDING && + bid.assignees.some((a) => a.role === ASSIGN_ROLE.BIDDER && a.userId === user.id)); + if (!allowed) throw new ForbiddenException('无权退回上一步'); + + const from = bid.status; + const nextCycle = bid.reviewCycle + 1; + await tx.bidCase.update({ + where: { id: bid.id }, + data: { status: target, reviewCycle: nextCycle }, + }); + await tx.approvalTask.updateMany({ + where: { bizId: bid.id, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + await tx.todoTask.updateMany({ + where: { bizId: bid.id, status: 'OPEN' }, + data: { status: 'CANCELLED' }, + }); + await tx.bidReviewer.updateMany({ + where: { bidCaseId: bid.id, result: 'PENDING' }, + data: { result: 'CANCELLED' }, + }); + await this.restoreStep(tx, { ...bid, reviewCycle: nextCycle }, target); + await this.log(tx, bid.id, user.id, 'ROLLBACK', from, target, note); + }); + return this.get(bidId, user); + } + + async restoreTerminated(bidId: string, user: AuthUser) { + this.assertModuleAccess(user); + if (!canTerminateBid(user.roles)) throw new ForbiddenException('无权恢复终止投标'); + await this.prisma.$transaction(async (tx) => { + const bid = await tx.bidCase.findFirst({ + where: { id: bidId, tenantId: '1' }, + include: { + reviewers: true, + assignees: true, + actionLogs: { where: { toStatus: BID_STATUS.TERMINATED }, orderBy: { createdAt: 'desc' }, take: 1 }, + }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + if (bid.status !== BID_STATUS.TERMINATED) throw new BadRequestException('当前事项未终止'); + const loggedTarget = bid.actionLogs[0]?.fromStatus; + const restorable = new Set([ + BID_STATUS.TECH_INITIAL, + BID_STATUS.FETCH, + BID_STATUS.TECH_FINAL, + BID_STATUS.DRAFTING, + BID_STATUS.REVIEW1, + BID_STATUS.REVIEW2, + BID_STATUS.PRINT, + BID_STATUS.LIST, + BID_STATUS.PENDING, + ]); + const target = loggedTarget && restorable.has(loggedTarget) + ? loggedTarget + : inferLegacyRestoreStatus(bid.reviewers, bid.assignees); + const nextCycle = bid.reviewCycle + 1; + await tx.bidCase.update({ where: { id: bid.id }, data: { status: target, reviewCycle: nextCycle } }); + await tx.approvalTask.updateMany({ + where: { bizId: bid.id, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + await tx.todoTask.updateMany({ + where: { bizId: bid.id, status: 'OPEN' }, + data: { status: 'CANCELLED' }, + }); + await tx.bidReviewer.updateMany({ + where: { bidCaseId: bid.id, result: 'PENDING' }, + data: { result: 'CANCELLED' }, + }); + await this.restoreStep(tx, { ...bid, reviewCycle: nextCycle }, target); + await this.log( + tx, + bid.id, + user.id, + 'RESTORE_TERMINATED', + BID_STATUS.TERMINATED, + target, + loggedTarget ? '恢复到终止前步骤' : '旧数据缺少终止日志,按现有办理记录恢复', + ); + }); + return this.get(bidId, user); + } + + private async restoreStep( + tx: Tx, + bid: { + id: string; + name: string; + sourceType?: string | null; + applyEndAt?: Date | null; + reviewCycle: number; + assignees: { userId: string; role: string }[]; + reviewers: { userId: string; stage: string }[]; + }, + target: string, + ) { + const reopen = async (stage: string, title: string, bizType: string) => { + const ids = [...new Set(bid.reviewers.filter((r) => r.stage === stage).map((r) => r.userId))]; + if (!ids.length) return; + await tx.bidReviewer.createMany({ + data: ids.map((userId) => ({ + bidCaseId: bid.id, + userId, + stage, + cycle: bid.reviewCycle, + })), + }); + await tx.approvalTask.createMany({ + data: ids.map((assigneeId) => ({ + tenantId: '1', + title: `${title}:${bid.name}`, + bizType, + bizId: bid.id, + assigneeId, + })), + }); + emitSms('BID_REVIEW', { userIds: ids, params: bidSmsParams(bid) }); + }; + + if (target === BID_STATUS.TECH_INITIAL) { + await reopen(REVIEW_STAGE.TECH_INITIAL, '技术初审(退回重审)', BIZ_TYPE.TECH_INITIAL); + return; + } + if (target === BID_STATUS.FETCH) { + const fetchers = bid.assignees.filter((a) => a.role === ASSIGN_ROLE.FETCHER).map((a) => ({ id: a.userId })); + if (fetchers.length) { + await this.createTodos(tx, bid.id, `取标(退回重做):${bid.name}`, BIZ_TYPE.FETCH, fetchers, bid.applyEndAt); + } else if (isSpecialBid(bid.sourceType)) { + const owners = bid.assignees + .filter((a) => a.role === ASSIGN_ROLE.COORDINATOR) + .map((a) => ({ id: a.userId })); + await this.createTodos( + tx, + bid.id, + `特殊筛选派取标(退回重做):${bid.name}`, + BIZ_TYPE.ASSIGN_FETCH, + owners, + bid.applyEndAt, + ); + } else { + await this.createTodos( + tx, + bid.id, + `派取标人(退回重做):${bid.name}`, + BIZ_TYPE.ASSIGN_FETCH, + await this.bizDeptDirectors(tx), + bid.applyEndAt, + ); + } + return; + } + if (target === BID_STATUS.TECH_FINAL) { + await reopen(REVIEW_STAGE.TECH_FINAL, '技术终审(退回重审)', BIZ_TYPE.TECH_FINAL); + return; + } + if (target === BID_STATUS.DRAFTING) { + const drafters = bid.assignees.filter((a) => a.role === ASSIGN_ROLE.DRAFTER).map((a) => ({ id: a.userId })); + if (drafters.length) { + await this.createTodos(tx, bid.id, `改标书(退回重做):${bid.name}`, BIZ_TYPE.DRAFT, drafters); + } else { + const owners = isSpecialBid(bid.sourceType) + ? bid.assignees.filter((a) => a.role === ASSIGN_ROLE.COORDINATOR).map((a) => ({ id: a.userId })) + : [...(await this.bizDeptDirectors(tx)), ...(await this.usersByRoles(tx, ['owner']))]; + await this.createTodos(tx, bid.id, `派制标(退回重做):${bid.name}`, BIZ_TYPE.ASSIGN_DRAFT, owners); + } + return; + } + if (target === BID_STATUS.REVIEW1) { + await reopen(REVIEW_STAGE.REVIEW1, '商务一审(退回重审)', BIZ_TYPE.REVIEW1); + return; + } + if (target === BID_STATUS.REVIEW2) { + await reopen(REVIEW_STAGE.REVIEW2, '商务二审(退回重审)', BIZ_TYPE.REVIEW2); + return; + } + if (target === BID_STATUS.PRINT) { + await this.createTodos( + tx, + bid.id, + `印封(退回重做):${bid.name}`, + BIZ_TYPE.PRINT, + await this.bizDeptDirectors(tx), + ); + return; + } + if (target === BID_STATUS.LIST) { + await this.createTodos( + tx, + bid.id, + `投标执行(退回重做):${bid.name}`, + BIZ_TYPE.COMPLETE, + await this.bizDeptDirectors(tx), + ); + return; + } + if (target === BID_STATUS.PENDING) { + const bidders = bid.assignees.filter((a) => a.role === ASSIGN_ROLE.BIDDER).map((a) => ({ id: a.userId })); + await this.createTodos(tx, bid.id, `开标反馈(恢复):${bid.name}`, BIZ_TYPE.OPEN, bidders); + } + } +} + +function parseMoney(v?: number | string | null) { + if (v == null || v === '') return null; + const n = Number(v); + if (!Number.isFinite(n) || n < 0) return null; + return n; +} diff --git a/apps/api/src/business/business.controller.ts b/apps/api/src/business/business.controller.ts new file mode 100644 index 0000000..f6d15d0 --- /dev/null +++ b/apps/api/src/business/business.controller.ts @@ -0,0 +1,62 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { TECH_LEAD_ROLES } from '../common/role-groups'; +import { PrismaService } from '../prisma/prisma.service'; + +@ApiTags('business') +@ApiBearerAuth() +@Controller() +export class BusinessController { + constructor(private readonly prisma: PrismaService) {} + + @Get('directors') + async directors() { + const users = await this.prisma.user.findMany({ + where: { + tenantId: '1', + roles: { some: { role: { code: { in: ['admin', 'tech_director', ...TECH_LEAD_ROLES] } } } }, + }, + include: { employee: { include: { department: true } }, roles: { include: { role: true } } }, + }); + return users.map((u) => ({ + id: u.id, + name: u.displayName, + department: u.employee?.department?.name, + roles: u.roles.map((r) => r.role.code), + })); + } + + @Get('staff') + async staff() { + const users = await this.prisma.user.findMany({ + where: { tenantId: '1', status: 'ACTIVE' }, + include: { employee: { include: { department: true } } }, + orderBy: { displayName: 'asc' }, + }); + const avatars = await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizType: 'USER_AVATAR', bizId: { in: users.map((u) => u.id) } }, + select: { id: true, bizId: true }, + orderBy: { createdAt: 'desc' }, + }); + const avatarByUser = new Map(); + for (const a of avatars) { + if (!avatarByUser.has(a.bizId)) avatarByUser.set(a.bizId, a.id); + } + return users.map((u) => { + const acc = u.employee?.bankAccount?.replace(/\s/g, '') || ''; + return { + id: u.id, + name: u.displayName, + username: u.username, + department: u.employee?.department?.name, + title: u.employee?.title, + mobile: u.employee?.mobile || null, + email: u.employee?.email || null, + avatarFileId: avatarByUser.get(u.id) || null, + bankReady: Boolean(acc), + bankName: u.employee?.bankName || null, + bankAccountMasked: acc ? `${'*'.repeat(Math.max(4, acc.length - 4))}${acc.slice(-4)}` : null, + }; + }); + } +} diff --git a/apps/api/src/common/data-scope.ts b/apps/api/src/common/data-scope.ts new file mode 100644 index 0000000..52da161 --- /dev/null +++ b/apps/api/src/common/data-scope.ts @@ -0,0 +1,105 @@ +import { ForbiddenException } from '@nestjs/common'; +import type { Prisma } from '@prisma/client'; +import type { AuthUser } from './decorators/current-user.decorator'; +import { PrismaService } from '../prisma/prisma.service'; + +export type DataScope = 'ALL' | 'DEPT' | 'SELF'; + +const RANK: Record = { SELF: 0, DEPT: 1, ALL: 2 }; + +export function mergeDataScope(scopes: string[]): DataScope { + let best: DataScope = 'SELF'; + for (const s of scopes) { + const v = s as DataScope; + if (RANK[v] > RANK[best]) best = v; + } + return best; +} + +export function effectiveScope(user: AuthUser): DataScope { + if (user.roles.includes('admin') || user.roles.includes('owner')) return 'ALL'; + return user.dataScope || 'SELF'; +} + +export async function descendantDeptIds(prisma: PrismaService, rootId?: string | null) { + if (!rootId) return [] as string[]; + const all = await prisma.department.findMany({ + where: { tenantId: '1' }, + select: { id: true, parentId: true }, + }); + const ids = new Set([rootId]); + let grew = true; + while (grew) { + grew = false; + for (const d of all) { + if (d.parentId && ids.has(d.parentId) && !ids.has(d.id)) { + ids.add(d.id); + grew = true; + } + } + } + return [...ids]; +} + +function empInScope(user: AuthUser, deptIds: string[]): Prisma.EmployeeWhereInput { + return { + OR: [ + { departmentId: { in: deptIds } }, + ...(user.employeeId + ? [{ managerId: user.employeeId }, { managerIds: { contains: user.employeeId } }] + : []), + ], + }; +} + +/** 本人 / 本部门及下级 / 全公司 */ +export async function ownerScope( + prisma: PrismaService, + user: AuthUser, + owner: 'createdById' | 'applicantId', +): Promise> { + const scope = effectiveScope(user); + if (scope === 'ALL') return {}; + if (scope === 'SELF' || !user.departmentId) return { [owner]: user.id }; + const deptIds = await descendantDeptIds(prisma, user.departmentId); + const rel = owner === 'createdById' ? 'createdBy' : 'applicant'; + return { + OR: [{ [owner]: user.id }, { [rel]: { employee: empInScope(user, deptIds) } }], + }; +} + +export function withAnd(where: T, extra: Record): T { + if (!extra || !Object.keys(extra).length) return where; + const prev = where.AND; + const list = Array.isArray(prev) ? [...prev] : prev ? [prev] : []; + list.push(extra); + where.AND = list; + return where; +} + +export async function canSeeOwner(prisma: PrismaService, user: AuthUser, ownerId?: string | null) { + if (!ownerId) return true; + const scope = effectiveScope(user); + if (scope === 'ALL') return true; + if (ownerId === user.id) return true; + if (scope !== 'DEPT' || !user.departmentId) return false; + const owner = await prisma.user.findUnique({ + where: { id: ownerId }, + include: { employee: true }, + }); + if (!owner?.employee) return false; + const deptIds = await descendantDeptIds(prisma, user.departmentId); + if (owner.employee.departmentId && deptIds.includes(owner.employee.departmentId)) return true; + const extra = String(owner.employee.managerIds || '') + .split(/[,,、\s]+/) + .filter(Boolean); + return Boolean( + user.employeeId && + (owner.employee.managerId === user.employeeId || extra.includes(user.employeeId)), + ); +} + +export async function assertCanSeeOwner(prisma: PrismaService, user: AuthUser, ownerId?: string | null) { + if (await canSeeOwner(prisma, user, ownerId)) return; + throw new ForbiddenException('超出数据范围,无权查看'); +} diff --git a/apps/api/src/common/decorators/auth.decorators.ts b/apps/api/src/common/decorators/auth.decorators.ts new file mode 100644 index 0000000..c5eed81 --- /dev/null +++ b/apps/api/src/common/decorators/auth.decorators.ts @@ -0,0 +1,8 @@ +import { SetMetadata } from '@nestjs/common'; + +export const IS_PUBLIC_KEY = 'isPublic'; +export const Public = () => SetMetadata(IS_PUBLIC_KEY, true); + +export const PERMISSIONS_KEY = 'permissions'; +export const RequirePermissions = (...codes: string[]) => + SetMetadata(PERMISSIONS_KEY, codes); diff --git a/apps/api/src/common/decorators/current-user.decorator.ts b/apps/api/src/common/decorators/current-user.decorator.ts new file mode 100644 index 0000000..3649000 --- /dev/null +++ b/apps/api/src/common/decorators/current-user.decorator.ts @@ -0,0 +1,18 @@ +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; + +export interface AuthUser { + id: string; + username: string; + displayName: string; + permissions: string[]; + roles: string[]; + employeeId?: string | null; + departmentId?: string | null; + dataScope: 'ALL' | 'DEPT' | 'SELF'; +} + +export const CurrentUser = createParamDecorator( + (_data: unknown, ctx: ExecutionContext): AuthUser => { + return ctx.switchToHttp().getRequest().user as AuthUser; + }, +); diff --git a/apps/api/src/common/doc-no.spec.ts b/apps/api/src/common/doc-no.spec.ts new file mode 100644 index 0000000..0b5b5f5 --- /dev/null +++ b/apps/api/src/common/doc-no.spec.ts @@ -0,0 +1,15 @@ +import { nextSerial } from './doc-no'; + +describe('nextSerial', () => { + it('starts at 00001 when empty', () => { + expect(nextSerial([], 'BX-2026-')).toBe('BX-2026-00001'); + }); + + it('uses max+1 so a hole in the middle does not collide', () => { + expect(nextSerial(['BX-2026-00001', 'BX-2026-00005'], 'BX-2026-')).toBe('BX-2026-00006'); + }); + + it('ignores old date-style numbers that share the year digits', () => { + expect(nextSerial(['BX-20260617-005', 'BX-2026-00002'], 'BX-2026-')).toBe('BX-2026-00003'); + }); +}); diff --git a/apps/api/src/common/doc-no.ts b/apps/api/src/common/doc-no.ts new file mode 100644 index 0000000..266a4d5 --- /dev/null +++ b/apps/api/src/common/doc-no.ts @@ -0,0 +1,84 @@ +import { Prisma } from '@prisma/client'; + +const DOC_NO_LOCK: Record = { BX: 91001, JK: 91002, BZ: 91003 }; + +export function nextSerial(existing: string[], prefix: string, pad = 5) { + let max = 0; + for (const no of existing) { + if (!no.startsWith(prefix)) continue; + const tail = no.slice(prefix.length); + if (/^\d+$/.test(tail)) { + const n = Number(tail); + if (Number.isFinite(n)) max = Math.max(max, n); + } + } + return `${prefix}${String(max + 1).padStart(pad, '0')}`; +} + +export function isUniqueConflict(e: unknown) { + return e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002'; +} + +export async function withUniqueRetry(fn: () => Promise, times = 8): Promise { + let last: unknown; + for (let i = 0; i < times; i += 1) { + try { + return await fn(); + } catch (e) { + last = e; + if (!isUniqueConflict(e)) throw e; + } + } + throw last instanceof Error ? last : new Error('单号冲突,请重试'); +} + +type LockTx = { $executeRaw: Prisma.TransactionClient['$executeRaw'] }; + +export async function lockDocNo(tx: LockTx, prefix: 'BX' | 'JK' | 'BZ') { + const key = DOC_NO_LOCK[prefix]; + await tx.$executeRaw`SELECT pg_advisory_xact_lock(${key}::bigint)`; +} + +type ClaimDb = { + expenseClaim: { + findMany: (args: { + where: { tenantId: string; claimNo: { startsWith: string } }; + select: { claimNo: true }; + }) => Promise<{ claimNo: string }[]>; + }; +}; + +export async function nextExpenseClaimNo(tx: LockTx & ClaimDb, year = new Date().getFullYear()) { + await lockDocNo(tx, 'BX'); + const prefix = `BX-${year}-`; + const rows = await tx.expenseClaim.findMany({ + where: { tenantId: '1', claimNo: { startsWith: prefix } }, + select: { claimNo: true }, + }); + return nextSerial( + rows.map((r) => r.claimNo), + prefix, + ); +} + +type BondDb = { + bondRecord: { + findMany: (args: { + where: { tenantId: string; bondNo: { startsWith: string } }; + select: { bondNo: true }; + }) => Promise<{ bondNo: string }[]>; + }; +}; + +export async function nextBondNo(tx: LockTx & BondDb, year = new Date().getFullYear()) { + await lockDocNo(tx, 'BZ'); + const prefix = `BZ-${year}-`; + const rows = await tx.bondRecord.findMany({ + where: { tenantId: '1', bondNo: { startsWith: prefix } }, + select: { bondNo: true }, + }); + return nextSerial( + rows.map((r) => r.bondNo), + prefix, + ); +} diff --git a/apps/api/src/common/dto/page-query.dto.ts b/apps/api/src/common/dto/page-query.dto.ts new file mode 100644 index 0000000..2d43828 --- /dev/null +++ b/apps/api/src/common/dto/page-query.dto.ts @@ -0,0 +1,24 @@ +import { IsInt, IsOptional, Max, Min } from 'class-validator'; +import { Type } from 'class-transformer'; + +export class PageQueryDto { + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + page = 1; + + @IsOptional() + @Type(() => Number) + @IsInt() + @Min(1) + @Max(100) + pageSize = 10; + + @IsOptional() + keyword?: string; +} + +export function pageMeta(page: number, pageSize: number, total: number) { + return { page, pageSize, total }; +} diff --git a/apps/api/src/common/filters/http-exception.filter.ts b/apps/api/src/common/filters/http-exception.filter.ts new file mode 100644 index 0000000..6f66440 --- /dev/null +++ b/apps/api/src/common/filters/http-exception.filter.ts @@ -0,0 +1,84 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Injectable, + Logger, +} from '@nestjs/common'; +import { Request, Response } from 'express'; +import { Prisma } from '@prisma/client'; +import { REQUEST_ID_HEADER } from '../middleware/request-id.middleware'; +import { PrismaService } from '../../prisma/prisma.service'; +import { LOG_ACTION, writeOpLog } from '../../system/op-log'; +import type { AuthUser } from '../decorators/current-user.decorator'; + +function publicErrorMessage(exception: unknown) { + if (exception instanceof Prisma.PrismaClientKnownRequestError) { + if (exception.code === 'P2002') return '单号已被占用,请再试一次'; + return '保存失败,请稍后重试'; + } + if (exception instanceof Error) { + if (/Invalid `prisma\.|Unique constraint failed/i.test(exception.message)) { + return '单号已被占用,请再试一次'; + } + return exception.message; + } + return '服务器内部错误'; +} + +@Catch() +@Injectable() +export class HttpErrorFilter implements ExceptionFilter { + private readonly logger = new Logger(HttpErrorFilter.name); + + constructor(private readonly prisma: PrismaService) {} + + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const res = ctx.getResponse(); + const req = ctx.getRequest(); + const requestId = (req.headers[REQUEST_ID_HEADER] as string) || ''; + + let status = HttpStatus.INTERNAL_SERVER_ERROR; + let message = '服务器内部错误'; + if (exception instanceof HttpException) { + status = exception.getStatus(); + const payload = exception.getResponse(); + if (typeof payload === 'string') { + message = payload; + } else if (typeof payload === 'object' && payload && 'message' in payload) { + const raw = (payload as { message: string | string[] }).message; + message = Array.isArray(raw) ? raw.join('; ') : raw; + } + } else { + message = publicErrorMessage(exception); + } + + this.logger.error( + `${req.method} ${req.url} ${status} ${message}`, + exception instanceof Error ? exception.stack : undefined, + ); + + if (status === HttpStatus.FORBIDDEN) { + void writeOpLog(this.prisma, { + userId: req.user?.id, + username: req.user?.username, + action: LOG_ACTION.FORBIDDEN, + path: `${req.method} ${req.originalUrl || req.url}`, + detail: message, + success: false, + requestId, + ip: req.ip, + }); + } + + res.status(status).json({ + code: status, + message, + data: null, + requestId, + }); + } +} diff --git a/apps/api/src/common/guards/auth.guards.ts b/apps/api/src/common/guards/auth.guards.ts new file mode 100644 index 0000000..798180a --- /dev/null +++ b/apps/api/src/common/guards/auth.guards.ts @@ -0,0 +1,37 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { AuthGuard } from '@nestjs/passport'; +import { IS_PUBLIC_KEY } from '../decorators/auth.decorators'; + +@Injectable() +export class JwtAuthGuard extends AuthGuard('jwt') { + constructor(private readonly reflector: Reflector) { + super(); + } + + canActivate(context: ExecutionContext) { + const isPublic = this.reflector.getAllAndOverride(IS_PUBLIC_KEY, [ + context.getHandler(), + context.getClass(), + ]); + if (isPublic) return true; + return super.canActivate(context); + } +} + +@Injectable() +export class PermissionsGuard implements CanActivate { + constructor(private readonly reflector: Reflector) {} + + canActivate(context: ExecutionContext): boolean { + const required = this.reflector.getAllAndOverride('permissions', [ + context.getHandler(), + context.getClass(), + ]); + if (!required?.length) return true; + const user = context.switchToHttp().getRequest().user; + if (!user?.permissions) return false; + if (user.roles?.includes('admin')) return true; + return required.every((code) => user.permissions.includes(code)); + } +} diff --git a/apps/api/src/common/interceptors/response.interceptor.ts b/apps/api/src/common/interceptors/response.interceptor.ts new file mode 100644 index 0000000..3ab8093 --- /dev/null +++ b/apps/api/src/common/interceptors/response.interceptor.ts @@ -0,0 +1,40 @@ +import { + CallHandler, + ExecutionContext, + Injectable, + NestInterceptor, + StreamableFile, +} from '@nestjs/common'; +import { Observable, map } from 'rxjs'; +import { REQUEST_ID_HEADER } from '../middleware/request-id.middleware'; + +export interface ApiResponse { + code: number; + message: string; + data: T; + requestId: string; +} + +@Injectable() +export class ResponseInterceptor + implements NestInterceptor | StreamableFile> +{ + intercept( + context: ExecutionContext, + next: CallHandler, + ): Observable | StreamableFile> { + const req = context.switchToHttp().getRequest(); + const requestId = (req.headers[REQUEST_ID_HEADER] as string) || ''; + return next.handle().pipe( + map((data) => { + if (data instanceof StreamableFile) return data; + return { + code: 0, + message: 'ok', + data: (data ?? null) as T, + requestId, + }; + }), + ); + } +} diff --git a/apps/api/src/common/middleware/request-id.middleware.ts b/apps/api/src/common/middleware/request-id.middleware.ts new file mode 100644 index 0000000..19afc8b --- /dev/null +++ b/apps/api/src/common/middleware/request-id.middleware.ts @@ -0,0 +1,15 @@ +import { randomUUID } from 'crypto'; +import { Injectable, NestMiddleware } from '@nestjs/common'; +import { NextFunction, Request, Response } from 'express'; + +export const REQUEST_ID_HEADER = 'x-request-id'; + +@Injectable() +export class RequestIdMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + const id = (req.headers[REQUEST_ID_HEADER] as string) || randomUUID(); + req.headers[REQUEST_ID_HEADER] = id; + res.setHeader(REQUEST_ID_HEADER, id); + next(); + } +} diff --git a/apps/api/src/common/org-chain.spec.ts b/apps/api/src/common/org-chain.spec.ts new file mode 100644 index 0000000..72c74a8 --- /dev/null +++ b/apps/api/src/common/org-chain.spec.ts @@ -0,0 +1,42 @@ +import { assembleApprovalChain } from './org-chain'; + +const staff = { id: 'staff', displayName: '员工甲', username: 'a' }; +const pm = { id: 'pm', displayName: '项目经理', username: 'pm' }; +const dir = { id: 'dir', displayName: '研发总监', username: 'rd' }; +const pan = { id: 'pan', displayName: '潘兴辉', username: 'pan' }; +const biz = { id: 'biz', displayName: '王迪', username: 'wangdi' }; + +describe('assembleApprovalChain', () => { + it('staff goes PM → dept director → 潘兴辉 → 商务总监', () => { + expect( + assembleApprovalChain({ + applicantId: staff.id, + manager: pm, + director: dir, + pan, + bizDirector: biz, + }).map((s) => `${s.role}:${s.user.displayName}`), + ).toEqual(['项目经理:项目经理', '部门总监:研发总监', '潘兴辉:潘兴辉', '商务总监:王迪']); + }); + + it('skips the applicant and duplicate people', () => { + expect( + assembleApprovalChain({ + applicantId: pm.id, + manager: pm, + director: dir, + pan, + bizDirector: biz, + }).map((s) => s.role), + ).toEqual(['部门总监', '潘兴辉', '商务总监']); + expect( + assembleApprovalChain({ + applicantId: staff.id, + manager: pm, + director: biz, + pan, + bizDirector: biz, + }).map((s) => s.role), + ).toEqual(['项目经理', '部门总监', '潘兴辉']); + }); +}); diff --git a/apps/api/src/common/org-chain.ts b/apps/api/src/common/org-chain.ts new file mode 100644 index 0000000..11ecadb --- /dev/null +++ b/apps/api/src/common/org-chain.ts @@ -0,0 +1,209 @@ +import type { Prisma } from '@prisma/client'; +import { DIRECTOR_ROLES, TECH_LEAD_ROLES } from './role-groups'; +import { PrismaService } from '../prisma/prisma.service'; + +export const PERSON_SELECT = { id: true, displayName: true, username: true } as const; +export type OrgPerson = { id: string; displayName: string; username: string }; + +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + +export function parsePeopleIds(raw?: string | null) { + if (!raw) return [] as string[]; + return [ + ...new Set( + raw + .split(/[,,、\s]+/) + .map((s) => s.trim()) + .filter((s) => UUID_RE.test(s)), + ), + ]; +} + +type OrgDb = PrismaService | Prisma.TransactionClient; + +const PAN_NAME = '潘兴辉'; + +export function assembleApprovalChain(input: { + applicantId: string; + manager: OrgPerson | null; + director: OrgPerson | null; + pan: OrgPerson | null; + bizDirector: OrgPerson | null; +}): { role: string; user: OrgPerson }[] { + const steps: { role: string; user: OrgPerson }[] = []; + const push = (role: string, user: OrgPerson | null | undefined) => { + if (!user) return; + if (user.id === input.applicantId) return; + if (steps.some((s) => s.user.id === user.id)) return; + steps.push({ role, user }); + }; + push('项目经理', input.manager); + push('部门总监', input.director); + push('潘兴辉', input.pan); + push('商务总监', input.bizDirector); + return steps; +} + +export async function ancestorDeptIds(db: OrgDb, startId?: string | null) { + if (!startId) return [] as string[]; + const all = await db.department.findMany({ + where: { tenantId: '1' }, + select: { id: true, parentId: true }, + }); + const ids: string[] = []; + const seen = new Set(); + let cur: string | null | undefined = startId; + while (cur && !seen.has(cur)) { + seen.add(cur); + ids.push(cur); + cur = all.find((d) => d.id === cur)?.parentId; + } + return ids; +} + +export async function findDeptDirector(db: OrgDb, departmentId?: string | null): Promise { + const chain = await ancestorDeptIds(db, departmentId); + for (const deptId of chain) { + const tech = await db.user.findFirst({ + where: { + tenantId: '1', + status: 'ACTIVE', + employee: { departmentId: deptId }, + roles: { some: { role: { code: { in: [...TECH_LEAD_ROLES] } } } }, + }, + select: PERSON_SELECT, + }); + if (tech) return tech; + const any = await db.user.findFirst({ + where: { + tenantId: '1', + status: 'ACTIVE', + employee: { departmentId: deptId }, + roles: { some: { role: { code: { in: [...DIRECTOR_ROLES] } } } }, + }, + select: PERSON_SELECT, + }); + if (any) return any; + } + return null; +} + +export async function findNamedPerson(db: OrgDb, name: string): Promise { + const users = await db.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + OR: [{ displayName: { contains: name } }, { employee: { name: { contains: name } } }], + }, + select: { ...PERSON_SELECT, employee: { select: { name: true } } }, + }); + const exact = users.find((u) => u.displayName === name || u.employee?.name === name); + const hit = exact || users[0]; + return hit ? { id: hit.id, displayName: hit.displayName, username: hit.username } : null; +} + +/** 投标/报销里的「商务总监」指商务部负责人,不含其他部门误挂 biz_director 的同事。 */ +export async function findBizDirectors(db: OrgDb): Promise { + const inBizDept = await db.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + roles: { some: { role: { code: 'biz_director' } } }, + employee: { + department: { + OR: [{ name: { contains: '商务' } }, { code: { contains: 'BIZ' } }, { code: { startsWith: 'OA_4' } }], + }, + }, + }, + select: PERSON_SELECT, + orderBy: { displayName: 'asc' }, + }); + if (inBizDept.length) return inBizDept; + return db.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + roles: { some: { role: { code: 'biz_director' } } }, + }, + select: PERSON_SELECT, + orderBy: { displayName: 'asc' }, + }); +} + +export async function findRolePerson(db: OrgDb, roleCode: string, exceptId?: string | null): Promise { + if (roleCode === 'biz_director') { + const users = await findBizDirectors(db); + return users.find((u) => u.id !== exceptId) || users[0] || null; + } + const users = await db.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + roles: { some: { role: { code: roleCode } } }, + }, + select: PERSON_SELECT, + }); + return users.find((u) => u.id !== exceptId) || users[0] || null; +} + +function managerUserOf(emp: { + manager?: { users: Array<{ id: string; displayName: string; username: string }> } | null; +} | null): OrgPerson | null { + const u = emp?.manager?.users[0]; + return u ? { id: u.id, displayName: u.displayName, username: u.username } : null; +} + +export async function resolveExpenseApproverChain(db: OrgDb, applicantId: string) { + const applicant = await db.user.findFirst({ + where: { id: applicantId }, + include: { + employee: { + include: { + manager: { include: { users: { where: { status: 'ACTIVE' }, take: 1 } } }, + }, + }, + }, + }); + const [director, pan, bizDirector] = await Promise.all([ + findDeptDirector(db, applicant?.employee?.departmentId), + findNamedPerson(db, PAN_NAME), + findRolePerson(db, 'biz_director'), + ]); + return assembleApprovalChain({ + applicantId, + manager: managerUserOf(applicant?.employee || null), + director, + pan, + bizDirector, + }); +} + +export async function resolveReportPeople(db: OrgDb, userId: string) { + const me = await db.user.findFirst({ + where: { id: userId }, + include: { + employee: { + include: { + manager: { include: { users: { where: { status: 'ACTIVE' }, take: 1 } } }, + reports: true, + }, + }, + }, + }); + const director = await findDeptDirector(db, me?.employee?.departmentId); + const extraIds = parsePeopleIds((me?.employee as { managerIds?: string | null } | undefined)?.managerIds).filter( + (id) => id !== me?.employee?.managerId, + ); + const extraManagers = extraIds.length + ? await db.user.findMany({ + where: { tenantId: '1', status: 'ACTIVE', employeeId: { in: extraIds } }, + select: PERSON_SELECT, + }) + : []; + return { + me, + manager: managerUserOf(me?.employee || null), + extraManagers, + director, + }; +} diff --git a/apps/api/src/common/role-groups.ts b/apps/api/src/common/role-groups.ts new file mode 100644 index 0000000..3aabc7d --- /dev/null +++ b/apps/api/src/common/role-groups.ts @@ -0,0 +1,60 @@ +export const TECH_LEAD_ROLES = [ + 'tech_director', + 'rd_director', + '3d_director', + 'video_director', + 'material_director', +]; + +/** 商务总监 + 各技术条线总监 */ +export const DIRECTOR_ROLES = ['biz_director', ...TECH_LEAD_ROLES]; + +/** 手工发短信:管理员、人事、各总监。 */ +export const SMS_SEND_ROLES = ['admin', 'owner', 'hr', ...DIRECTOR_ROLES]; + +export function canSendSms(roles: string[], permissions: string[] = []) { + if (hasAnyRole(roles, SMS_SEND_ROLES)) return true; + return permissions.includes('office:sms') || permissions.includes('system:sms'); +} + +export const TECH_STAFF_ROLES = ['employee', 'pm', 'rd_staff', '3d_staff', 'video_staff', 'material_staff', 'tech']; + +export const APPROVER_ROLES = [ + 'admin', + 'owner', + 'biz_director', + 'biz_staff', + 'hr', + 'finance', + ...TECH_LEAD_ROLES, +]; + +export function isStaffRole(roles: string[]) { + return roles.length > 0 && roles.every((r) => TECH_STAFF_ROLES.includes(r)); +} + +export function canApproveOthers(roles: string[], permissions: string[]) { + if (hasAnyRole(roles, APPROVER_ROLES)) return true; + return permissions.includes('office:approvals'); +} + +export function hasAnyRole(roles: string[], codes: readonly string[]) { + return codes.some((c) => roles.includes(c)); +} + +export function isTechLead(roles: string[]) { + return hasAnyRole(roles, TECH_LEAD_ROLES); +} + +/** 研发总监与系统管理员同等。登录后补上 admin,后端原有管理员判断都会生效。 */ +export function elevateToAdmin(roles: string[]) { + if (roles.includes('rd_director') && !roles.includes('admin')) return [...roles, 'admin']; + return roles; +} + +/** 总监、项目经理、公司负责人可给下属分配工作。 */ +export const WORK_ASSIGN_ROLES = ['admin', 'owner', 'biz_director', 'pm', ...TECH_LEAD_ROLES]; + +export function canAssignWork(roles: string[]) { + return hasAnyRole(roles, WORK_ASSIGN_ROLES); +} diff --git a/apps/api/src/common/tencent-lbs.ts b/apps/api/src/common/tencent-lbs.ts new file mode 100644 index 0000000..bd19dc3 --- /dev/null +++ b/apps/api/src/common/tencent-lbs.ts @@ -0,0 +1,288 @@ +function key() { + return process.env.TENCENT_MAP_KEY || ''; +} + +const REFERER = 'https://oa.fysxkj.com/'; + +async function tencentGet(path: string, params: Record) { + const k = key(); + if (!k) return null; + const q = new URLSearchParams({ key: k, output: 'json', ...params }); + const res = await fetch(`https://apis.map.qq.com${path}?${q.toString()}`, { + headers: { Referer: REFERER }, + }); + if (!res.ok) return null; + return (await res.json()) as Record; +} + +export async function locateByIp(ip?: string) { + const json = await tencentGet('/ws/location/v1/ip', ip ? { ip } : {}); + const result = json?.result as + | { location?: { lat?: number; lng?: number }; ad_info?: { city?: string; province?: string; district?: string } } + | undefined; + if (json?.status !== 0 || !result?.location) return null; + return { + lat: Number(result.location.lat), + lng: Number(result.location.lng), + city: String(result.ad_info?.city || result.ad_info?.province || '').replace(/市$/, ''), + district: String(result.ad_info?.district || ''), + }; +} + +export function tencentMapKey() { + return key(); +} + +export type PlaceHit = { + lat: number; + lng: number; + title: string; + address: string; + city?: string; + distance?: number; +}; + +function asPoi(p: { + title?: string; + address?: string; + location?: { lat?: number; lng?: number }; + _distance?: number | string; + ad_info?: { city?: string }; +}): PlaceHit | null { + const lat = Number(p.location?.lat); + const lng = Number(p.location?.lng); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null; + const dist = Number(p._distance); + return { + lat, + lng, + title: String(p.title || p.address || '位置'), + address: String(p.address || ''), + city: String(p.ad_info?.city || '').replace(/市$/, ''), + distance: Number.isFinite(dist) ? dist : undefined, + }; +} + +export async function reverseGeocode(lat: number, lng: number) { + const json = await tencentGet('/ws/geocoder/v1/', { + location: `${lat},${lng}`, + get_poi: '1', + poi_options: 'address_format=short;radius=1000;page_size=20;policy=2', + }); + const result = json?.result as + | { + address?: string; + formatted_addresses?: { recommend?: string; rough?: string }; + address_component?: { city?: string; province?: string; district?: string }; + pois?: Array<{ + title?: string; + address?: string; + location?: { lat?: number; lng?: number }; + _distance?: number | string; + }>; + } + | undefined; + if (json?.status !== 0 || !result) return null; + const address = String(result.address || ''); + const title = String(result.formatted_addresses?.recommend || result.formatted_addresses?.rough || address); + return { + address, + title: title || address, + city: String(result.address_component?.city || result.address_component?.province || '').replace(/市$/, ''), + district: String(result.address_component?.district || ''), + lat, + lng, + pois: (result.pois || []).map(asPoi).filter((p): p is PlaceHit => !!p), + }; +} + +export async function suggestPlaces(keyword: string, lat?: number, lng?: number) { + const q = keyword.trim(); + if (!q) return [] as PlaceHit[]; + const params: Record = { keyword: q, page_size: '20' }; + if (Number.isFinite(Number(lat)) && Number.isFinite(Number(lng))) params.location = `${lat},${lng}`; + const json = await tencentGet('/ws/place/v1/suggestion/', params); + const rows = (json?.data as Array[0]>) || []; + return rows.map(asPoi).filter((p): p is PlaceHit => !!p); +} + +const CITIES = [ + '合肥', + '南京', + '北京', + '上海', + '杭州', + '苏州', + '无锡', + '宁波', + '武汉', + '成都', + '西安', + '广州', + '深圳', + '青岛', + '济南', + '郑州', + '长沙', + '南昌', + '福州', + '厦门', + '天津', + '重庆', + '沈阳', + '大连', + '哈尔滨', + '长春', + '昆明', + '贵阳', + '南宁', + '海口', + '兰州', + '银川', + '西宁', + '太原', + '石家庄', + '呼和浩特', + '乌鲁木齐', +]; + +function cityHint(text: string) { + return CITIES.find((c) => text.includes(c)) || ''; +} + +function poiScore(query: string, title: string, addr: string) { + const hay = `${title}${addr}`; + if (title === query) return 1000; + if (title.includes(query) || query.includes(title)) return 800; + let s = 0; + for (const part of query.match(/[\u4e00-\u9fff]{2,}/g) || []) { + if (hay.includes(part)) s += part.length * 10; + } + return s; +} + +type GeoHit = { + lat: number; + lng: number; + title: string; + address: string; + city: string; +}; + +function asHit(lat: number, lng: number, title: string, address: string, city: string): GeoHit { + return { lat, lng, title, address, city }; +} + +export async function geocodeAddress(address: string) { + const q = address.trim(); + if (!q) return null; + const geo = await tencentGet('/ws/geocoder/v1/', { address: q }); + const geoResult = geo?.result as + | { location?: { lat?: number; lng?: number }; title?: string; address?: string } + | undefined; + if (geo?.status === 0 && geoResult?.location) { + const extra = await reverseGeocode(Number(geoResult.location.lat), Number(geoResult.location.lng)); + return asHit( + Number(geoResult.location.lat), + Number(geoResult.location.lng), + String(geoResult.title || q), + extra?.address || String(geoResult.address || q), + extra?.city || '', + ); + } + + const city = cityHint(q); + const [sug, search] = await Promise.all([ + tencentGet('/ws/place/v1/suggestion/', city ? { keyword: q, region: city } : { keyword: q }), + tencentGet('/ws/place/v1/search/', { + keyword: q, + boundary: city ? `region(${city},0)` : 'region(全国,0)', + page_size: '10', + }), + ]); + type Poi = { title?: string; address?: string; location?: { lat?: number; lng?: number }; ad_info?: { city?: string } }; + const pois = [...((sug?.data as Poi[]) || []), ...((search?.data as Poi[]) || [])].filter( + (p) => p?.location && Number.isFinite(Number(p.location.lat)), + ); + if (!pois.length) return null; + const ranked = [...pois].sort( + (a, b) => + poiScore(q, String(b.title || ''), String(b.address || '')) - + poiScore(q, String(a.title || ''), String(a.address || '')), + ); + const best = ranked[0]; + const extra = await reverseGeocode(Number(best.location!.lat), Number(best.location!.lng)); + return asHit( + Number(best.location!.lat), + Number(best.location!.lng), + String(best.title || q), + extra?.address || String(best.address || q), + extra?.city || String(best.ad_info?.city || city).replace(/市$/, ''), + ); +} + +type WeatherNow = { + city: string; + weather: string; + temperature?: number; + humidity?: number; + wind?: string; + advice: string; +}; + +function adviceFrom(info: string, temp?: number) { + const tips: string[] = []; + if (/雨|雪|雷/.test(info)) tips.push('注意下雨,出门带伞'); + if (/晴/.test(info) || (typeof temp === 'number' && temp >= 30)) tips.push('注意紫外线,做好防晒'); + if (typeof temp === 'number' && temp >= 35) tips.push('注意防暑降温'); + if (typeof temp === 'number' && temp <= 5) tips.push('注意保暖'); + if (/雾|霾|沙/.test(info)) tips.push('注意出行能见度'); + if (/风/.test(info) && !/微风/.test(info)) tips.push('注意大风'); + if (!tips.length) tips.push('注意劳逸结合'); + return [...new Set(tips)].slice(0, 2).join(','); +} + +export async function weatherByLocation(lat: number, lng: number, cityHint?: string): Promise { + const json = await tencentGet('/ws/weather/v1/', { + location: `${lat},${lng}`, + type: 'now', + added_fields: 'alarm', + }); + if (json?.status !== 0) return null; + const result = json.result as { realtime?: Array> } | undefined; + const row = result?.realtime?.[0]; + if (!row) return null; + const infos = (row.infos || {}) as Record; + const info = String(infos.weather || '—'); + const temperature = Number(infos.temperature); + const humidity = Number(infos.humidity); + const windDir = String(infos.wind_direction || ''); + const windPower = String(infos.wind_power_v2 || infos.wind_power || ''); + const city = String(row.city || cityHint || '').replace(/市$/, ''); + return { + city: city || '当地', + weather: info, + temperature: Number.isFinite(temperature) ? temperature : undefined, + humidity: Number.isFinite(humidity) ? humidity : undefined, + wind: [windDir, windPower].filter(Boolean).join(' ') || undefined, + advice: adviceFrom(info, Number.isFinite(temperature) ? temperature : undefined), + }; +} + +const GREETINGS = [ + (n: string) => `尊敬的${n}您好,美好的一天开始了,祝您工作愉快。`, + (n: string) => `尊敬的${n},新的一天已就绪,愿今天经手的每一件事都顺利落地。`, + (n: string) => `${n},早上好。保持节奏,今天也会很出色。`, + (n: string) => `尊敬的${n}您好,愿今天思路清晰、协作顺畅。`, + (n: string) => `${n},新的一天值得认真对待,也值得对自己温柔一点。`, + (n: string) => `尊敬的${n},把难事先拆小步,今天就会轻松很多。`, + (n: string) => `${n}您好,专注当下这一件事,成果会自己跟上。`, +]; + +export function dailyGreeting(name: string, seed: string) { + const day = new Date().toISOString().slice(0, 10); + let h = 0; + const s = `${day}:${seed}`; + for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; + return GREETINGS[h % GREETINGS.length](name || '同事'); +} diff --git a/apps/api/src/config/env.spec.ts b/apps/api/src/config/env.spec.ts new file mode 100644 index 0000000..b4657d1 --- /dev/null +++ b/apps/api/src/config/env.spec.ts @@ -0,0 +1,22 @@ +import { loadEnv } from './env'; + +describe('loadEnv', () => { + const base = { + DATABASE_URL: 'postgresql://ops:ops@localhost:5432/enterprise_ops', + REDIS_URL: 'redis://localhost:6379', + MINIO_ACCESS_KEY: 'opsadmin', + MINIO_SECRET_KEY: 'opsadmin_dev_only', + JWT_ACCESS_SECRET: 'change-me-access-secret-dev-only', + JWT_REFRESH_SECRET: 'change-me-refresh-secret-dev-only', + }; + + it('accepts valid development env', () => { + const env = loadEnv({ ...base, NODE_ENV: 'development' }); + expect(env.APP_PORT).toBe(3000); + expect(env.API_PREFIX).toBe('api/v1'); + }); + + it('rejects missing database url', () => { + expect(() => loadEnv({ ...base, DATABASE_URL: '' })).toThrow(/环境变量校验失败/); + }); +}); diff --git a/apps/api/src/config/env.ts b/apps/api/src/config/env.ts new file mode 100644 index 0000000..fe4a8b6 --- /dev/null +++ b/apps/api/src/config/env.ts @@ -0,0 +1,60 @@ +import { z } from 'zod'; + +export const envSchema = z.object({ + NODE_ENV: z.enum(['development', 'test', 'production']).default('development'), + APP_PORT: z.coerce.number().default(3000), + APP_NAME: z.string().default('enterprise-ops'), + API_PREFIX: z.string().default('api/v1'), + CORS_ORIGINS: z.string().default('http://localhost:5173'), + DATABASE_URL: z.string().min(1), + REDIS_URL: z.string().min(1), + MINIO_ENDPOINT: z.string().default('localhost'), + MINIO_PORT: z.coerce.number().default(9000), + MINIO_USE_SSL: z.coerce.boolean().default(false), + MINIO_ACCESS_KEY: z.string().min(1), + MINIO_SECRET_KEY: z.string().min(1), + MINIO_BUCKET: z.string().default('enterprise-ops'), + JWT_ACCESS_SECRET: z.string().min(16), + JWT_REFRESH_SECRET: z.string().min(16), + JWT_ACCESS_EXPIRES: z.string().default('15m'), + JWT_REFRESH_EXPIRES: z.string().default('7d'), + ALIYUN_SMS_ACCESS_KEY_ID: z.string().optional(), + ALIYUN_SMS_ACCESS_KEY_SECRET: z.string().optional(), + ALIYUN_SMS_ENDPOINT: z.string().optional(), + ALIYUN_SMS_REGION: z.string().optional(), + ALIYUN_SMS_DEFAULT_SIGN: z.string().optional(), + ALIYUN_RAM_LOGIN: z.string().optional(), + ALIYUN_RAM_SECURITY_PHONE: z.string().optional(), + ALIYUN_RAM_SECURITY_EMAIL: z.string().optional(), + TENCENT_MAP_KEY: z.string().optional(), + IM_TCP_PORT: z.coerce.number().optional(), + IM_HTTP_PORT: z.coerce.number().optional(), + IM_INTERNAL_TOKEN: z.string().optional(), + VIVO_PUSH_APP_ID: z.string().optional(), + VIVO_PUSH_APP_KEY: z.string().optional(), + VIVO_PUSH_APP_SECRET: z.string().optional(), + OPPO_PUSH_APP_ID: z.string().optional(), + OPPO_PUSH_APP_KEY: z.string().optional(), + OPPO_PUSH_APP_SECRET: z.string().optional(), + OPPO_PUSH_MASTER_SECRET: z.string().optional(), + MEIZU_PUSH_APP_ID: z.string().optional(), + MEIZU_PUSH_APP_KEY: z.string().optional(), + MEIZU_PUSH_APP_SECRET: z.string().optional(), + HUAWEI_PUSH_APP_ID: z.string().optional(), + HUAWEI_PUSH_CLIENT_ID: z.string().optional(), + HUAWEI_PUSH_CLIENT_SECRET: z.string().optional(), + HUAWEI_PUSH_PROJECT_ID: z.string().optional(), +}); + +export type Env = z.infer; + +export function loadEnv(raw: NodeJS.ProcessEnv = process.env): Env { + const parsed = envSchema.safeParse(raw); + if (!parsed.success) { + const details = parsed.error.issues + .map((i) => `${i.path.join('.')}: ${i.message}`) + .join('; '); + throw new Error(`环境变量校验失败: ${details}`); + } + return parsed.data; +} diff --git a/apps/api/src/contract/contract-workflow.spec.ts b/apps/api/src/contract/contract-workflow.spec.ts new file mode 100644 index 0000000..a4817ca --- /dev/null +++ b/apps/api/src/contract/contract-workflow.spec.ts @@ -0,0 +1,26 @@ +import { + canAccessContract, + canApproveContract, + canEditContract, + milestoneSumOk, +} from './contract-workflow'; + +describe('contract-workflow', () => { + it('allows biz roles to draft, and owner/director to approve', () => { + expect(canEditContract(['biz_staff'])).toBe(true); + expect(canEditContract(['tech_director'])).toBe(false); + expect(canApproveContract(['owner'])).toBe(true); + expect(canApproveContract(['biz_staff'])).toBe(false); + }); + + it('requires milestone ratios to sum to 100', () => { + expect(milestoneSumOk([60, 40])).toBe(true); + expect(milestoneSumOk([30, 30, 40])).toBe(true); + expect(milestoneSumOk([50, 40])).toBe(false); + }); + + it('lets finance see contracts via permission', () => { + expect(canAccessContract(['finance'], ['contract:list'])).toBe(true); + expect(canAccessContract(['employee'], ['office'])).toBe(false); + }); +}); diff --git a/apps/api/src/contract/contract-workflow.ts b/apps/api/src/contract/contract-workflow.ts new file mode 100644 index 0000000..35e1c75 --- /dev/null +++ b/apps/api/src/contract/contract-workflow.ts @@ -0,0 +1,29 @@ +export const CONTRACT_STATUS = { + DRAFT: 'DRAFT', + PENDING: 'PENDING', + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', +} as const; + +export const CONTRACT_BIZ = { + APPROVAL: 'CONTRACT_APPROVAL', + DRAFT: 'CONTRACT_DRAFT', +} as const; + +export function canAccessContract(roles: string[], permissions: string[]) { + if (roles.includes('admin')) return true; + return permissions.some((p) => p === 'contract' || p.startsWith('contract:') || p === 'bid' || p.startsWith('bid:')); +} + +export function canEditContract(roles: string[]) { + return ['admin', 'owner', 'biz_director', 'biz_staff'].some((r) => roles.includes(r)); +} + +export function canApproveContract(roles: string[]) { + return ['admin', 'owner', 'biz_director'].some((r) => roles.includes(r)); +} + +export function milestoneSumOk(ratios: number[]) { + const sum = ratios.reduce((a, b) => a + b, 0); + return Math.abs(sum - 100) < 0.01; +} diff --git a/apps/api/src/contract/contract.controller.ts b/apps/api/src/contract/contract.controller.ts new file mode 100644 index 0000000..cb5ed1c --- /dev/null +++ b/apps/api/src/contract/contract.controller.ts @@ -0,0 +1,102 @@ +import { Body, Controller, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator'; +import { ContractService } from './contract.service'; + +@ApiTags('contract') +@ApiBearerAuth() +@Controller() +export class ContractController { + constructor(private readonly contracts: ContractService) {} + + @Get('contracts') + list( + @CurrentUser() user: AuthUser, + @Query('page') page = '1', + @Query('pageSize') pageSize = '20', + @Query('keyword') keyword?: string, + @Query('bucket') bucket?: string, + ) { + return this.contracts.list(user, Number(page) || 1, Number(pageSize) || 20, keyword, bucket); + } + + @Get('contracts/:id') + get(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.contracts.get(id, user); + } + + @Post('contracts') + create( + @CurrentUser() user: AuthUser, + @Body() + body: { + bidCaseId?: string; + legalEntityId?: string; + amount: number; + name?: string; + partyA?: string; + partyB?: string; + remark?: string; + contractNo?: string; + kind?: string; + }, + ) { + if (body.bidCaseId && !body.kind) return this.contracts.createFromBid(body, user); + return this.contracts.createIncremental(body, user); + } + + @Patch('contracts/:id') + update( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() + body: { + name?: string; + amount?: number; + partyA?: string; + partyB?: string; + remark?: string; + nasPath?: string; + contractNo?: string; + }, + ) { + return this.contracts.update(id, user, body); + } + + @Patch('contracts/:id/kind') + moveKind(@CurrentUser() user: AuthUser, @Param('id') id: string, @Body() body: { kind?: string }) { + return this.contracts.moveKind(id, user, body.kind || 'OTHER'); + } + + @Put('contracts/:id/milestones') + milestones( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { items: Array<{ name: string; ratio: number; dueAt?: string; trigger?: string }> }, + ) { + return this.contracts.replaceMilestones(id, user, body.items || []); + } + + @Post('contracts/:id/seal') + seal( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { sealType: string; reason: string; takeOut?: boolean; returnAt?: string }, + ) { + return this.contracts.createSeal(id, user, body); + } + + @Post('contracts/:id/submit') + submit(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.contracts.submit(id, user); + } + + @Post('contracts/:id/reviews') + review( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { result: 'APPROVED' | 'REJECTED'; comment?: string }, + ) { + return this.contracts.review(id, user, body); + } +} diff --git a/apps/api/src/contract/contract.service.ts b/apps/api/src/contract/contract.service.ts new file mode 100644 index 0000000..1267cf5 --- /dev/null +++ b/apps/api/src/contract/contract.service.ts @@ -0,0 +1,666 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { BIZ_TYPE } from '../bidding/bid-workflow'; +import { SealService } from '../seal/seal.service'; +import { CONTRACT_BIZ, CONTRACT_STATUS, canAccessContract, canEditContract, milestoneSumOk } from './contract-workflow'; +import { nextBizNo } from '../system/numbering'; +import { assertCanSeeOwner, ownerScope, withAnd } from '../common/data-scope'; +import { BUDGET_CATEGORY, PROJECT_STATUS, splitEven } from '../project/project-workflow'; + +const USER_SELECT = { id: true, displayName: true, username: true }; + +const DETAIL_INCLUDE = { + bidCase: { select: { id: true, bidNo: true, name: true, status: true, quoteAmount: true, party: { select: { name: true } } } }, + legalEntity: true, + createdBy: { select: USER_SELECT }, + milestones: { include: { invoice: true }, orderBy: { dueAt: 'asc' as const } }, + reviewers: { + include: { user: { select: USER_SELECT } }, + orderBy: { decidedAt: 'asc' as const }, + }, + actionLogs: { + include: { actor: { select: USER_SELECT } }, + orderBy: { createdAt: 'asc' as const }, + }, + sealRequests: { orderBy: { createdAt: 'desc' as const } }, + invoices: true, + projects: true, +}; + +type Tx = Prisma.TransactionClient; + +export type CreateContractInput = { + bidCaseId?: string; + legalEntityId?: string; + amount: number; + name?: string; + partyA?: string; + partyB?: string; + remark?: string; + contractNo?: string; + kind?: string; +}; + +export type UpdateContractInput = { + name?: string; + amount?: number; + partyA?: string; + partyB?: string; + remark?: string; + nasPath?: string; + contractNo?: string; +}; + +function officialContractNo(raw?: string | null) { + const n = (raw || '').trim(); + if (!n || n === '待填' || n === '-' || n === '—') return ''; + return n; +} + +export type MilestoneInput = { + name: string; + ratio: number; + dueAt?: string; + trigger?: string; +}; + +export type ReviewInput = { + result: 'APPROVED' | 'REJECTED'; + comment?: string; +}; + +export type SealInput = { + sealType: string; + reason: string; + takeOut?: boolean; + returnAt?: string; +}; + +@Injectable() +export class ContractService { + constructor( + private readonly prisma: PrismaService, + private readonly seals: SealService, + ) {} + + assertAccess(user: AuthUser) { + if (!canAccessContract(user.roles, user.permissions)) { + throw new ForbiddenException('无权访问合同管理'); + } + } + + async list(user: AuthUser, page: number, pageSize: number, keyword?: string, bucket?: string) { + this.assertAccess(user); + const where: Prisma.ContractWhereInput = { tenantId: '1' }; + if (bucket === 'tender' || bucket === 'bid' || bucket === 'approval') { + where.OR = [{ kind: 'TENDER' }, { bidCaseId: { not: null } }]; + } + if (bucket === 'direct') { + where.kind = 'DIRECT'; + } + if (bucket === 'incremental' || bucket === 'other') { + where.kind = 'OTHER'; + where.bidCaseId = null; + } + if (keyword) { + where.AND = [ + { + OR: [ + { name: { contains: keyword } }, + { contractNo: { contains: keyword } }, + { partyA: { contains: keyword } }, + { partyB: { contains: keyword } }, + ], + }, + ]; + } + const scoped = withAnd(where, (await ownerScope(this.prisma, user, 'createdById')) as Record); + const [items, total] = await this.prisma.$transaction([ + this.prisma.contract.findMany({ + where: scoped, + include: { + bidCase: { + select: { + id: true, + bidNo: true, + externalNo: true, + name: true, + party: { select: { name: true } }, + createdBy: { select: USER_SELECT }, + }, + }, + legalEntity: true, + createdBy: { select: USER_SELECT }, + projects: { select: { id: true, projectNo: true, status: true } }, + milestones: true, + }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.contract.count({ where: scoped }), + ]); + return { items, page, pageSize, total }; + } + + async get(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.contract.findFirst({ + where: { id, tenantId: '1' }, + include: DETAIL_INCLUDE, + }); + if (!row) throw new NotFoundException('合同不存在'); + await assertCanSeeOwner(this.prisma, user, row.createdById); + const files = await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizId: id, bizType: { startsWith: 'CONTRACT' } }, + orderBy: { createdAt: 'asc' }, + }); + return { ...row, files, myActions: this.myActions(row, user) }; + } + + async createIncremental(dto: CreateContractInput, user: AuthUser) { + this.assertAccess(user); + if (!canEditContract(user.roles)) { + throw new ForbiddenException('无权起草合同'); + } + const name = dto.name?.trim(); + if (!name) throw new BadRequestException('请填写合同名称'); + if (!dto.amount || dto.amount <= 0) throw new BadRequestException('请填写合同金额'); + const kind = dto.kind === 'TENDER' || dto.kind === 'DIRECT' ? dto.kind : 'OTHER'; + const contract = await this.prisma.$transaction(async (tx) => { + const entity = dto.legalEntityId + ? await tx.legalEntity.findFirst({ where: { id: dto.legalEntityId, tenantId: '1' } }) + : (await tx.legalEntity.findFirst({ where: { tenantId: '1', isDefault: true } })) || + (await tx.legalEntity.findFirst({ where: { tenantId: '1' } })); + if (dto.legalEntityId && !entity) throw new NotFoundException('我方主体不存在'); + if (kind === 'TENDER' && dto.bidCaseId) { + const bid = await tx.bidCase.findFirst({ where: { id: dto.bidCaseId, tenantId: '1' } }); + if (!bid) throw new NotFoundException('投标事项不存在'); + const existing = await tx.contract.findFirst({ + where: { + bidCaseId: bid.id, + status: { in: [CONTRACT_STATUS.DRAFT, CONTRACT_STATUS.PENDING, CONTRACT_STATUS.APPROVED] }, + }, + }); + if (existing) { + throw new BadRequestException('该中标事项已自动生成合同草稿,请到合同管理完善后提交审批'); + } + } + const contractNo = officialContractNo(dto.contractNo) || null; + if (contractNo) { + const clash = await tx.contract.findFirst({ where: { tenantId: '1', contractNo } }); + if (clash) throw new BadRequestException('该合同编号已存在'); + } + const created = await tx.contract.create({ + data: { + tenantId: '1', + contractNo, + name, + kind, + bidCaseId: kind === 'TENDER' ? dto.bidCaseId || undefined : undefined, + legalEntityId: entity?.id, + amount: dto.amount, + partyA: dto.partyA?.trim() || undefined, + partyB: dto.partyB?.trim() || entity?.name || undefined, + remark: dto.remark, + status: CONTRACT_STATUS.DRAFT, + createdById: user.id, + }, + }); + const label = kind === 'TENDER' ? '投标合同' : kind === 'DIRECT' ? '直签合同' : '其他合同'; + await this.log(tx, created.id, user.id, 'CREATE', undefined, CONTRACT_STATUS.DRAFT, `新建${label}`); + return created; + }); + return this.get(contract.id, user); + } + + async createFromBid(dto: CreateContractInput, user: AuthUser) { + this.assertAccess(user); + if (!canEditContract(user.roles)) { + throw new ForbiddenException('无权起草合同'); + } + if (!dto.bidCaseId) throw new BadRequestException('请选择投标事项'); + if (!dto.amount || dto.amount <= 0) { + throw new BadRequestException('请填写合同金额'); + } + const contract = await this.prisma.$transaction(async (tx) => { + const bid = await tx.bidCase.findFirst({ + where: { id: dto.bidCaseId, tenantId: '1' }, + include: { party: true, legalEntity: true }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + if (bid.status !== 'WON') { + throw new BadRequestException('只有中标事项可以转合同'); + } + const existing = await tx.contract.findFirst({ + where: { + bidCaseId: bid.id, + status: { in: [CONTRACT_STATUS.DRAFT, CONTRACT_STATUS.PENDING, CONTRACT_STATUS.APPROVED] }, + }, + }); + if (existing) { + throw new BadRequestException('该中标事项已自动生成合同草稿,请到合同管理完善后提交审批'); + } + const contractNo = officialContractNo(dto.contractNo) || null; + if (contractNo) { + const clash = await tx.contract.findFirst({ where: { tenantId: '1', contractNo } }); + if (clash) throw new BadRequestException('该合同编号已存在'); + } + const created = await tx.contract.create({ + data: { + tenantId: '1', + contractNo, + name: dto.name?.trim() || bid.name, + bidCaseId: bid.id, + legalEntityId: bid.legalEntityId, + amount: dto.amount, + partyA: dto.partyA?.trim() || bid.party?.name || undefined, + partyB: dto.partyB?.trim() || bid.legalEntity?.name || undefined, + remark: dto.remark, + kind: 'TENDER', + status: CONTRACT_STATUS.DRAFT, + createdById: user.id, + }, + }); + await tx.todoTask.updateMany({ + where: { + status: 'OPEN', + bizType: BIZ_TYPE.CONVERT_CONTRACT, + bizId: { in: [bid.id, created.id] }, + }, + data: { status: 'DONE' }, + }); + await this.log(tx, created.id, user.id, 'CREATE', undefined, CONTRACT_STATUS.DRAFT, `由 ${bid.bidNo} 转合同`); + return created; + }); + return this.get(contract.id, user); + } + + async update(id: string, user: AuthUser, dto: UpdateContractInput) { + this.assertAccess(user); + if (!canEditContract(user.roles)) throw new ForbiddenException('无权修改合同'); + const row = await this.require(id); + if (row.status !== CONTRACT_STATUS.DRAFT && row.status !== CONTRACT_STATUS.REJECTED) { + throw new BadRequestException('当前状态不能修改'); + } + const contractNo = dto.contractNo === undefined ? undefined : officialContractNo(dto.contractNo) || null; + if (contractNo) { + const clash = await this.prisma.contract.findFirst({ + where: { tenantId: '1', contractNo, id: { not: id } }, + }); + if (clash) throw new BadRequestException('该合同编号已存在'); + } + await this.prisma.contract.update({ + where: { id }, + data: { + name: dto.name?.trim() || undefined, + amount: dto.amount, + partyA: dto.partyA?.trim() || undefined, + partyB: dto.partyB?.trim() || undefined, + remark: dto.remark, + nasPath: dto.nasPath, + contractNo, + status: CONTRACT_STATUS.DRAFT, + }, + }); + await this.prisma.contractActionLog.create({ + data: { + contractId: id, + actorId: user.id, + action: 'UPDATE', + fromStatus: row.status, + toStatus: CONTRACT_STATUS.DRAFT, + }, + }); + return this.get(id, user); + } + + async replaceMilestones(id: string, user: AuthUser, items: MilestoneInput[]) { + this.assertAccess(user); + if (!canEditContract(user.roles)) throw new ForbiddenException('无权修改收款条款'); + const row = await this.require(id); + if (row.status !== CONTRACT_STATUS.DRAFT && row.status !== CONTRACT_STATUS.REJECTED) { + throw new BadRequestException('当前状态不能改收款条款'); + } + if (!items?.length) throw new BadRequestException('请至少填写一条收款条款'); + const ratios = items.map((i) => Number(i.ratio)); + if (ratios.some((n) => !Number.isFinite(n) || n <= 0)) { + throw new BadRequestException('比例必须大于 0'); + } + if (!milestoneSumOk(ratios)) { + throw new BadRequestException('收款条款比例合计必须为 100%'); + } + await this.prisma.$transaction(async (tx) => { + await tx.paymentMilestone.deleteMany({ where: { contractId: id } }); + await tx.paymentMilestone.createMany({ + data: items.map((i) => ({ + contractId: id, + name: i.name, + ratio: i.ratio, + dueAt: i.dueAt ? new Date(i.dueAt) : null, + trigger: i.trigger || null, + })), + }); + await tx.contract.update({ + where: { id }, + data: { status: CONTRACT_STATUS.DRAFT }, + }); + await this.log(tx, id, user.id, 'MILESTONES', row.status, CONTRACT_STATUS.DRAFT, `更新 ${items.length} 条收款条款`); + }); + return this.get(id, user); + } + + async createSeal(id: string, user: AuthUser, dto: SealInput) { + this.assertAccess(user); + if (!canEditContract(user.roles)) throw new ForbiddenException('无权申请用章'); + if (!dto.sealType?.trim() || !dto.reason?.trim()) { + throw new BadRequestException('请填写印章类型和事由'); + } + const row = await this.require(id); + if (row.status === CONTRACT_STATUS.APPROVED) { + throw new BadRequestException('合同已通过,用章请走证照用章模块补登记'); + } + await this.prisma.sealRequest.create({ + data: { + tenantId: '1', + sealType: dto.sealType.trim(), + reason: dto.reason.trim(), + contractId: row.id, + bidCaseId: row.bidCaseId, + applicantId: user.id, + takeOut: Boolean(dto.takeOut), + returnAt: dto.returnAt ? new Date(dto.returnAt) : null, + status: 'PENDING', + }, + }); + await this.prisma.contractActionLog.create({ + data: { + contractId: id, + actorId: user.id, + action: 'SEAL_REQUEST', + comment: `${dto.sealType}:${dto.reason}`, + }, + }); + return this.get(id, user); + } + + async submit(id: string, user: AuthUser) { + this.assertAccess(user); + if (!canEditContract(user.roles)) throw new ForbiddenException('无权提交合同审批'); + await this.prisma.$transaction(async (tx) => { + const row = await tx.contract.findFirst({ + where: { id, tenantId: '1' }, + include: { milestones: true, sealRequests: true, reviewers: true }, + }); + if (!row) throw new NotFoundException('合同不存在'); + if (row.status !== CONTRACT_STATUS.DRAFT && row.status !== CONTRACT_STATUS.REJECTED) { + throw new BadRequestException('当前状态不能提交审批'); + } + if (Number(row.amount) <= 0) throw new BadRequestException('请填写合同金额'); + if (!officialContractNo(row.contractNo)) { + throw new BadRequestException('请先填写正式合同编号,不要用系统自动号'); + } + const ratios = row.milestones.map((m) => Number(m.ratio)); + if (!ratios.length || !milestoneSumOk(ratios)) { + throw new BadRequestException('请先保存合计为 100% 的收款条款'); + } + if (!row.sealRequests.length) { + throw new BadRequestException('提交审批前请先申请用章,必须关联本合同'); + } + const approvers = await tx.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + roles: { some: { role: { code: { in: ['owner', 'biz_director'] } } } }, + }, + }); + const ids = [...new Set(approvers.map((u) => u.id))]; + if (!ids.length) throw new BadRequestException('没有可审批的企业负责人或商务总监'); + await tx.contract.update({ + where: { id }, + data: { status: CONTRACT_STATUS.PENDING, submittedAt: new Date() }, + }); + await tx.contractReviewer.deleteMany({ where: { contractId: id } }); + await tx.contractReviewer.createMany({ + data: ids.map((userId) => ({ contractId: id, userId })), + }); + await tx.approvalTask.createMany({ + data: ids.map((assigneeId) => ({ + tenantId: '1', + title: `合同审批:${row.name}`, + bizType: CONTRACT_BIZ.APPROVAL, + bizId: id, + assigneeId, + })), + }); + await tx.todoTask.updateMany({ + where: { + status: 'OPEN', + bizType: BIZ_TYPE.CONVERT_CONTRACT, + bizId: { in: [id, row.bidCaseId].filter(Boolean) as string[] }, + }, + data: { status: 'DONE' }, + }); + await this.log(tx, id, user.id, 'SUBMIT', row.status, CONTRACT_STATUS.PENDING, '提交合同审批(含用章)'); + }); + return this.get(id, user); + } + + async review(id: string, user: AuthUser, dto: ReviewInput) { + this.assertAccess(user); + if (dto.result === 'REJECTED' && !dto.comment?.trim()) { + throw new BadRequestException('驳回请填写意见'); + } + await this.prisma.$transaction(async (tx) => { + const row = await tx.contract.findFirst({ + where: { id, tenantId: '1' }, + include: { reviewers: true, bidCase: { include: { expenses: true, party: { select: { name: true } } } }, sealRequests: true }, + }); + if (!row) throw new NotFoundException('合同不存在'); + if (row.status !== CONTRACT_STATUS.PENDING) { + throw new BadRequestException('当前不是待审批状态'); + } + const mine = row.reviewers.find((r) => r.userId === user.id && r.result === 'PENDING'); + if (!mine) throw new ForbiddenException('没有待办的合同审批'); + await tx.contractReviewer.update({ + where: { id: mine.id }, + data: { + result: dto.result, + comment: dto.comment?.trim() || null, + decidedAt: new Date(), + }, + }); + await tx.approvalTask.updateMany({ + where: { + bizId: id, + assigneeId: user.id, + status: 'PENDING', + bizType: CONTRACT_BIZ.APPROVAL, + }, + data: { status: dto.result === 'APPROVED' ? 'APPROVED' : 'REJECTED' }, + }); + + if (dto.result === 'REJECTED') { + await tx.contract.update({ + where: { id }, + data: { status: CONTRACT_STATUS.DRAFT }, + }); + await tx.approvalTask.updateMany({ + where: { bizId: id, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + await tx.contractReviewer.updateMany({ + where: { contractId: id, result: 'PENDING' }, + data: { result: 'CANCELLED' }, + }); + await this.log(tx, id, user.id, 'REJECT', CONTRACT_STATUS.PENDING, CONTRACT_STATUS.DRAFT, dto.comment!.trim()); + return; + } + + await this.log(tx, id, user.id, 'APPROVE', CONTRACT_STATUS.PENDING, undefined, dto.comment?.trim() || '同意'); + const pending = await tx.contractReviewer.count({ + where: { contractId: id, result: 'PENDING' }, + }); + if (pending > 0) return; + + await this.seals.applyContractApproved(tx, id); + const presales = (row.bidCase?.expenses || []).reduce( + (s, e) => s + Number(e.amount), + 0, + ); + const existingProject = await tx.project.findFirst({ where: { contractId: row.id } }); + const contractAmount = Number(row.amount || 0); + let project = existingProject; + if (existingProject) { + const patch: Prisma.ProjectUpdateInput = { + presalesCost: presales, + partyName: row.partyA || row.bidCase?.party?.name || existingProject.partyName, + }; + if (contractAmount > 0) patch.amount = contractAmount; + if (existingProject.status === PROJECT_STATUS.NOT_STARTED) { + patch.status = PROJECT_STATUS.ACTIVE; + } + project = await tx.project.update({ where: { id: existingProject.id }, data: patch }); + const budgetCount = await tx.projectBudget.count({ where: { projectId: existingProject.id } }); + if (contractAmount > 0 && budgetCount === 0) { + const split = splitEven(contractAmount); + await tx.projectBudget.createMany({ + data: [ + { projectId: existingProject.id, category: BUDGET_CATEGORY.BUSINESS, amount: split.business }, + { projectId: existingProject.id, category: BUDGET_CATEGORY.TECH, amount: split.tech }, + ], + }); + } + } else { + const projectNo = await nextBizNo(tx, 'PRJ', (start) => + tx.project.count({ where: { projectNo: { startsWith: start } } }), + ); + project = await tx.project.create({ + data: { + tenantId: '1', + projectNo, + name: row.name, + contractId: row.id, + partyName: row.partyA || row.bidCase?.party?.name || null, + status: PROJECT_STATUS.ACTIVE, + presalesCost: presales, + }, + }); + if (contractAmount > 0) { + const split = splitEven(contractAmount); + await tx.projectBudget.createMany({ + data: [ + { projectId: project.id, category: BUDGET_CATEGORY.BUSINESS, amount: split.business }, + { projectId: project.id, category: BUDGET_CATEGORY.TECH, amount: split.tech }, + ], + }); + } + } + if (row.bidCaseId) { + await tx.expenseClaim.updateMany({ + where: { bidCaseId: row.bidCaseId }, + data: { projectId: project.id }, + }); + } + await tx.contract.update({ + where: { id }, + data: { + status: CONTRACT_STATUS.APPROVED, + approvedAt: new Date(), + signedAt: new Date(), + }, + }); + await this.log( + tx, + id, + user.id, + 'APPROVED', + CONTRACT_STATUS.PENDING, + CONTRACT_STATUS.APPROVED, + existingProject + ? `合同通过,项目 ${project.projectNo} 已在中标时立项,继承售前成本 ${presales}` + : `立项 ${project.projectNo},继承售前成本 ${presales}`, + ); + }); + return this.get(id, user); + } + + async decideApproval(approvalId: string, user: AuthUser, dto: ReviewInput) { + const task = await this.prisma.approvalTask.findFirst({ + where: { id: approvalId, tenantId: '1' }, + }); + if (!task) throw new NotFoundException('审批任务不存在'); + if (task.assigneeId !== user.id) throw new ForbiddenException('这不是你的审批'); + if (task.status !== 'PENDING') throw new BadRequestException('该审批已办理'); + if (task.bizType !== CONTRACT_BIZ.APPROVAL || !task.bizId) { + throw new BadRequestException('不是合同审批'); + } + return this.review(task.bizId, user, dto); + } + + async moveKind(id: string, user: AuthUser, kind: string) { + this.assertAccess(user); + if (!canEditContract(user.roles)) throw new ForbiddenException('无权调整合同分类'); + const normalized = String(kind || '').trim().toUpperCase(); + if (!['TENDER', 'DIRECT', 'OTHER'].includes(normalized)) { + throw new BadRequestException('合同分类只能是投标合同、直签合同或其他合同'); + } + const row = await this.require(id); + if (row.status !== CONTRACT_STATUS.DRAFT && row.status !== CONTRACT_STATUS.REJECTED) { + throw new BadRequestException('仅草稿或已驳回合同可以调整分类'); + } + const updated = await this.prisma.contract.update({ where: { id }, data: { kind: normalized } }); + return this.get(updated.id, user); + } + + private async require(id: string) { + const row = await this.prisma.contract.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('合同不存在'); + return row; + } + + private async log( + tx: Tx, + contractId: string, + actorId: string, + action: string, + fromStatus?: string, + toStatus?: string, + comment?: string, + ) { + await tx.contractActionLog.create({ + data: { contractId, actorId, action, fromStatus, toStatus, comment }, + }); + } + + private myActions( + row: { + status: string; + reviewers: { userId: string; result: string }[]; + milestones: unknown[]; + sealRequests: unknown[]; + projects: unknown[]; + }, + user: AuthUser, + ) { + const editable = + (row.status === CONTRACT_STATUS.DRAFT || row.status === CONTRACT_STATUS.REJECTED) && + canEditContract(user.roles); + return { + canEdit: editable, + canSubmit: editable, + canRequestSeal: editable, + canReview: Boolean( + row.status === CONTRACT_STATUS.PENDING && + row.reviewers.some((r) => r.userId === user.id && r.result === 'PENDING'), + ), + }; + } +} diff --git a/apps/api/src/contract/won-ledger.ts b/apps/api/src/contract/won-ledger.ts new file mode 100644 index 0000000..b47d5c5 --- /dev/null +++ b/apps/api/src/contract/won-ledger.ts @@ -0,0 +1,106 @@ +import type { Prisma } from '@prisma/client'; +import { nextBizNo } from '../system/numbering'; +import { + BUDGET_CATEGORY, + normalizeProjectType, + PROJECT_STATUS, + splitEven, +} from '../project/project-workflow'; +import { CONTRACT_STATUS } from './contract-workflow'; + +type Tx = Prisma.TransactionClient; + +export type WonBidSeed = { + id: string; + bidNo: string; + name: string; + quoteAmount?: string | null; + projectType?: string | null; + legalEntityId?: string | null; + createdById?: string | null; + party?: { name: string } | null; + legalEntity?: { name: string } | null; + expenses?: { amount: unknown }[]; +}; + +/** 中标后立刻落合同草稿 + 项目台账,便于报销挂项目。幂等。 */ +export async function ensureWonLedger(tx: Tx, bid: WonBidSeed, actorId: string) { + const amount = Number(bid.quoteAmount) || 0; + const presales = (bid.expenses || []).reduce((s, e) => s + Number(e.amount || 0), 0); + const partyA = bid.party?.name || undefined; + const partyB = bid.legalEntity?.name || undefined; + + let contract = await tx.contract.findFirst({ + where: { + bidCaseId: bid.id, + status: { in: [CONTRACT_STATUS.DRAFT, CONTRACT_STATUS.PENDING, CONTRACT_STATUS.APPROVED] }, + }, + }); + if (!contract) { + contract = await tx.contract.create({ + data: { + tenantId: '1', + name: bid.name, + kind: 'TENDER', + bidCaseId: bid.id, + legalEntityId: bid.legalEntityId || undefined, + amount, + partyA, + partyB, + remark: `由中标事项 ${bid.bidNo} 自动生成草稿`, + status: CONTRACT_STATUS.DRAFT, + createdById: bid.createdById || actorId, + }, + }); + await tx.contractActionLog.create({ + data: { + contractId: contract.id, + actorId, + action: 'CREATE', + toStatus: CONTRACT_STATUS.DRAFT, + comment: `中标自动生成合同草稿 ${bid.bidNo}`, + }, + }); + } + + let project = await tx.project.findFirst({ where: { contractId: contract.id } }); + if (!project) { + const projectNo = await nextBizNo(tx, 'PRJ', (start) => + tx.project.count({ where: { projectNo: { startsWith: start } } }), + ); + project = await tx.project.create({ + data: { + tenantId: '1', + projectNo, + name: bid.name, + contractId: contract.id, + partyName: partyA || null, + projectType: normalizeProjectType(bid.projectType) || '', + amount, + status: PROJECT_STATUS.NOT_STARTED, + presalesCost: presales, + }, + }); + if (amount > 0) { + const split = splitEven(amount); + await tx.projectBudget.createMany({ + data: [ + { projectId: project.id, category: BUDGET_CATEGORY.BUSINESS, amount: split.business }, + { projectId: project.id, category: BUDGET_CATEGORY.TECH, amount: split.tech }, + ], + }); + } + } else if (presales > Number(project.presalesCost || 0)) { + project = await tx.project.update({ + where: { id: project.id }, + data: { presalesCost: presales }, + }); + } + + await tx.expenseClaim.updateMany({ + where: { bidCaseId: bid.id, projectId: null }, + data: { projectId: project.id }, + }); + + return { contract, project }; +} diff --git a/apps/api/src/files/files.controller.ts b/apps/api/src/files/files.controller.ts new file mode 100644 index 0000000..83a1fc3 --- /dev/null +++ b/apps/api/src/files/files.controller.ts @@ -0,0 +1,58 @@ +import { + Body, + Controller, + Delete, + Get, + Param, + ParseUUIDPipe, + Post, + StreamableFile, + UploadedFile, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { ApiBearerAuth, ApiConsumes, ApiTags } from '@nestjs/swagger'; +import { join } from 'path'; +import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator'; +import { decodeUploadFileName, FilesService } from './files.service'; + +@ApiTags('files') +@ApiBearerAuth() +@Controller('files') +export class FilesController { + constructor(private readonly files: FilesService) { + this.files.root(); + } + + @Post() + @ApiConsumes('multipart/form-data') + @UseInterceptors( + FileInterceptor('file', { + dest: join(process.cwd(), 'uploads', 'tmp'), + limits: { fileSize: 20 * 1024 * 1024 }, + }), + ) + upload( + @CurrentUser() user: AuthUser, + @UploadedFile() file: { originalname: string; mimetype: string; size: number; buffer?: Buffer; path?: string }, + @Body() body: { bizType?: string; bizId?: string }, + ) { + return this.files.create(user, { bizType: body.bizType || '', bizId: body.bizId || '' }, file); + } + + @Get(':id') + async download(@CurrentUser() user: AuthUser, @Param('id', ParseUUIDPipe) id: string) { + const { row, stream } = await this.files.download(id, user); + const fileName = decodeUploadFileName(row.fileName); + return new StreamableFile(stream, { + type: row.mimeType || 'application/octet-stream', + disposition: `inline; filename*=UTF-8''${encodeURIComponent(fileName)}`, + length: row.size || undefined, + }); + } + + @Delete(':id') + remove(@CurrentUser() user: AuthUser, @Param('id', ParseUUIDPipe) id: string) { + return this.files.remove(id, user); + } +} diff --git a/apps/api/src/files/files.mime.spec.ts b/apps/api/src/files/files.mime.spec.ts new file mode 100644 index 0000000..59a5786 --- /dev/null +++ b/apps/api/src/files/files.mime.spec.ts @@ -0,0 +1,19 @@ +import { isAllowedChatImage, mimeFromName, sniffUploadMime } from './files.mime'; + +describe('files.mime', () => { + it('sniffs jpeg/png/gif even when declared as octet-stream', () => { + expect(sniffUploadMime(Buffer.from([0xff, 0xd8, 0xff, 0xe0]), 'application/octet-stream', 'a.bin')).toBe( + 'image/jpeg', + ); + expect(sniffUploadMime(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), 'application/octet-stream')).toBe( + 'image/png', + ); + expect(sniffUploadMime(Buffer.from('GIF89a'), '', 'x.gif')).toBe('image/gif'); + }); + + it('maps image/jpg and filename screenshot.jpg', () => { + expect(sniffUploadMime(Buffer.from('xxxx'), 'image/jpg', 'shot.jpg')).toBe('image/jpeg'); + expect(mimeFromName('screenshot_com.tencent.wechat.jpg')).toBe('image/jpeg'); + expect(isAllowedChatImage('image/jpg')).toBe(true); + }); +}); diff --git a/apps/api/src/files/files.mime.ts b/apps/api/src/files/files.mime.ts new file mode 100644 index 0000000..44271f7 --- /dev/null +++ b/apps/api/src/files/files.mime.ts @@ -0,0 +1,77 @@ +const IMAGE_OK = new Set([ + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/gif', + 'image/bmp', + 'image/heic', + 'image/heif', +]); + +const ALIAS: Record = { + 'image/jpg': 'image/jpeg', + 'image/pjpeg': 'image/jpeg', + 'image/x-png': 'image/png', + 'image/x-icon': 'image/png', +}; + +export function normalizeMime(raw?: string | null) { + const m = (raw || '').split(';')[0].trim().toLowerCase(); + if (!m) return ''; + return ALIAS[m] || m; +} + +export function mimeFromName(name?: string | null) { + const n = (name || '').toLowerCase(); + if (/\.jpe?g$/.test(n)) return 'image/jpeg'; + if (n.endsWith('.png')) return 'image/png'; + if (n.endsWith('.gif')) return 'image/gif'; + if (n.endsWith('.webp')) return 'image/webp'; + if (n.endsWith('.bmp')) return 'image/bmp'; + if (n.endsWith('.heic') || n.endsWith('.heif')) return 'image/heic'; + if (n.endsWith('.pdf')) return 'application/pdf'; + if (n.endsWith('.doc')) return 'application/msword'; + if (n.endsWith('.docx')) return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; + if (n.endsWith('.xls')) return 'application/vnd.ms-excel'; + if (n.endsWith('.xlsx')) return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + if (n.endsWith('.m4a') || n.endsWith('.mp4')) return 'audio/mp4'; + if (n.endsWith('.aac')) return 'audio/aac'; + if (n.endsWith('.mp3')) return 'audio/mpeg'; + if (n.endsWith('.amr')) return 'audio/amr'; + if (n.endsWith('.ogg') || n.endsWith('.opus')) return 'audio/ogg'; + return ''; +} + +export function sniffUploadMime(buf: Buffer, declared?: string | null, name?: string | null) { + if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return 'image/jpeg'; + if (buf.length >= 8 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return 'image/png'; + if (buf.length >= 6 && buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) return 'image/gif'; + if ( + buf.length >= 12 && + buf[0] === 0x52 && + buf[1] === 0x49 && + buf[2] === 0x46 && + buf[3] === 0x46 && + buf[8] === 0x57 && + buf[9] === 0x45 && + buf[10] === 0x42 && + buf[11] === 0x50 + ) { + return 'image/webp'; + } + if (buf.length >= 4 && buf[0] === 0x25 && buf[1] === 0x50 && buf[2] === 0x44 && buf[3] === 0x46) return 'application/pdf'; + if (buf.length >= 8 && buf[4] === 0x66 && buf[5] === 0x74 && buf[6] === 0x79 && buf[7] === 0x70) { + const d = normalizeMime(declared); + if (d.startsWith('video/')) return d; + return 'audio/mp4'; + } + const fromName = mimeFromName(name); + if (fromName) return fromName; + const d = normalizeMime(declared); + if (d && d !== 'application/octet-stream') return d; + return d || 'application/octet-stream'; +} + +export function isAllowedChatImage(mime: string) { + return IMAGE_OK.has(normalizeMime(mime)); +} diff --git a/apps/api/src/files/files.module.ts b/apps/api/src/files/files.module.ts new file mode 100644 index 0000000..7adb8e3 --- /dev/null +++ b/apps/api/src/files/files.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { FilesController } from './files.controller'; +import { FilesService } from './files.service'; + +@Module({ + controllers: [FilesController], + providers: [FilesService], + exports: [FilesService], +}) +export class FilesModule {} diff --git a/apps/api/src/files/files.service.ts b/apps/api/src/files/files.service.ts new file mode 100644 index 0000000..31e4494 --- /dev/null +++ b/apps/api/src/files/files.service.ts @@ -0,0 +1,433 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { createReadStream, existsSync, mkdirSync, openSync, readFileSync, readSync, closeSync, unlinkSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { randomUUID } from 'crypto'; +import { PrismaService } from '../prisma/prisma.service'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { ASSIGN_ROLE, BID_STATUS, canAccessBidModule } from '../bidding/bid-workflow'; +import { canAccessContract } from '../contract/contract-workflow'; +import { canAccessProject } from '../project/project-workflow'; +import { canApproveFinance, canFileFinance } from '../finance/finance-workflow'; +import { canManageHr } from '../hr/hr-workflow'; +import { canManageNotice, canSeeNotice } from '../office/office-apply'; +import { isAllowedChatImage, sniffUploadMime } from './files.mime'; + +export const FILE_BIZ = { + EXPENSE: 'EXPENSE', + EXPENSE_LINE: 'EXPENSE_LINE', + LOAN: 'LOAN', + BID_ANNOUNCE: 'BID_ANNOUNCE', + BID_FILE: 'BID_FILE', + BID_FORMAL: 'BID_FORMAL', + BID_PROOF: 'BID_PROOF', + CONTRACT: 'CONTRACT', + CONTRACT_ACCEPT: 'CONTRACT_ACCEPT', + CONTRACT_AWARD: 'CONTRACT_AWARD', + PROJECT: 'PROJECT', + PROJECT_ACCEPT: 'PROJECT_ACCEPT', + PURCHASE: 'PURCHASE', + PURCHASE_ACCEPT: 'PURCHASE_ACCEPT', + USER_AVATAR: 'USER_AVATAR', + IM_IMAGE: 'IM_IMAGE', + IM_VOICE: 'IM_VOICE', + IM_STICKER: 'IM_STICKER', + IM_FILE: 'IM_FILE', + USER_ID_PHOTO: 'USER_ID_PHOTO', + EMPLOYEE_ID_PHOTO: 'EMPLOYEE_ID_PHOTO', + EMPLOYEE_ID_FRONT: 'EMPLOYEE_ID_FRONT', + EMPLOYEE_ID_BACK: 'EMPLOYEE_ID_BACK', + EMPLOYEE_CERT: 'EMPLOYEE_CERT', + EMPLOYEE_LABOR: 'EMPLOYEE_LABOR', + NOTICE: 'NOTICE', +} as const; + +const ATTACHABLE = new Set([ + FILE_BIZ.EXPENSE, + FILE_BIZ.EXPENSE_LINE, + FILE_BIZ.LOAN, + FILE_BIZ.BID_ANNOUNCE, + FILE_BIZ.BID_FILE, + FILE_BIZ.BID_FORMAL, + FILE_BIZ.BID_PROOF, + FILE_BIZ.CONTRACT, + FILE_BIZ.CONTRACT_ACCEPT, + FILE_BIZ.CONTRACT_AWARD, + FILE_BIZ.PROJECT, + FILE_BIZ.PROJECT_ACCEPT, + FILE_BIZ.PURCHASE, + FILE_BIZ.PURCHASE_ACCEPT, + FILE_BIZ.USER_AVATAR, + FILE_BIZ.IM_IMAGE, + FILE_BIZ.IM_VOICE, + FILE_BIZ.IM_STICKER, + FILE_BIZ.IM_FILE, + FILE_BIZ.USER_ID_PHOTO, + FILE_BIZ.EMPLOYEE_ID_PHOTO, + FILE_BIZ.EMPLOYEE_ID_FRONT, + FILE_BIZ.EMPLOYEE_ID_BACK, + FILE_BIZ.EMPLOYEE_CERT, + FILE_BIZ.EMPLOYEE_LABOR, + FILE_BIZ.NOTICE, +]); + +const ALLOWED = new Set([ + 'image/jpeg', + 'image/png', + 'image/webp', + 'image/gif', + 'audio/mp4', + 'audio/aac', + 'audio/mpeg', + 'audio/amr', + 'audio/3gpp', + 'audio/ogg', + 'video/mp4', + 'application/pdf', + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/vnd.ms-excel', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', +]); + +const VOICE_OK = new Set(['audio/mp4', 'audio/aac', 'audio/mpeg', 'audio/amr', 'audio/3gpp', 'audio/ogg', 'video/mp4', 'application/octet-stream']); +const MAX = 8 * 1024 * 1024; + +function sniffStoredMime(full: string, declared?: string | null, name?: string | null) { + const buf = Buffer.alloc(16); + try { + const fd = openSync(full, 'r'); + readSync(fd, buf, 0, 16, 0); + closeSync(fd); + } catch { + return declared || 'application/octet-stream'; + } + return sniffUploadMime(buf, declared, name); +} + +/** multer 把 Content-Disposition 文件名按 latin1 解,中文会变成乱码。 */ +export function decodeUploadFileName(name?: string) { + if (!name) return '附件'; + if (/[\u4e00-\u9fff]/.test(name)) return name; + try { + const decoded = Buffer.from(name, 'latin1').toString('utf8'); + if (/[\u4e00-\u9fff]/.test(decoded) && !decoded.includes('\uFFFD')) return decoded; + } catch { + /* keep original */ + } + return name; +} + +@Injectable() +export class FilesService { + constructor(private readonly prisma: PrismaService) {} + + root() { + const dir = join(process.cwd(), 'uploads'); + mkdirSync(dir, { recursive: true }); + mkdirSync(join(dir, 'tmp'), { recursive: true }); + return dir; + } + + async list(bizType: string, bizId: string) { + const rows = await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizType, bizId }, + orderBy: { createdAt: 'asc' }, + }); + return rows.map((r) => ({ ...r, fileName: decodeUploadFileName(r.fileName) })); + } + + async create( + user: AuthUser, + dto: { bizType: string; bizId: string }, + file?: { originalname: string; mimetype: string; size: number; buffer?: Buffer; path?: string }, + ) { + if (!file) throw new BadRequestException('请选择文件'); + const buffer = file.buffer?.length + ? file.buffer + : file.path && existsSync(file.path) + ? readFileSync(file.path) + : undefined; + if (!buffer?.length) throw new BadRequestException('请选择文件'); + const bizType = (dto.bizType || '').toUpperCase(); + if (file.size > (bizType === FILE_BIZ.IM_FILE ? 20 * 1024 * 1024 : MAX)) { + throw new BadRequestException(bizType === FILE_BIZ.IM_FILE ? '文件不能超过 20MB' : '文件不能超过 8MB'); + } + const mime = sniffUploadMime(buffer, file.mimetype, file.originalname); + const voice = bizType === FILE_BIZ.IM_VOICE; + const chatFile = bizType === FILE_BIZ.IM_FILE; + const chatImage = bizType === FILE_BIZ.IM_IMAGE || bizType === FILE_BIZ.IM_STICKER; + if (voice) { + if (mime && !VOICE_OK.has(mime) && !mime.startsWith('audio/')) { + throw new BadRequestException('语音只接受音频文件'); + } + } else if (chatFile) { + /* 聊天文件放宽类型 */ + } else if (chatImage) { + if (!isAllowedChatImage(mime)) { + throw new BadRequestException('只接受图片'); + } + } else if (mime && !ALLOWED.has(mime) && !isAllowedChatImage(mime)) { + throw new BadRequestException('只接受图片、语音、PDF 或 Office 文件'); + } + if (!ATTACHABLE.has(bizType)) { + throw new BadRequestException('该类型不能上传附件'); + } + await this.assertCanAttach(user, bizType, dto.bizId); + const fileName = decodeUploadFileName(file.originalname); + const id = randomUUID(); + const ext = (fileName.split('.').pop() || 'bin').replace(/[^a-zA-Z0-9]/g, '').slice(0, 8); + const storageKey = `${bizType}/${dto.bizId}/${id}.${ext || 'bin'}`; + const full = join(this.root(), storageKey); + mkdirSync(join(full, '..'), { recursive: true }); + writeFileSync(full, buffer); + if (file.path && existsSync(file.path)) unlinkSync(file.path); + return this.prisma.fileAsset.create({ + data: { + id, + tenantId: '1', + bizType, + bizId: dto.bizId, + fileName, + mimeType: mime || file.mimetype || 'application/octet-stream', + size: file.size, + storageKey, + uploadedById: user.id, + }, + }); + } + + async download(id: string, user: AuthUser) { + const row = await this.prisma.fileAsset.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('附件不存在'); + await this.assertCanSee(user, row.bizType, row.bizId); + const full = join(this.root(), row.storageKey); + if (!existsSync(full)) throw new NotFoundException('文件已丢失'); + const mimeType = sniffStoredMime(full, row.mimeType, row.fileName); + return { row: { ...row, mimeType }, stream: createReadStream(full) }; + } + + async remove(id: string, user: AuthUser) { + const row = await this.prisma.fileAsset.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('附件不存在'); + await this.assertCanAttach(user, row.bizType, row.bizId); + const full = join(this.root(), row.storageKey); + if (existsSync(full)) unlinkSync(full); + await this.prisma.fileAsset.delete({ where: { id } }); + return { ok: true }; + } + + private async assertCanSee(user: AuthUser, bizType: string, bizId: string) { + if (canApproveFinance(user.roles) || user.roles.includes('admin') || user.roles.includes('owner')) return; + if (bizType === FILE_BIZ.EXPENSE || bizType === FILE_BIZ.EXPENSE_LINE) { + const row = + bizType === FILE_BIZ.EXPENSE + ? await this.prisma.expenseClaim.findFirst({ where: { id: bizId, tenantId: '1' } }) + : ( + await this.prisma.expenseLine.findFirst({ + where: { id: bizId }, + include: { claim: true }, + }) + )?.claim; + if (row && row.applicantId === user.id) return; + } + if (bizType === FILE_BIZ.LOAN) { + const row = await this.prisma.loanRecord.findFirst({ where: { id: bizId, tenantId: '1' } }); + if (row && row.applicantId === user.id) return; + } + if ( + bizType === FILE_BIZ.USER_AVATAR || + bizType === FILE_BIZ.IM_IMAGE || + bizType === FILE_BIZ.IM_VOICE || + bizType === FILE_BIZ.IM_STICKER || + bizType === FILE_BIZ.IM_FILE + ) + return; + if (bizType.startsWith('USER') && (bizId === user.id || user.roles.includes('admin') || user.roles.includes('hr'))) + return; + if ( + (bizType === FILE_BIZ.EMPLOYEE_ID_PHOTO || + bizType === FILE_BIZ.EMPLOYEE_ID_FRONT || + bizType === FILE_BIZ.EMPLOYEE_ID_BACK || + bizType === FILE_BIZ.EMPLOYEE_CERT) && + (user.roles.includes('admin') || user.roles.includes('hr') || user.employeeId === bizId) + ) + return; + if (bizType === FILE_BIZ.EMPLOYEE_LABOR) { + if (canManageHr(user.roles, user.permissions) || canAccessContract(user.roles, user.permissions)) return; + const row = await this.prisma.laborContract.findFirst({ + where: { id: bizId, tenantId: '1' }, + include: { employee: { select: { id: true } } }, + }); + if (row && user.employeeId && row.employeeId === user.employeeId) return; + } + if (bizType === FILE_BIZ.BID_FILE) { + const bid = await this.prisma.bidCase.findFirst({ + where: { id: bizId, tenantId: '1' }, + include: { assignees: true }, + }); + if ( + bid && + (canAccessBidModule(user.roles, user.permissions) || + bid.assignees.some((a) => a.role === ASSIGN_ROLE.FETCHER && a.userId === user.id)) + ) { + return; + } + } + if (bizType.startsWith('BID') && canAccessBidModule(user.roles, user.permissions)) return; + if (bizType.startsWith('CONTRACT') && canAccessContract(user.roles, user.permissions)) return; + if ((bizType.startsWith('PROJECT') || bizType.startsWith('PURCHASE')) && canAccessProject(user.roles, user.permissions)) + return; + if (bizType === FILE_BIZ.NOTICE) { + const row = await this.prisma.notice.findFirst({ where: { id: bizId, tenantId: '1' } }); + if (row && (await canSeeNotice(this.prisma, user, row))) return; + } + throw new ForbiddenException('无权查看该附件'); + } + + private async assertCanAttach(user: AuthUser, bizType: string, bizId: string) { + if (bizType.startsWith('USER')) { + if (bizId !== user.id && !user.roles.includes('admin') && !user.roles.includes('hr')) { + throw new ForbiddenException('只能上传自己的头像或证件照'); + } + const row = await this.prisma.user.findFirst({ where: { id: bizId, tenantId: '1' } }); + if (!row) throw new NotFoundException('账号不存在'); + await this.prisma.fileAsset.deleteMany({ + where: { tenantId: '1', bizType, bizId }, + }); + return; + } + if ( + bizType === FILE_BIZ.EMPLOYEE_ID_PHOTO || + bizType === FILE_BIZ.EMPLOYEE_ID_FRONT || + bizType === FILE_BIZ.EMPLOYEE_ID_BACK || + bizType === FILE_BIZ.EMPLOYEE_CERT + ) { + const self = user.employeeId === bizId; + if (!self && !user.roles.includes('admin') && !user.roles.includes('hr')) { + throw new ForbiddenException('只有本人或人事可以补档案附件'); + } + const row = await this.prisma.employee.findFirst({ where: { id: bizId, tenantId: '1' } }); + if (!row) throw new NotFoundException('员工不存在'); + if ( + bizType === FILE_BIZ.EMPLOYEE_ID_PHOTO || + bizType === FILE_BIZ.EMPLOYEE_ID_FRONT || + bizType === FILE_BIZ.EMPLOYEE_ID_BACK + ) { + await this.prisma.fileAsset.deleteMany({ + where: { tenantId: '1', bizType, bizId }, + }); + } + return; + } + if (bizType === FILE_BIZ.EMPLOYEE_LABOR) { + if (!canManageHr(user.roles, user.permissions) && !canAccessContract(user.roles, user.permissions)) { + throw new ForbiddenException('无权上传劳动合同'); + } + const row = await this.prisma.laborContract.findFirst({ where: { id: bizId, tenantId: '1' } }); + if (!row) throw new NotFoundException('人事合同不存在'); + return; + } + if (bizType.startsWith('BID')) { + const bid = await this.prisma.bidCase.findFirst({ + where: { id: bizId, tenantId: '1' }, + include: { assignees: true }, + }); + if (!bid) throw new NotFoundException('投标事项不存在'); + if (bizType === FILE_BIZ.BID_ANNOUNCE) { + if (bid.createdById !== user.id && !user.roles.includes('admin')) { + throw new ForbiddenException('只有该投标的发起人可以上传招标公告附件'); + } + return; + } + if (bizType === FILE_BIZ.BID_FILE) { + const assignedFetcher = bid.assignees.some( + (a) => a.role === ASSIGN_ROLE.FETCHER && a.userId === user.id, + ); + const canHandleFetch = + bid.status === BID_STATUS.FETCH && + (assignedFetcher || user.roles.includes('admin')); + if (!canHandleFetch) { + throw new ForbiddenException('只有当前指定的取标办理人可以上传或删除取标附件'); + } + return; + } + if (!canAccessBidModule(user.roles, user.permissions)) { + throw new ForbiddenException('无权上传投标附件'); + } + const assigned = bid.assignees.some((a) => a.userId === user.id); + if (!assigned && bid.createdById !== user.id && !user.roles.includes('admin')) { + throw new ForbiddenException('只有该投标的指定办理人可以上传附件'); + } + return; + } + if (bizType === FILE_BIZ.PROJECT_ACCEPT) { + if (!canAccessProject(user.roles, user.permissions)) { + throw new ForbiddenException('无权上传验收材料'); + } + const row = await this.prisma.projectAcceptMaterial.findFirst({ + where: { id: bizId }, + include: { project: { select: { status: true } } }, + }); + if (!row) throw new NotFoundException('验收材料不存在'); + if (row.project.status === 'ACCEPTED' || row.project.status === 'DONE') { + throw new BadRequestException('项目已验收,不能再改材料'); + } + await this.prisma.fileAsset.deleteMany({ + where: { tenantId: '1', bizType, bizId }, + }); + return; + } + if (bizType.startsWith('PROJECT') || bizType.startsWith('PURCHASE') || bizType.startsWith('CONTRACT')) { + if (!canAccessProject(user.roles, user.permissions) && !canAccessContract(user.roles, user.permissions)) { + throw new ForbiddenException('无权上传该附件'); + } + return; + } + if (bizType === FILE_BIZ.IM_IMAGE || bizType === FILE_BIZ.IM_VOICE || bizType === FILE_BIZ.IM_STICKER || bizType === FILE_BIZ.IM_FILE) { + return; + } + if (bizType === FILE_BIZ.NOTICE) { + if (!canManageNotice(user.roles, user.permissions)) { + throw new ForbiddenException('无权给通知上传附件'); + } + const row = await this.prisma.notice.findFirst({ where: { id: bizId, tenantId: '1' } }); + if (!row) throw new NotFoundException('通知不存在'); + return; + } + if (!canFileFinance(user.roles, user.permissions) && !canApproveFinance(user.roles)) { + throw new ForbiddenException('无权上传附件'); + } + if (bizType === FILE_BIZ.EXPENSE || bizType === FILE_BIZ.EXPENSE_LINE) { + const row = + bizType === FILE_BIZ.EXPENSE + ? await this.prisma.expenseClaim.findFirst({ where: { id: bizId, tenantId: '1' } }) + : ( + await this.prisma.expenseLine.findFirst({ + where: { id: bizId }, + include: { claim: true }, + }) + )?.claim; + if (!row) throw new NotFoundException('报销单不存在'); + if (row.applicantId !== user.id && !canApproveFinance(user.roles)) { + throw new ForbiddenException('只能给自己的报销上传发票'); + } + if (row.status !== 'DRAFT' && row.status !== 'REJECTED' && row.status !== 'PENDING' && !canApproveFinance(user.roles)) { + throw new BadRequestException('当前状态不能改附件'); + } + return; + } + const row = await this.prisma.loanRecord.findFirst({ where: { id: bizId, tenantId: '1' } }); + if (!row) throw new NotFoundException('借款单不存在'); + if (row.applicantId !== user.id && !canApproveFinance(user.roles)) { + throw new ForbiddenException('只能给自己的借款上传附件'); + } + if (row.status !== 'DRAFT' && row.status !== 'REJECTED' && !canApproveFinance(user.roles)) { + throw new BadRequestException('当前状态不能改附件'); + } + } +} diff --git a/apps/api/src/finance/bank-pay.ts b/apps/api/src/finance/bank-pay.ts new file mode 100644 index 0000000..e5f8fb0 --- /dev/null +++ b/apps/api/src/finance/bank-pay.ts @@ -0,0 +1,49 @@ +import { BadRequestException } from '@nestjs/common'; + +/** 打款通道。BANK_API 是银企直联预留口,未对接具体银行。 */ +export const BANK_PAY_CHANNELS = [ + { code: 'MANUAL', name: '线下网银 / 柜面', enabled: true, hint: '财务在网银打完后点「已打款」。' }, + { code: 'BANK_API', name: '银企直联', enabled: false, hint: '接口已预留,尚未对接银行主机。' }, +] as const; + +export type BankPayInstruction = { + paymentId: string; + payNo: string; + amount: number; + purpose: string; + payeeName: string; + bankName: string; + bankAccount: string; +}; + +export type BankPayResult = { + ok: boolean; + channel: string; + bankTxnNo?: string; + message: string; +}; + +export function maskBankAccount(acc?: string | null) { + const s = String(acc || '').replace(/\s/g, ''); + if (!s) return null; + if (s.length <= 4) return s; + return `${'*'.repeat(Math.max(4, s.length - 4))}${s.slice(-4)}`; +} + +export function assertBankInstruction(ins: BankPayInstruction) { + if (!ins.payeeName?.trim()) throw new BadRequestException('缺少收款人'); + if (!ins.bankAccount?.trim()) throw new BadRequestException('收款人没有银行账号,无法打款'); + if (!(ins.amount > 0)) throw new BadRequestException('打款金额无效'); +} + +/** 银企直联入口。开通后在此对接银行,不要把账号写进业务页面。 */ +export async function dispatchBankPay(channel: string, ins: BankPayInstruction): Promise { + assertBankInstruction(ins); + if (channel === 'MANUAL') { + return { ok: true, channel: 'MANUAL', message: '请财务在网银打完后点已打款' }; + } + if (channel === 'BANK_API') { + throw new BadRequestException('银企直联尚未开通,当前请线下打到收款人账户后点「已打款」'); + } + throw new BadRequestException('未知打款通道'); +} diff --git a/apps/api/src/finance/finance-workflow.spec.ts b/apps/api/src/finance/finance-workflow.spec.ts new file mode 100644 index 0000000..e862784 --- /dev/null +++ b/apps/api/src/finance/finance-workflow.spec.ts @@ -0,0 +1,39 @@ +import { + assertCostObject, + bondBucket, + canAccessFinanceDesk, + canApproveFinance, + canFileFinance, + FILE_COST_TYPES, + loanRemaining, +} from './finance-workflow'; + +describe('finance-workflow', () => { + it('requires a cost object of bid, project, dept or admin', () => { + expect(assertCostObject('BID')).toMatch(/投标/); + expect(assertCostObject('BID', 'bid-1')).toBeNull(); + expect(assertCostObject('PROJECT', undefined, 'p1')).toBeNull(); + expect(assertCostObject('DEPT')).toBeNull(); + expect(assertCostObject('ADMIN')).toBeNull(); + expect(assertCostObject('MARKETING')).toMatch(/必须是/); + expect(assertCostObject('BID', 'bid-1', 'p1')).toMatch(/只挂投标/); + }); + + it('lets office users file and finance approve', () => { + expect(canFileFinance(['employee'], ['office:todos'])).toBe(true); + expect(canApproveFinance(['finance'])).toBe(true); + expect(canApproveFinance(['biz_director'])).toBe(true); + expect(canApproveFinance(['biz_staff'])).toBe(false); + expect(canAccessFinanceDesk(['finance'], ['finance:expenses'])).toBe(true); + expect(canAccessFinanceDesk(['employee'], ['office'])).toBe(false); + }); + + it('computes loan remaining and bond buckets', () => { + expect(loanRemaining(1000, 400)).toBe(600); + expect(FILE_COST_TYPES).toContain('BID'); + expect(bondBucket({ status: 'UNPAID' })).toBe('unpaid'); + expect(bondBucket({ status: 'PAID', dueBackAt: new Date(Date.now() - 86400000) })).toBe('overdue'); + expect(bondBucket({ status: 'PAID', dueBackAt: new Date(Date.now() + 86400000) })).toBe('due'); + expect(bondBucket({ status: 'RETURNED' })).toBe('returned'); + }); +}); diff --git a/apps/api/src/finance/finance-workflow.ts b/apps/api/src/finance/finance-workflow.ts new file mode 100644 index 0000000..ccb89e4 --- /dev/null +++ b/apps/api/src/finance/finance-workflow.ts @@ -0,0 +1,94 @@ +export const COST_TYPE = { + BID: 'BID', + PROJECT: 'PROJECT', + DEPT: 'DEPT', + ADMIN: 'ADMIN', + MARKETING: 'MARKETING', +} as const; + +export const FILE_COST_TYPES = [COST_TYPE.BID, COST_TYPE.PROJECT, COST_TYPE.DEPT, COST_TYPE.ADMIN] as const; + +export const EXPENSE_STATUS = { + DRAFT: 'DRAFT', + PENDING: 'PENDING', + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', +} as const; + +export const LOAN_STATUS = { + DRAFT: 'DRAFT', + PENDING: 'PENDING', + OPEN: 'OPEN', + OFFSET: 'OFFSET', + REJECTED: 'REJECTED', +} as const; + +export const BOND_TYPE = { + TENDER: 'TENDER', + PERFORMANCE: 'PERFORMANCE', + WARRANTY: 'WARRANTY', +} as const; + +export const BOND_STATUS = { + UNPAID: 'UNPAID', + PAID: 'PAID', + RETURNED: 'RETURNED', +} as const; + +export const INVOICE_STATUS = { + TO_ISSUE: 'TO_ISSUE', + ISSUED: 'ISSUED', + RECEIVED: 'RECEIVED', + OVERDUE: 'OVERDUE', +} as const; + +export const FINANCE_BIZ = { + EXPENSE: 'EXPENSE_APPROVAL', + LOAN: 'LOAN_APPROVAL', + OFFSET: 'LOAN_OFFSET', +} as const; + +export function canFileFinance(roles: string[], permissions: string[]) { + if (roles.includes('admin')) return true; + return permissions.some((p) => p === 'office' || p.startsWith('office:')); +} + +export function canApproveFinance(roles: string[]) { + return ['admin', 'owner', 'finance', 'biz_director'].some((r) => roles.includes(r)); +} + +export function canAccessFinanceDesk(roles: string[], permissions: string[]) { + if (roles.includes('admin')) return true; + return permissions.some((p) => p === 'finance' || p.startsWith('finance:')); +} + +/** 上一级:财务确认岗。草稿不到这里;未通过不到再上一级(台账/报表/付款)。 */ +export function canSeeSubmittedFinance(roles: string[], permissions: string[]) { + return canApproveFinance(roles) || canAccessFinanceDesk(roles, permissions); +} + +export function assertCostObject(costType: string, bidCaseId?: string, projectId?: string) { + if (!(FILE_COST_TYPES as readonly string[]).includes(costType)) { + return '报销归属必须是投标、项目、部门或行政之一'; + } + if (costType === COST_TYPE.BID && !bidCaseId) return '挂投标费用请选择投标事项'; + if (costType === COST_TYPE.PROJECT && !projectId) return '挂项目费用请选择正式项目'; + if (costType === COST_TYPE.BID && projectId) return '中标前费用请只挂投标编号'; + if (costType === COST_TYPE.PROJECT && bidCaseId) return '正式项目费用请只挂项目,不要同时挂投标'; + if ((costType === COST_TYPE.DEPT || costType === COST_TYPE.ADMIN) && (bidCaseId || projectId)) { + return '部门或行政费用不要挂投标或项目'; + } + return null; +} + +export function loanRemaining(amount: number, offsetAmount: number) { + return Math.round((amount - offsetAmount) * 100) / 100; +} + +export function bondBucket(row: { status: string; dueBackAt?: Date | string | null; returnedAt?: Date | string | null }) { + if (row.status === BOND_STATUS.UNPAID) return 'unpaid'; + if (row.status === BOND_STATUS.RETURNED || row.returnedAt) return 'returned'; + if (row.dueBackAt && new Date(row.dueBackAt).getTime() < Date.now()) return 'overdue'; + if (row.dueBackAt) return 'due'; + return 'paid'; +} diff --git a/apps/api/src/finance/finance.controller.ts b/apps/api/src/finance/finance.controller.ts new file mode 100644 index 0000000..b33eba1 --- /dev/null +++ b/apps/api/src/finance/finance.controller.ts @@ -0,0 +1,308 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator'; +import { FinanceService } from './finance.service'; + +@ApiTags('finance') +@ApiBearerAuth() +@Controller() +export class FinanceController { + constructor(private readonly finance: FinanceService) {} + + @Get('cost-targets') + costTargets(@CurrentUser() user: AuthUser) { + return this.finance.costTargets(user); + } + + @Get('asset-purchases') + assetPurchases(@CurrentUser() user: AuthUser) { + return this.finance.assetPurchaseLedger(user); + } + + @Get('expenses') + expenses( + @CurrentUser() user: AuthUser, + @Query('page') page = '1', + @Query('pageSize') pageSize = '10', + @Query('bucket') bucket?: string, + @Query('keyword') keyword?: string, + ) { + return this.finance.listExpenses(user, Number(page) || 1, Number(pageSize) || 10, bucket, keyword); + } + + @Get('expenses/:id') + expense(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.finance.getExpense(id, user); + } + + @Post('expenses') + createExpense( + @CurrentUser() user: AuthUser, + @Body() + body: { + costType: string; + amount: number; + bidCaseId?: string; + projectId?: string; + loanId?: string; + remark?: string; + budgetCategory?: string; + expenseCategory?: string; + purpose?: string; + prepaid?: boolean; + payerId?: string; + lines?: { + id?: string; + kind: string; + amount: number; + invoiceNo?: string; + occurAt?: string; + remark?: string; + }[]; + }, + ) { + return this.finance.createExpense(user, body); + } + + @Patch('expenses/:id') + updateExpense( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() + body: { + costType: string; + amount: number; + bidCaseId?: string; + projectId?: string; + loanId?: string; + remark?: string; + budgetCategory?: string; + expenseCategory?: string; + purpose?: string; + prepaid?: boolean; + payerId?: string; + lines?: { + id?: string; + kind: string; + amount: number; + invoiceNo?: string; + occurAt?: string; + remark?: string; + }[]; + }, + ) { + return this.finance.updateExpense(id, user, body); + } + + @Post('expenses/:id/link-loan') + linkExpenseLoan( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { loanId: string }, + ) { + return this.finance.linkExpenseLoan(id, user, body.loanId); + } + + @Delete('expenses/:id') + deleteExpense(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.finance.deleteExpense(id, user); + } + + @Post('expenses/:id/submit') + submitExpense(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.finance.submitExpense(id, user); + } + + @Post('expenses/:id/reviews') + reviewExpense( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { result: 'APPROVED' | 'REJECTED'; comment?: string }, + ) { + return this.finance.reviewExpense(id, user, body); + } + + @Get('loans') + loans( + @CurrentUser() user: AuthUser, + @Query('page') page = '1', + @Query('pageSize') pageSize = '10', + @Query('bucket') bucket?: string, + @Query('keyword') keyword?: string, + ) { + return this.finance.listLoans(user, Number(page) || 1, Number(pageSize) || 10, bucket, keyword); + } + + @Get('loans/:id') + loan(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.finance.getLoan(id, user); + } + + @Post('loans') + createLoan( + @CurrentUser() user: AuthUser, + @Body() + body: { + costType: string; + amount: number; + purpose: string; + bidCaseId?: string; + projectId?: string; + remark?: string; + }, + ) { + return this.finance.createLoan(user, body); + } + + @Patch('loans/:id') + updateLoan( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() + body: { + costType: string; + amount: number; + purpose: string; + bidCaseId?: string; + projectId?: string; + remark?: string; + }, + ) { + return this.finance.updateLoan(id, user, body); + } + + @Delete('loans/:id') + deleteLoan(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.finance.deleteLoan(id, user); + } + + @Post('loans/:id/submit') + submitLoan(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.finance.submitLoan(id, user); + } + + @Post('loans/:id/reviews') + reviewLoan( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { result: 'APPROVED' | 'REJECTED'; comment?: string }, + ) { + return this.finance.reviewLoan(id, user, body); + } + + @Get('bonds') + bonds( + @CurrentUser() user: AuthUser, + @Query('page') page = '1', + @Query('pageSize') pageSize = '20', + @Query('bucket') bucket?: string, + @Query('bondType') bondType?: string, + ) { + return this.finance.listBonds(user, Number(page) || 1, Number(pageSize) || 20, bucket, bondType); + } + + @Post('bonds') + createBond( + @CurrentUser() user: AuthUser, + @Body() + body: { + bondType: string; + amount: number; + bidCaseId?: string; + projectId?: string; + contractId?: string; + dueBackAt?: string; + remark?: string; + }, + ) { + return this.finance.createBond(user, body); + } + + @Post('bonds/:id/pay') + payBond( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { dueBackAt?: string }, + ) { + return this.finance.payBond(id, user, body); + } + + @Post('bonds/:id/return') + returnBond(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.finance.returnBond(id, user); + } + + @Delete('bonds/:id') + deleteBond(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.finance.deleteBond(id, user); + } + + @Get('invoices') + invoices( + @CurrentUser() user: AuthUser, + @Query('page') page = '1', + @Query('pageSize') pageSize = '20', + @Query('bucket') bucket?: string, + ) { + return this.finance.listInvoices(user, Number(page) || 1, Number(pageSize) || 20, bucket); + } + + @Post('invoices') + createInvoice( + @CurrentUser() user: AuthUser, + @Body() body: { contractId: string; milestoneId: string; amount?: number; dueAt?: string; remark?: string }, + ) { + return this.finance.createInvoice(user, body); + } + + @Post('invoices/:id/issue') + issueInvoice( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { invoiceNo: string }, + ) { + return this.finance.issueInvoice(id, user, body); + } + + @Post('invoices/:id/receive') + receiveInvoice(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.finance.receiveInvoice(id, user); + } + + @Get('payments') + payments(@CurrentUser() user: AuthUser) { + return this.finance.listPayments(user); + } + + @Get('payments/bank-channels') + bankChannels(@CurrentUser() user: AuthUser) { + return this.finance.listBankChannels(user); + } + + @Post('payments/bank-webhook') + bankWebhook() { + return this.finance.receiveBankWebhook(); + } + + @Post('payments') + createPayment( + @CurrentUser() user: AuthUser, + @Body() body: { kind: string; refId?: string; amount?: number; remark?: string }, + ) { + return this.finance.createPayment(user, body); + } + + @Post('payments/:id/dispatch') + dispatchPayment( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { channel?: string }, + ) { + return this.finance.dispatchPayment(id, user, body.channel || 'BANK_API'); + } + + @Post('payments/:id/pay') + pay(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.finance.pay(id, user); + } +} diff --git a/apps/api/src/finance/finance.service.ts b/apps/api/src/finance/finance.service.ts new file mode 100644 index 0000000..a38d16b --- /dev/null +++ b/apps/api/src/finance/finance.service.ts @@ -0,0 +1,1461 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import { BANK_PAY_CHANNELS, dispatchBankPay, maskBankAccount } from './bank-pay'; +import { emitSms } from '../sms/sms-bus'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { + BOND_STATUS, + BOND_TYPE, + COST_TYPE, + EXPENSE_STATUS, + FINANCE_BIZ, + INVOICE_STATUS, + LOAN_STATUS, + assertCostObject, + bondBucket, + canAccessFinanceDesk, + canApproveFinance, + canFileFinance, + canSeeSubmittedFinance, + loanRemaining, +} from './finance-workflow'; +import { ProjectService } from '../project/project.service'; +import { assertExpenseBudgetCategory, encodeExpenseBudget } from '../project/project-workflow'; +import { assertCanSeeOwner, ownerScope, withAnd } from '../common/data-scope'; +import { resolveExpenseApproverChain } from '../common/org-chain'; +import { lockDocNo, nextSerial, withUniqueRetry } from '../common/doc-no'; + +const USER_SELECT = { id: true, displayName: true, username: true }; +const PAYEE_EMPLOYEE = { select: { name: true, bankName: true, bankAccount: true, department: { select: { name: true } } } }; +const BID_SELECT = { id: true, bidNo: true, externalNo: true, name: true, status: true, party: { select: { name: true } } }; +const PROJECT_SELECT = { id: true, projectNo: true, name: true, status: true }; + +const EXPENSE_INCLUDE = { + applicant: { select: { id: true, displayName: true, username: true, employee: PAYEE_EMPLOYEE } }, + decidedBy: { select: USER_SELECT }, + payer: { select: { id: true, displayName: true, username: true, employee: PAYEE_EMPLOYEE } }, + bidCase: { select: BID_SELECT }, + project: { select: PROJECT_SELECT }, + loan: true, + lines: { orderBy: { sortNo: 'asc' as const } }, +}; + +const LINE_KINDS = ['AIR', 'HSR', 'TRAIN', 'TAXI', 'HOTEL', 'MEAL', 'OFFICE', 'OTHER'] as const; + +const LOAN_INCLUDE = { + applicant: { select: { id: true, displayName: true, username: true, employee: { select: { name: true, department: { select: { name: true } } } } } }, + decidedBy: { select: USER_SELECT }, + bidCase: { select: BID_SELECT }, + project: { select: PROJECT_SELECT }, + expenses: true, +}; + +type Tx = Prisma.TransactionClient; + +export type CostInput = { + costType: string; + amount: number; + bidCaseId?: string; + projectId?: string; + remark?: string; +}; + +export type ExpenseLineInput = { + id?: string; + kind: string; + amount: number; + invoiceNo?: string; + occurAt?: string; + remark?: string; +}; + +const LINE_TO_CATEGORY: Record = { + AIR: '交通', + HSR: '交通', + TRAIN: '交通', + TAXI: '交通', + HOTEL: '住宿', + MEAL: '餐饮', + OFFICE: '办公', + OTHER: '其他', +}; + +export type ExpenseInput = CostInput & { + loanId?: string; + budgetCategory?: string; + expenseCategory?: string; + purpose?: string; + prepaid?: boolean; + payerId?: string; + lines?: ExpenseLineInput[]; +}; +export type LoanInput = CostInput & { purpose: string }; +export type ReviewInput = { result: 'APPROVED' | 'REJECTED'; comment?: string }; + +@Injectable() +export class FinanceService { + constructor( + private readonly prisma: PrismaService, + private readonly projects: ProjectService, + ) {} + + assertFile(user: AuthUser) { + if (!canFileFinance(user.roles, user.permissions)) { + throw new ForbiddenException('无权填报费用'); + } + } + + assertDesk(user: AuthUser) { + if (!canAccessFinanceDesk(user.roles, user.permissions)) { + throw new ForbiddenException('无权访问财务收支'); + } + } + + async costTargets(user: AuthUser) { + this.assertFile(user); + const canBid = + user.roles.includes('admin') || + user.permissions.some((p) => p === 'bid' || p.startsWith('bid:')); + const [bids, projects] = await Promise.all([ + this.prisma.bidCase.findMany({ + where: canBid + ? { tenantId: '1' } + : { tenantId: '1', assignees: { some: { userId: user.id } } }, + select: BID_SELECT, + orderBy: { createdAt: 'desc' }, + take: 80, + }), + this.prisma.project.findMany({ + where: { tenantId: '1' }, + select: PROJECT_SELECT, + orderBy: { createdAt: 'desc' }, + take: 200, + }), + ]); + return { bids, projects }; + } + + async listExpenses(user: AuthUser, page: number, pageSize: number, bucket?: string, keyword?: string) { + this.assertFile(user); + const where: Prisma.ExpenseClaimWhereInput = { tenantId: '1' }; + if (bucket === 'pending') where.status = EXPENSE_STATUS.PENDING; + if (bucket === 'approved') where.status = EXPENSE_STATUS.APPROVED; + if (bucket === 'rejected') where.status = EXPENSE_STATUS.REJECTED; + if (bucket === 'mine') where.applicantId = user.id; + else { + withAnd(where, (await ownerScope(this.prisma, user, 'applicantId')) as Record); + withAnd(where, this.othersSeeSubmitted(user)); + } + if (keyword) { + withAnd(where, { + OR: [ + { claimNo: { contains: keyword } }, + { purpose: { contains: keyword } }, + { remark: { contains: keyword } }, + { applicant: { displayName: { contains: keyword } } }, + ], + }); + } + const [items, total] = await this.prisma.$transaction([ + this.prisma.expenseClaim.findMany({ + where, + include: EXPENSE_INCLUDE, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.expenseClaim.count({ where }), + ]); + const reveal = canAccessFinanceDesk(user.roles, user.permissions); + return { items: items.map((i) => this.presentExpensePeople(i, reveal)), page, pageSize, total }; + } + + async getExpense(id: string, user: AuthUser) { + this.assertFile(user); + const row = await this.prisma.expenseClaim.findFirst({ + where: { id, tenantId: '1' }, + include: EXPENSE_INCLUDE, + }); + if (!row) throw new NotFoundException('报销单不存在'); + await this.assertVisibleTo(user, row, FINANCE_BIZ.EXPENSE); + const myTask = await this.prisma.approvalTask.findFirst({ + where: { bizType: FINANCE_BIZ.EXPENSE, bizId: id, assigneeId: user.id }, + }); + if (!myTask) await assertCanSeeOwner(this.prisma, user, row.applicantId); + const files = await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizType: 'EXPENSE', bizId: id }, + orderBy: { createdAt: 'asc' }, + }); + const lineFiles = row.lines.length + ? await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizType: 'EXPENSE_LINE', bizId: { in: row.lines.map((l) => l.id) } }, + orderBy: { createdAt: 'asc' }, + }) + : []; + const payment = await this.prisma.paymentRecord.findFirst({ + where: { kind: 'EXPENSE', refId: id }, + orderBy: { createdAt: 'desc' }, + }); + const approvalChain = await this.expenseChainStatus(id, row.applicantId); + const reveal = canAccessFinanceDesk(user.roles, user.permissions); + const presented = this.presentExpensePeople(row, reveal); + return { + ...presented, + lines: presented.lines.map((l) => ({ + ...l, + files: lineFiles.filter((f) => f.bizId === l.id), + })), + files, + payment, + payout: this.expensePayout(row, reveal), + approvalChain, + myActions: this.expenseActions( + row, + user, + Boolean(myTask && myTask.status === 'PENDING'), + approvalChain.some((step) => step.status === 'APPROVED'), + ), + }; + } + + async createExpense(user: AuthUser, dto: ExpenseInput) { + this.assertFile(user); + this.deriveExpenseHeader(dto); + await this.resolveExpenseBudget(user, dto); + const amount = this.normalizeExpenseAmount(dto); + await this.assertPayer(dto, user.id); + await this.validateCost(dto, true); + if (amount > 0) await this.assertProjectBudget(dto); + const created = await withUniqueRetry(() => + this.prisma.$transaction(async (tx) => { + await this.assertLoanForExpense(tx, user, dto); + const claimNo = await this.nextNo(tx, 'BX'); + return tx.expenseClaim.create({ + data: { + tenantId: '1', + claimNo, + applicantId: user.id, + costType: dto.costType, + budgetCategory: dto.costType === COST_TYPE.PROJECT ? dto.budgetCategory : null, + bidCaseId: dto.costType === COST_TYPE.BID ? dto.bidCaseId : null, + projectId: dto.costType === COST_TYPE.PROJECT ? dto.projectId : null, + loanId: dto.prepaid ? null : dto.loanId || null, + expenseCategory: dto.expenseCategory?.trim() || null, + purpose: dto.purpose?.trim() || null, + amount, + remark: dto.remark, + prepaid: Boolean(dto.prepaid), + payerId: dto.prepaid ? dto.payerId || null : null, + status: EXPENSE_STATUS.DRAFT, + lines: { create: this.lineRows(dto.lines) }, + }, + }); + }), + ); + return this.getExpense(created.id, user); + } + + async updateExpense(id: string, user: AuthUser, dto: ExpenseInput) { + const row = await this.requireOwnExpense(id, user); + if (row.status !== EXPENSE_STATUS.DRAFT && row.status !== EXPENSE_STATUS.REJECTED) { + throw new BadRequestException('当前状态不能修改'); + } + this.deriveExpenseHeader(dto); + await this.resolveExpenseBudget(user, dto); + const amount = this.normalizeExpenseAmount(dto); + await this.assertPayer(dto, user.id); + await this.validateCost(dto, true); + if (amount > 0) await this.assertProjectBudget(dto, id); + await this.prisma.$transaction(async (tx) => { + await this.assertLoanForExpense(tx, user, dto, id); + await this.syncLines(tx, id, dto.lines); + await tx.expenseClaim.update({ + where: { id }, + data: { + costType: dto.costType, + budgetCategory: dto.costType === COST_TYPE.PROJECT ? dto.budgetCategory : null, + bidCaseId: dto.costType === COST_TYPE.BID ? dto.bidCaseId : null, + projectId: dto.costType === COST_TYPE.PROJECT ? dto.projectId : null, + loanId: dto.prepaid ? null : dto.loanId || null, + expenseCategory: dto.expenseCategory?.trim() || null, + purpose: dto.purpose?.trim() || null, + amount, + remark: dto.remark, + prepaid: Boolean(dto.prepaid), + payerId: dto.prepaid ? dto.payerId || null : null, + status: EXPENSE_STATUS.DRAFT, + rejectComment: null, + }, + }); + }); + return this.getExpense(id, user); + } + + async linkExpenseLoan(id: string, user: AuthUser, loanId: string) { + this.assertFile(user); + if (!loanId) throw new BadRequestException('请选择要冲销的借款'); + await this.prisma.$transaction(async (tx) => { + const row = await tx.expenseClaim.findFirst({ + where: { id, tenantId: '1' }, + include: { lines: true }, + }); + if (!row) throw new NotFoundException('报销单不存在'); + if (row.applicantId !== user.id) throw new ForbiddenException('只能关联自己的报销单'); + if (!( [EXPENSE_STATUS.DRAFT, EXPENSE_STATUS.REJECTED, EXPENSE_STATUS.PENDING] as string[]).includes(row.status)) { + throw new BadRequestException('当前状态不能关联冲账'); + } + if (row.status === EXPENSE_STATUS.PENDING) { + const passed = await tx.approvalTask.count({ + where: { bizType: FINANCE_BIZ.EXPENSE, bizId: id, status: 'APPROVED' }, + }); + if (passed) throw new BadRequestException('审批已开始,不能再变更冲账借款'); + } + const loan = await tx.loanRecord.findFirst({ + where: { id: loanId, tenantId: '1', applicantId: user.id, status: LOAN_STATUS.OPEN }, + }); + if (!loan) throw new BadRequestException('请选择本人已放款、未结清的借款'); + const dto: ExpenseInput = { + costType: loan.costType, + bidCaseId: loan.bidCaseId || undefined, + projectId: loan.projectId || undefined, + budgetCategory: loan.costType === COST_TYPE.PROJECT ? row.budgetCategory || undefined : undefined, + amount: Number(row.amount), + loanId: loan.id, + prepaid: false, + }; + await this.validateCost(dto, true); + if (dto.costType === COST_TYPE.PROJECT) await this.assertProjectBudget(dto, id); + await this.assertLoanForExpense(tx, user, dto, id); + await tx.expenseClaim.update({ + where: { id }, + data: { + costType: loan.costType, + bidCaseId: loan.costType === COST_TYPE.BID ? loan.bidCaseId : null, + projectId: loan.costType === COST_TYPE.PROJECT ? loan.projectId : null, + budgetCategory: loan.costType === COST_TYPE.PROJECT ? row.budgetCategory : null, + loanId: loan.id, + prepaid: false, + payerId: null, + }, + }); + }); + return this.getExpense(id, user); + } + + async submitExpense(id: string, user: AuthUser) { + await this.prisma.$transaction(async (tx) => { + const row = await tx.expenseClaim.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('报销单不存在'); + if (row.applicantId !== user.id) throw new ForbiddenException('只能提交自己的报销'); + if (row.status !== EXPENSE_STATUS.DRAFT && row.status !== EXPENSE_STATUS.REJECTED) { + throw new BadRequestException('当前状态不能提交'); + } + const lines = await tx.expenseLine.findMany({ where: { claimId: id }, orderBy: { sortNo: 'asc' } }); + if (!lines.length) throw new BadRequestException('至少填一张发票'); + if (lines.some((l) => Number(l.amount) <= 0)) throw new BadRequestException('请填写每张发票的金额'); + const costErr = assertCostObject(row.costType, row.bidCaseId || undefined, row.projectId || undefined); + if (costErr) throw new BadRequestException(costErr); + if (!Number(row.amount) || Number(row.amount) <= 0) throw new BadRequestException('请填写发票金额'); + await this.assertProjectBudget( + { + costType: row.costType, + amount: Number(row.amount), + bidCaseId: row.bidCaseId || undefined, + projectId: row.projectId || undefined, + budgetCategory: row.budgetCategory || undefined, + }, + id, + ); + await this.assertLoanForExpense( + tx, + user, + { + costType: row.costType, + amount: Number(row.amount), + bidCaseId: row.bidCaseId || undefined, + projectId: row.projectId || undefined, + loanId: row.loanId || undefined, + prepaid: row.prepaid, + }, + id, + ); + const chain = await resolveExpenseApproverChain(tx, user.id); + if (!chain.length) { + throw new BadRequestException('无法解析报销审批链,请先在档案里维护项目经理和部门总监'); + } + const first = chain[0]; + await tx.expenseClaim.update({ + where: { id }, + data: { status: EXPENSE_STATUS.PENDING, submittedAt: new Date() }, + }); + await tx.approvalTask.create({ + data: { + tenantId: '1', + title: `报销待审(${first.role}):${(row.purpose || '').trim() || row.claimNo}`, + bizType: FINANCE_BIZ.EXPENSE, + bizId: id, + assigneeId: first.user.id, + }, + }); + emitSms('EXPENSE', { + userIds: [first.user.id], + params: { content: `报销待确认:${row.claimNo}`, apply_no: row.claimNo }, + }); + if (row.loanId) { + await tx.todoTask.updateMany({ + where: { bizId: row.loanId, bizType: FINANCE_BIZ.OFFSET, assigneeId: user.id, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + } + }); + return this.getExpense(id, user); + } + + async reviewExpense(id: string, user: AuthUser, dto: ReviewInput) { + if (dto.result === 'REJECTED' && !dto.comment?.trim()) { + throw new BadRequestException('打回请填写意见'); + } + await this.prisma.$transaction(async (tx) => { + const row = await tx.expenseClaim.findFirst({ + where: { id, tenantId: '1' }, + include: { loan: true }, + }); + if (!row) throw new NotFoundException('报销单不存在'); + if (row.status !== EXPENSE_STATUS.PENDING) { + throw new BadRequestException('当前不是待确认状态'); + } + const myTask = await tx.approvalTask.findFirst({ + where: { bizType: FINANCE_BIZ.EXPENSE, bizId: id, assigneeId: user.id, status: 'PENDING' }, + }); + if (!myTask) throw new ForbiddenException('这不是你当前要审的报销'); + if (dto.result === 'REJECTED') { + await tx.expenseClaim.update({ + where: { id }, + data: { + status: EXPENSE_STATUS.REJECTED, + decidedAt: new Date(), + decidedById: user.id, + rejectComment: dto.comment!.trim(), + }, + }); + await this.closeApprovals(tx, FINANCE_BIZ.EXPENSE, id, 'REJECTED', user.id); + await tx.todoTask.create({ + data: { + tenantId: '1', + title: (row.purpose || '').trim() || `修改报销 ${row.claimNo}`, + bizType: FINANCE_BIZ.EXPENSE, + bizId: id, + assigneeId: row.applicantId, + }, + }); + return; + } + await tx.approvalTask.update({ + where: { id: myTask.id }, + data: { status: 'APPROVED' }, + }); + const chain = await resolveExpenseApproverChain(tx, row.applicantId); + const done = await tx.approvalTask.findMany({ + where: { bizType: FINANCE_BIZ.EXPENSE, bizId: id, status: 'APPROVED' }, + select: { assigneeId: true }, + }); + const doneIds = new Set(done.map((t) => t.assigneeId)); + const next = chain.find((s) => !doneIds.has(s.user.id)); + if (next) { + await tx.approvalTask.create({ + data: { + tenantId: '1', + title: `报销待审(${next.role}):${(row.purpose || '').trim() || row.claimNo}`, + bizType: FINANCE_BIZ.EXPENSE, + bizId: id, + assigneeId: next.user.id, + }, + }); + emitSms('EXPENSE', { + userIds: [next.user.id], + params: { content: `报销待确认:${row.claimNo}`, apply_no: row.claimNo }, + }); + return; + } + if (row.loanId && !row.prepaid) { + await this.applyOffset(tx, row.loanId, Number(row.amount), id); + } + await tx.expenseClaim.update({ + where: { id }, + data: { + status: EXPENSE_STATUS.APPROVED, + decidedAt: new Date(), + decidedById: user.id, + rejectComment: null, + }, + }); + await this.closeApprovals(tx, FINANCE_BIZ.EXPENSE, id, 'APPROVED', user.id); + if (row.prepaid || !row.loanId) { + await this.enqueueExpensePayment(tx, row, user.id); + } + }); + return this.getExpense(id, user); + } + + async listLoans(user: AuthUser, page: number, pageSize: number, bucket?: string, keyword?: string) { + this.assertFile(user); + const where: Prisma.LoanRecordWhereInput = { tenantId: '1' }; + if (bucket === 'pending') where.status = LOAN_STATUS.PENDING; + if (bucket === 'open') where.status = LOAN_STATUS.OPEN; + if (bucket === 'mine') where.applicantId = user.id; + else { + withAnd(where, (await ownerScope(this.prisma, user, 'applicantId')) as Record); + withAnd(where, this.othersSeeSubmitted(user, 'loan')); + } + if (keyword) { + withAnd(where, { + OR: [{ loanNo: { contains: keyword } }, { purpose: { contains: keyword } }, { remark: { contains: keyword } }], + }); + } + const [items, total] = await this.prisma.$transaction([ + this.prisma.loanRecord.findMany({ + where, + include: LOAN_INCLUDE, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.loanRecord.count({ where }), + ]); + return { + items: items.map((r) => ({ + ...r, + remaining: loanRemaining(Number(r.amount), Number(r.offsetAmount)), + })), + page, + pageSize, + total, + }; + } + + async getLoan(id: string, user: AuthUser) { + this.assertFile(user); + const row = await this.prisma.loanRecord.findFirst({ + where: { id, tenantId: '1' }, + include: LOAN_INCLUDE, + }); + if (!row) throw new NotFoundException('借款单不存在'); + await this.assertVisibleTo(user, row, FINANCE_BIZ.LOAN); + await assertCanSeeOwner(this.prisma, user, row.applicantId); + const files = await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizType: 'LOAN', bizId: id }, + orderBy: { createdAt: 'asc' }, + }); + return { + ...row, + files, + remaining: loanRemaining(Number(row.amount), Number(row.offsetAmount)), + myActions: this.loanActions(row, user), + }; + } + + async deleteExpense(id: string, user: AuthUser) { + const row = await this.requireOwnExpense(id, user); + if (row.status !== EXPENSE_STATUS.DRAFT && row.status !== EXPENSE_STATUS.REJECTED) { + throw new BadRequestException('已提交的报销不能删除'); + } + const lines = await this.prisma.expenseLine.findMany({ where: { claimId: id }, select: { id: true } }); + await this.prisma.fileAsset.deleteMany({ + where: { + OR: [{ bizType: 'EXPENSE', bizId: id }, { bizType: 'EXPENSE_LINE', bizId: { in: lines.map((l) => l.id) } }], + }, + }); + await this.prisma.expenseClaim.delete({ where: { id } }); + return { ok: true }; + } + + async deleteLoan(id: string, user: AuthUser) { + const row = await this.requireOwnLoan(id, user); + if (row.status !== LOAN_STATUS.DRAFT && row.status !== LOAN_STATUS.REJECTED) { + throw new BadRequestException('已提交的借款不能删除'); + } + await this.prisma.fileAsset.deleteMany({ where: { bizType: 'LOAN', bizId: id } }); + await this.prisma.loanRecord.delete({ where: { id } }); + return { ok: true }; + } + + async createLoan(user: AuthUser, dto: LoanInput) { + this.assertFile(user); + if (!dto.purpose?.trim()) throw new BadRequestException('请填写借款用途'); + await this.validateCost(dto); + const created = await withUniqueRetry(() => + this.prisma.$transaction(async (tx) => { + const loanNo = await this.nextNo(tx, 'JK'); + return tx.loanRecord.create({ + data: { + tenantId: '1', + loanNo, + applicantId: user.id, + costType: dto.costType, + bidCaseId: dto.costType === COST_TYPE.BID ? dto.bidCaseId : null, + projectId: dto.costType === COST_TYPE.PROJECT ? dto.projectId : null, + amount: dto.amount, + purpose: dto.purpose.trim(), + remark: dto.remark, + status: LOAN_STATUS.DRAFT, + }, + }); + }), + ); + return this.getLoan(created.id, user); + } + + async updateLoan(id: string, user: AuthUser, dto: LoanInput) { + const row = await this.requireOwnLoan(id, user); + if (row.status !== LOAN_STATUS.DRAFT && row.status !== LOAN_STATUS.REJECTED) { + throw new BadRequestException('当前状态不能修改'); + } + if (!dto.purpose?.trim()) throw new BadRequestException('请填写借款用途'); + await this.validateCost(dto); + await this.prisma.loanRecord.update({ + where: { id }, + data: { + costType: dto.costType, + bidCaseId: dto.costType === COST_TYPE.BID ? dto.bidCaseId : null, + projectId: dto.costType === COST_TYPE.PROJECT ? dto.projectId : null, + amount: dto.amount, + purpose: dto.purpose.trim(), + remark: dto.remark, + status: LOAN_STATUS.DRAFT, + }, + }); + return this.getLoan(id, user); + } + + async submitLoan(id: string, user: AuthUser) { + await this.prisma.$transaction(async (tx) => { + const row = await tx.loanRecord.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('借款单不存在'); + if (row.applicantId !== user.id) throw new ForbiddenException('只能提交自己的借款'); + if (row.status !== LOAN_STATUS.DRAFT && row.status !== LOAN_STATUS.REJECTED) { + throw new BadRequestException('当前状态不能提交'); + } + if (Number(row.amount) <= 0) throw new BadRequestException('请填写借款金额'); + const approvers = await this.financeApprovers(tx); + if (!approvers.length) throw new BadRequestException('没有可确认的财务人员'); + await tx.loanRecord.update({ + where: { id }, + data: { status: LOAN_STATUS.PENDING, submittedAt: new Date() }, + }); + await tx.approvalTask.createMany({ + data: approvers.map((u) => ({ + tenantId: '1', + title: (row.purpose || '').trim() || `借支 ${row.loanNo}`, + bizType: FINANCE_BIZ.LOAN, + bizId: id, + assigneeId: u.id, + })), + }); + emitSms('EXPENSE', { + userIds: approvers.map((u) => u.id), + params: { content: `借款待确认:${row.loanNo}`, apply_no: row.loanNo }, + }); + }); + return this.getLoan(id, user); + } + + async reviewLoan(id: string, user: AuthUser, dto: ReviewInput) { + if (!canApproveFinance(user.roles)) throw new ForbiddenException('无权确认借款'); + if (dto.result === 'REJECTED' && !dto.comment?.trim()) { + throw new BadRequestException('打回请填写意见'); + } + await this.prisma.$transaction(async (tx) => { + const row = await tx.loanRecord.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('借款单不存在'); + if (row.status !== LOAN_STATUS.PENDING) throw new BadRequestException('当前不是待确认状态'); + if (dto.result === 'REJECTED') { + await tx.loanRecord.update({ + where: { id }, + data: { status: LOAN_STATUS.REJECTED, decidedAt: new Date(), decidedById: user.id, remark: dto.comment!.trim() }, + }); + await this.closeApprovals(tx, FINANCE_BIZ.LOAN, id, 'REJECTED', user.id); + return; + } + await tx.loanRecord.update({ + where: { id }, + data: { status: LOAN_STATUS.OPEN, decidedAt: new Date(), decidedById: user.id }, + }); + await this.closeApprovals(tx, FINANCE_BIZ.LOAN, id, 'APPROVED', user.id); + await tx.todoTask.create({ + data: { + tenantId: '1', + title: `报销冲账:${row.loanNo}`, + bizType: FINANCE_BIZ.OFFSET, + bizId: id, + assigneeId: row.applicantId, + }, + }); + }); + return this.getLoan(id, user); + } + + async listBonds(user: AuthUser, page: number, pageSize: number, bucket?: string, bondType?: string) { + this.assertDesk(user); + const type = bondType && Object.values(BOND_TYPE).includes(bondType as (typeof BOND_TYPE)[keyof typeof BOND_TYPE]) + ? bondType + : undefined; + const rows = await this.prisma.bondRecord.findMany({ + where: type ? { tenantId: '1', bondType: type } : { tenantId: '1' }, + include: { + bidCase: { select: BID_SELECT }, + project: { select: PROJECT_SELECT }, + contract: { select: { id: true, contractNo: true, name: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + const mapped = rows.map((r) => ({ ...r, bucket: bondBucket(r) })); + const filtered = bucket ? mapped.filter((r) => r.bucket === bucket) : mapped; + const start = (page - 1) * pageSize; + return { items: filtered.slice(start, start + pageSize), page, pageSize, total: filtered.length }; + } + + async createBond( + user: AuthUser, + dto: { + bondType: string; + amount: number; + bidCaseId?: string; + projectId?: string; + contractId?: string; + dueBackAt?: string; + remark?: string; + }, + ) { + this.assertDesk(user); + if (!Object.values(BOND_TYPE).includes(dto.bondType as (typeof BOND_TYPE)[keyof typeof BOND_TYPE])) { + throw new BadRequestException('保证金类型须为投标、履约或质保'); + } + if (!dto.amount || dto.amount <= 0) throw new BadRequestException('请填写保证金金额'); + if (dto.bondType === BOND_TYPE.TENDER && !dto.bidCaseId) { + throw new BadRequestException('投标保证金必须挂投标'); + } + if (dto.bondType === BOND_TYPE.PERFORMANCE && !dto.bidCaseId && !dto.projectId && !dto.contractId) { + throw new BadRequestException('履约保证金必须挂投标或项目'); + } + if (dto.bondType === BOND_TYPE.WARRANTY && !dto.projectId) { + throw new BadRequestException('项目质保金必须挂项目'); + } + return withUniqueRetry(() => + this.prisma.$transaction(async (tx) => { + const bondNo = await this.nextNo(tx, 'BZ'); + return tx.bondRecord.create({ + data: { + tenantId: '1', + bondNo, + bondType: dto.bondType, + amount: dto.amount, + bidCaseId: dto.bidCaseId || null, + projectId: dto.projectId || null, + contractId: dto.contractId || null, + dueBackAt: dto.dueBackAt ? new Date(dto.dueBackAt) : null, + remark: dto.remark, + status: BOND_STATUS.UNPAID, + }, + }); + }), + ); + } + + async payBond(id: string, user: AuthUser, dto: { dueBackAt?: string }) { + this.assertDesk(user); + const row = await this.prisma.bondRecord.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('保证金记录不存在'); + if (row.status !== BOND_STATUS.UNPAID) throw new BadRequestException('当前不是未缴纳状态'); + return this.prisma.bondRecord.update({ + where: { id }, + data: { + status: BOND_STATUS.PAID, + paidAt: new Date(), + dueBackAt: dto.dueBackAt ? new Date(dto.dueBackAt) : row.dueBackAt, + }, + }); + } + + async returnBond(id: string, user: AuthUser) { + this.assertDesk(user); + const row = await this.prisma.bondRecord.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('保证金记录不存在'); + if (row.status !== BOND_STATUS.PAID) throw new BadRequestException('未缴纳或已退回'); + return this.prisma.bondRecord.update({ + where: { id }, + data: { status: BOND_STATUS.RETURNED, returnedAt: new Date() }, + }); + } + + async deleteBond(id: string, user: AuthUser) { + this.assertDesk(user); + const row = await this.prisma.bondRecord.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('保证金记录不存在'); + await this.prisma.bondRecord.delete({ where: { id } }); + return { ok: true }; + } + + async listInvoices(user: AuthUser, page: number, pageSize: number, bucket?: string) { + this.assertDesk(user); + await this.markOverdueInvoices(); + const where: Prisma.InvoiceRecordWhereInput = { tenantId: '1' }; + if (bucket === 'to_issue') where.status = INVOICE_STATUS.TO_ISSUE; + if (bucket === 'issued') where.status = { in: [INVOICE_STATUS.ISSUED, INVOICE_STATUS.OVERDUE] }; + if (bucket === 'received') where.status = INVOICE_STATUS.RECEIVED; + if (bucket === 'overdue') where.status = INVOICE_STATUS.OVERDUE; + const [items, total] = await this.prisma.$transaction([ + this.prisma.invoiceRecord.findMany({ + where, + include: { + contract: { select: { id: true, contractNo: true, name: true, amount: true } }, + milestone: true, + createdBy: { select: USER_SELECT }, + }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.invoiceRecord.count({ where }), + ]); + return { items, page, pageSize, total }; + } + + async createInvoice( + user: AuthUser, + dto: { contractId: string; milestoneId: string; amount?: number; dueAt?: string; remark?: string }, + ) { + this.assertDesk(user); + const contract = await this.prisma.contract.findFirst({ + where: { id: dto.contractId, tenantId: '1' }, + include: { milestones: true }, + }); + if (!contract) throw new NotFoundException('合同不存在'); + if (contract.status !== 'APPROVED') throw new BadRequestException('只有审批通过的合同可以开票'); + const mile = contract.milestones.find((m) => m.id === dto.milestoneId); + if (!mile) throw new BadRequestException('收款条款不属于本合同'); + const exists = await this.prisma.invoiceRecord.findFirst({ where: { milestoneId: mile.id } }); + if (exists) throw new BadRequestException('该收款条款已经开过票'); + const amount = + dto.amount && dto.amount > 0 + ? dto.amount + : Math.round(Number(contract.amount) * Number(mile.ratio)) / 100; + if (amount <= 0) throw new BadRequestException('开票金额无效'); + return this.prisma.invoiceRecord.create({ + data: { + tenantId: '1', + contractId: contract.id, + milestoneId: mile.id, + amount, + dueAt: dto.dueAt ? new Date(dto.dueAt) : mile.dueAt, + remark: dto.remark, + createdById: user.id, + status: INVOICE_STATUS.TO_ISSUE, + }, + include: { contract: true, milestone: true }, + }); + } + + async issueInvoice(id: string, user: AuthUser, dto: { invoiceNo: string }) { + this.assertDesk(user); + if (!dto.invoiceNo?.trim()) throw new BadRequestException('请填写发票号'); + const row = await this.prisma.invoiceRecord.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('开票记录不存在'); + if (row.status !== INVOICE_STATUS.TO_ISSUE) throw new BadRequestException('当前不是待开状态'); + return this.prisma.invoiceRecord.update({ + where: { id }, + data: { status: INVOICE_STATUS.ISSUED, invoiceNo: dto.invoiceNo.trim(), issuedAt: new Date() }, + }); + } + + async receiveInvoice(id: string, user: AuthUser) { + this.assertDesk(user); + const row = await this.prisma.invoiceRecord.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('开票记录不存在'); + if (row.status !== INVOICE_STATUS.ISSUED && row.status !== INVOICE_STATUS.OVERDUE) { + throw new BadRequestException('当前不能登记回款'); + } + return this.prisma.invoiceRecord.update({ + where: { id }, + data: { status: INVOICE_STATUS.RECEIVED, receivedAt: new Date() }, + }); + } + + async decideApproval(approvalId: string, user: AuthUser, dto: ReviewInput) { + const task = await this.prisma.approvalTask.findFirst({ where: { id: approvalId, tenantId: '1' } }); + if (!task) throw new NotFoundException('审批任务不存在'); + if (task.assigneeId !== user.id) throw new ForbiddenException('这不是你的审批'); + if (task.status !== 'PENDING') throw new BadRequestException('该审批已办理'); + if (!task.bizId) throw new BadRequestException('审批缺少业务单'); + if (task.bizType === FINANCE_BIZ.EXPENSE) return this.reviewExpense(task.bizId, user, dto); + if (task.bizType === FINANCE_BIZ.LOAN) return this.reviewLoan(task.bizId, user, dto); + throw new BadRequestException('不是费用或借款审批'); + } + + async myWorkbench(user: AuthUser) { + this.assertFile(user); + const [expenses, loans] = await Promise.all([ + this.prisma.expenseClaim.findMany({ + // 旧版本曾留下没有任何发票明细的空报销草稿;现行规则不可能提交此类单据, + // 不在个人费用中展示,以免和实际报销重复。 + where: { tenantId: '1', applicantId: user.id, lines: { some: {} } }, + include: { bidCase: { select: BID_SELECT }, project: { select: PROJECT_SELECT }, loan: true }, + orderBy: { createdAt: 'desc' }, + }), + this.prisma.loanRecord.findMany({ + where: { tenantId: '1', applicantId: user.id }, + include: { bidCase: { select: BID_SELECT }, project: { select: PROJECT_SELECT } }, + orderBy: { createdAt: 'desc' }, + }), + ]); + return { + expenses, + loans: loans.map((r) => ({ + ...r, + remaining: loanRemaining(Number(r.amount), Number(r.offsetAmount)), + })), + }; + } + + async assetPurchaseLedger(user: AuthUser) { + this.assertDesk(user); + return this.prisma.officeApply.findMany({ + where: { tenantId: '1', kind: 'ASSET_BUY' }, + include: { + applicant: { select: { id: true, displayName: true, username: true } }, + decidedBy: { select: { id: true, displayName: true, username: true } }, + project: { select: { id: true, projectNo: true, name: true } }, + purchase: { select: { id: true, title: true, amount: true, status: true } }, + inboundAsset: { select: { id: true, code: true, name: true, status: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + } + + private normalizeExpenseAmount(dto: ExpenseInput) { + this.validateLines(dto.lines); + if (dto.lines?.length) { + dto.amount = Math.round(dto.lines.reduce((s, l) => s + Number(l.amount || 0), 0) * 100) / 100; + } + return dto.amount || 0; + } + + private validateLines(lines?: ExpenseLineInput[]) { + if (!lines?.length) return; + lines.forEach((line, i) => { + if (!LINE_KINDS.includes(line.kind as (typeof LINE_KINDS)[number])) { + throw new BadRequestException(`第 ${i + 1} 张发票类别无效`); + } + }); + } + + private lineRows(lines?: ExpenseLineInput[]) { + return (lines || []).map((l, i) => ({ + tenantId: '1', + kind: l.kind, + amount: l.amount, + invoiceNo: l.invoiceNo?.trim() || null, + occurAt: l.occurAt ? new Date(l.occurAt) : null, + remark: l.remark?.trim() || null, + sortNo: i, + })); + } + + private deriveExpenseHeader(dto: ExpenseInput) { + const first = dto.lines?.[0]; + if (!dto.expenseCategory?.trim() && first) { + dto.expenseCategory = LINE_TO_CATEGORY[first.kind] || '其他'; + } + if (!dto.purpose?.trim() && dto.lines?.length) { + dto.purpose = + dto.lines + .map((l) => l.remark?.trim()) + .filter(Boolean) + .join(';') || undefined; + } + } + + private async resolveExpenseBudget(user: AuthUser, dto: ExpenseInput) { + if (dto.costType !== COST_TYPE.PROJECT) { + dto.budgetCategory = dto.budgetCategory || undefined; + return; + } + if (dto.budgetCategory === 'PURCHASE') return; + if (dto.budgetCategory === 'TECH') return; + if (dto.budgetCategory && dto.budgetCategory !== 'TRAVEL' && dto.budgetCategory !== 'BUSINESS') return; + const emp = await this.prisma.user.findFirst({ + where: { id: user.id }, + select: { employee: { select: { department: { select: { deptType: true } } } } }, + }); + const deptType = emp?.employee?.department?.deptType || ''; + dto.budgetCategory = encodeExpenseBudget( + dto.budgetCategory === 'BUSINESS' ? 'TRAVEL' : dto.budgetCategory || 'TRAVEL', + deptType === 'TECH' ? 'TECH' : 'BUSINESS', + ); + } + + private async syncLines(tx: Tx, claimId: string, lines?: ExpenseLineInput[]) { + const incoming = lines || []; + const old = await tx.expenseLine.findMany({ where: { claimId } }); + const keep = new Set(incoming.map((l) => l.id).filter(Boolean) as string[]); + const removed = old.filter((o) => !keep.has(o.id)); + if (removed.length) { + await tx.fileAsset.deleteMany({ + where: { bizType: 'EXPENSE_LINE', bizId: { in: removed.map((r) => r.id) } }, + }); + await tx.expenseLine.deleteMany({ where: { id: { in: removed.map((r) => r.id) } } }); + } + for (const [i, l] of incoming.entries()) { + const data = { + kind: l.kind, + amount: l.amount, + invoiceNo: l.invoiceNo?.trim() || null, + occurAt: l.occurAt ? new Date(l.occurAt) : null, + remark: l.remark?.trim() || null, + sortNo: i, + }; + if (l.id && old.some((o) => o.id === l.id)) { + await tx.expenseLine.update({ where: { id: l.id }, data }); + } else { + await tx.expenseLine.create({ data: { ...data, tenantId: '1', claimId } }); + } + } + } + + private async assertPayer(dto: ExpenseInput, applicantId: string) { + if (!dto.prepaid) return; + if (!dto.payerId) throw new BadRequestException('请选择代付人'); + const payer = await this.prisma.user.findFirst({ + where: { id: dto.payerId, tenantId: '1', status: 'ACTIVE' }, + include: { employee: true }, + }); + if (!payer) throw new BadRequestException('代付人不存在'); + if (payer.id === applicantId) throw new BadRequestException('代付人不能是申请人自己'); + if (!payer.employee?.bankAccount?.trim()) { + throw new BadRequestException('代付人档案没有银行账号,请先在人事补卡号,否则无法打款'); + } + } + + private async validateCost(dto: CostInput, draft = false) { + if (!draft) { + if (!dto.amount || dto.amount <= 0) throw new BadRequestException('请填写金额'); + const err = assertCostObject(dto.costType, dto.bidCaseId, dto.projectId); + if (err) throw new BadRequestException(err); + } + if (dto.costType === COST_TYPE.BID && dto.bidCaseId) { + const bid = await this.prisma.bidCase.findFirst({ where: { id: dto.bidCaseId, tenantId: '1' } }); + if (!bid) throw new NotFoundException('投标事项不存在'); + } + if (dto.costType === COST_TYPE.PROJECT && dto.projectId) { + const project = await this.prisma.project.findFirst({ where: { id: dto.projectId, tenantId: '1' } }); + if (!project) throw new NotFoundException('项目不存在'); + } + } + + private async assertProjectBudget(dto: ExpenseInput, excludeExpenseId?: string) { + if (dto.costType !== COST_TYPE.PROJECT || !dto.projectId) return; + const err = assertExpenseBudgetCategory(dto.costType, dto.budgetCategory); + if (err) throw new BadRequestException(err); + await this.projects.assertBudgetRoom(dto.projectId, dto.budgetCategory!, dto.amount, { + excludeExpenseId, + }); + } + + private async assertLoanForExpense(tx: Tx, user: AuthUser, dto: ExpenseInput, excludeId?: string) { + if (dto.prepaid) return; + if (!dto.amount || dto.amount <= 0) return; + const open = await tx.loanRecord.findMany({ + where: { + tenantId: '1', + applicantId: user.id, + costType: dto.costType, + bidCaseId: dto.costType === COST_TYPE.BID ? dto.bidCaseId || null : null, + projectId: dto.costType === COST_TYPE.PROJECT ? dto.projectId || null : null, + status: { in: [LOAN_STATUS.DRAFT, LOAN_STATUS.PENDING, LOAN_STATUS.OPEN] }, + }, + include: { expenses: true }, + }); + if (!open.length) { + if (dto.loanId) throw new BadRequestException('借款单无效'); + return; + } + if (!dto.loanId) { + throw new BadRequestException('该成本对象有在途或未还借款,请先冲账,避免重复报销'); + } + const loan = open.find((l) => l.id === dto.loanId); + if (!loan) throw new BadRequestException('请选择同一成本对象上的借款冲账'); + if (loan.status !== LOAN_STATUS.OPEN) { + throw new BadRequestException('借款通过后才能冲账'); + } + const held = loan.expenses + .filter((e) => e.id !== excludeId && (e.status === EXPENSE_STATUS.PENDING || e.status === EXPENSE_STATUS.APPROVED)) + .reduce((s, e) => s + Number(e.amount), 0); + const remain = loanRemaining(Number(loan.amount), held); + if (dto.amount - remain > 0.001) { + throw new BadRequestException(`冲账金额不能超过未还 ${remain}`); + } + } + + private async applyOffset(tx: Tx, loanId: string, amount: number, expenseId: string) { + const loan = await tx.loanRecord.findFirst({ + where: { id: loanId }, + include: { expenses: true }, + }); + if (!loan || loan.status !== LOAN_STATUS.OPEN) { + throw new BadRequestException('借款不是未还状态,不能冲账'); + } + const held = loan.expenses + .filter((e) => e.id !== expenseId && (e.status === EXPENSE_STATUS.PENDING || e.status === EXPENSE_STATUS.APPROVED)) + .reduce((s, e) => s + Number(e.amount), 0); + const remain = loanRemaining(Number(loan.amount), held); + if (amount - remain > 0.001) throw new BadRequestException('冲账金额超过未还借款'); + const nextOffset = Number(loan.offsetAmount) + amount; + await tx.loanRecord.update({ + where: { id: loanId }, + data: { + offsetAmount: nextOffset, + status: nextOffset + 0.001 >= Number(loan.amount) ? LOAN_STATUS.OFFSET : LOAN_STATUS.OPEN, + }, + }); + } + + private othersSeeSubmitted(user: AuthUser, kind: 'expense' | 'loan' = 'expense') { + const desk = canSeeSubmittedFinance(user.roles, user.permissions); + if (desk) { + return { status: { not: EXPENSE_STATUS.DRAFT } }; + } + const passed = kind === 'loan' ? { in: [LOAN_STATUS.OPEN, LOAN_STATUS.OFFSET] } : EXPENSE_STATUS.APPROVED; + return { OR: [{ applicantId: user.id }, { status: passed }] }; + } + + private async assertVisibleTo( + user: AuthUser, + row: { id: string; status: string; applicantId: string }, + bizType?: string, + ) { + if (row.applicantId === user.id) return; + if (row.status === EXPENSE_STATUS.DRAFT || row.status === LOAN_STATUS.DRAFT) { + throw new ForbiddenException('草稿只在填写人自己的账号里,提交后才会到确认岗'); + } + const desk = canSeeSubmittedFinance(user.roles, user.permissions); + if ( + (row.status === EXPENSE_STATUS.PENDING || + row.status === EXPENSE_STATUS.REJECTED || + row.status === LOAN_STATUS.PENDING || + row.status === LOAN_STATUS.REJECTED) && + !desk + ) { + if (bizType) { + const task = await this.prisma.approvalTask.findFirst({ + where: { bizType, bizId: row.id, assigneeId: user.id }, + }); + if (task) return; + } + throw new ForbiddenException('尚未通过确认,不能传到下一岗'); + } + } + + private async expenseChainStatus(bizId: string, applicantId: string) { + const chain = await resolveExpenseApproverChain(this.prisma, applicantId); + const tasks = await this.prisma.approvalTask.findMany({ + where: { bizType: FINANCE_BIZ.EXPENSE, bizId }, + orderBy: { createdAt: 'asc' }, + }); + return chain.map((s) => { + const t = tasks.find((x) => x.assigneeId === s.user.id); + let status = 'WAITING'; + if (t?.status === 'APPROVED') status = 'APPROVED'; + else if (t?.status === 'PENDING') status = 'PENDING'; + else if (t?.status === 'REJECTED') status = 'REJECTED'; + return { role: s.role, name: s.user.displayName, userId: s.user.id, status }; + }); + } + + private async financeApprovers(tx: Tx) { + const users = await tx.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + roles: { some: { role: { code: { in: ['admin', 'finance', 'owner'] } } } }, + }, + }); + return [...new Map(users.map((u) => [u.id, u])).values()]; + } + + private async closeApprovals(tx: Tx, bizType: string, bizId: string, result: 'APPROVED' | 'REJECTED', actorId: string) { + await tx.approvalTask.updateMany({ + where: { bizType, bizId, assigneeId: actorId, status: 'PENDING' }, + data: { status: result }, + }); + await tx.approvalTask.updateMany({ + where: { bizType, bizId, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + } + + private async nextNo(tx: Tx, prefix: 'BX' | 'JK' | 'BZ') { + await lockDocNo(tx, prefix); + const year = new Date().getFullYear(); + const start = `${prefix}-${year}-`; + const existing = + prefix === 'BX' + ? (await tx.expenseClaim.findMany({ where: { tenantId: '1', claimNo: { startsWith: start } }, select: { claimNo: true } })).map( + (r) => r.claimNo, + ) + : prefix === 'JK' + ? (await tx.loanRecord.findMany({ where: { tenantId: '1', loanNo: { startsWith: start } }, select: { loanNo: true } })).map( + (r) => r.loanNo, + ) + : (await tx.bondRecord.findMany({ where: { tenantId: '1', bondNo: { startsWith: start } }, select: { bondNo: true } })).map( + (r) => r.bondNo, + ); + return nextSerial(existing, start); + } + + private async markOverdueInvoices() { + await this.prisma.invoiceRecord.updateMany({ + where: { status: INVOICE_STATUS.ISSUED, dueAt: { lt: new Date() } }, + data: { status: INVOICE_STATUS.OVERDUE }, + }); + } + + private async requireOwnExpense(id: string, user: AuthUser) { + this.assertFile(user); + const row = await this.prisma.expenseClaim.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('报销单不存在'); + if (row.applicantId !== user.id) throw new ForbiddenException('只能改自己的报销'); + return row; + } + + private async requireOwnLoan(id: string, user: AuthUser) { + this.assertFile(user); + const row = await this.prisma.loanRecord.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('借款单不存在'); + if (row.applicantId !== user.id) throw new ForbiddenException('只能改自己的借款'); + return row; + } + + private expenseActions( + row: { status: string; applicantId: string; prepaid?: boolean }, + user: AuthUser, + canReview = false, + approvalStarted = false, + ) { + const own = row.applicantId === user.id; + const draft = row.status === EXPENSE_STATUS.DRAFT || row.status === EXPENSE_STATUS.REJECTED; + const canAmendBeforeApproval = ([EXPENSE_STATUS.DRAFT, EXPENSE_STATUS.REJECTED, EXPENSE_STATUS.PENDING] as string[]).includes(row.status); + return { + canEdit: own && draft, + canSubmit: own && draft, + canDelete: own && draft, + canReview: canReview && row.status === EXPENSE_STATUS.PENDING, + canAttachFiles: own && canAmendBeforeApproval, + canLinkLoan: own && !row.prepaid && !approvalStarted && canAmendBeforeApproval, + }; + } + + private loanActions(row: { status: string; applicantId: string }, user: AuthUser) { + const own = row.applicantId === user.id; + const draft = row.status === LOAN_STATUS.DRAFT || row.status === LOAN_STATUS.REJECTED; + return { + canEdit: own && draft, + canSubmit: own && draft, + canDelete: own && draft, + canReview: canApproveFinance(user.roles) && row.status === LOAN_STATUS.PENDING, + canOffset: own && row.status === LOAN_STATUS.OPEN, + }; + } + + async listPayments(user: AuthUser) { + this.assertDesk(user); + const items = await this.prisma.paymentRecord.findMany({ + include: { createdBy: { select: { id: true, displayName: true } } }, + orderBy: { createdAt: 'desc' }, + }); + return { items: items.map((i) => ({ ...i, amount: Number(i.amount) })), channels: BANK_PAY_CHANNELS }; + } + + async listBankChannels(user: AuthUser) { + this.assertDesk(user); + return { items: BANK_PAY_CHANNELS }; + } + + async createPayment( + user: AuthUser, + dto: { kind: string; refId?: string; amount?: number; remark?: string }, + ) { + this.assertDesk(user); + if (!canApproveFinance(user.roles)) throw new ForbiddenException('无权登记付款'); + let amount = Number(dto.amount || 0); + let remark = dto.remark?.trim() || ''; + let payee: Awaited> | null = null; + if (dto.kind === 'EXPENSE' && dto.refId) { + const exp = await this.prisma.expenseClaim.findFirst({ where: { id: dto.refId } }); + if (!exp) throw new BadRequestException('报销单不存在'); + if (exp.status !== EXPENSE_STATUS.APPROVED) throw new BadRequestException('只能对已通过的报销登记付款'); + amount = amount || Number(exp.amount); + remark = remark || `报销 ${exp.claimNo}`; + payee = await this.resolvePayee(exp.prepaid ? exp.payerId : exp.applicantId, exp.prepaid ? 'PAYER' : 'APPLICANT'); + } else if (dto.kind === 'BOND' && dto.refId) { + const bond = await this.prisma.bondRecord.findFirst({ where: { id: dto.refId } }); + if (!bond) throw new BadRequestException('保证金不存在'); + amount = amount || Number(bond.amount); + remark = remark || `保证金 ${bond.bondNo}`; + } else if (dto.kind === 'PURCHASE' && dto.refId) { + const purchase = await this.prisma.purchaseRequest.findFirst({ where: { id: dto.refId } }); + if (!purchase) throw new BadRequestException('采购申请不存在'); + if (purchase.status !== 'APPROVED') throw new BadRequestException('只能对已通过的采购付款'); + amount = amount || Number(purchase.amount); + remark = remark || `采购 ${purchase.title}`; + } + if (!(amount > 0)) throw new BadRequestException('请填写付款金额'); + const payNo = await this.nextPayNo(this.prisma); + return this.prisma.paymentRecord.create({ + data: { + tenantId: '1', + payNo, + kind: dto.kind, + refId: dto.refId || null, + amount, + remark: remark || null, + createdById: user.id, + status: 'TODO', + payeeUserId: payee?.payeeUserId || null, + payeeName: payee?.payeeName || null, + payeeKind: payee?.payeeKind || null, + bankName: payee?.bankName || null, + bankAccount: payee?.bankAccount || null, + }, + }); + } + + async pay(id: string, user: AuthUser) { + this.assertDesk(user); + if (!canApproveFinance(user.roles)) throw new ForbiddenException('无权打款'); + const row = await this.prisma.paymentRecord.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('付款单不存在'); + if (row.status === 'PAID') throw new BadRequestException('已经打过款'); + if (row.kind === 'EXPENSE' && !row.bankAccount?.trim()) { + throw new BadRequestException('收款人没有银行账号,补齐人事卡号后再打款'); + } + return this.prisma.paymentRecord.update({ + where: { id }, + data: { status: 'PAID', paidAt: new Date(), payChannel: 'MANUAL', bankStatus: 'SUCCEEDED' }, + }); + } + + async dispatchPayment(id: string, user: AuthUser, channel = 'BANK_API') { + this.assertDesk(user); + if (!canApproveFinance(user.roles)) throw new ForbiddenException('无权打款'); + const row = await this.prisma.paymentRecord.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('付款单不存在'); + if (row.status === 'PAID') throw new BadRequestException('已经打过款'); + const result = await dispatchBankPay(channel, { + paymentId: row.id, + payNo: row.payNo, + amount: Number(row.amount), + purpose: row.remark || row.payNo, + payeeName: row.payeeName || '', + bankName: row.bankName || '', + bankAccount: row.bankAccount || '', + }); + return this.prisma.paymentRecord.update({ + where: { id }, + data: { + payChannel: result.channel, + bankStatus: result.ok ? 'SENT' : 'FAILED', + bankTxnNo: result.bankTxnNo || null, + bankPayload: JSON.stringify(result), + }, + }); + } + + receiveBankWebhook() { + throw new BadRequestException('银企直联尚未开通,回调口已预留'); + } + + private presentExpensePeople(row: T, reveal: boolean): T { + const hideEmp = (emp: { bankAccount?: string | null } | null | undefined) => + emp ? { ...emp, bankAccount: reveal ? emp.bankAccount : maskBankAccount(emp.bankAccount) } : emp; + const hideUser = (u: { employee?: { bankAccount?: string | null } | null } | null | undefined) => + u ? { ...u, employee: hideEmp(u.employee) } : u; + return { ...row, applicant: hideUser(row.applicant as never), payer: hideUser(row.payer as never) }; + } + + private expensePayout( + row: { + prepaid: boolean; + loanId?: string | null; + applicant?: { displayName: string; employee?: { name?: string | null; bankName?: string | null; bankAccount?: string | null } | null }; + payer?: { displayName: string; employee?: { name?: string | null; bankName?: string | null; bankAccount?: string | null } | null } | null; + }, + fullAccount: boolean, + ) { + if (!row.prepaid && row.loanId) { + return { kind: 'OFFSET' as const, name: null, bankName: null, bankAccount: null }; + } + const person = row.prepaid ? row.payer : row.applicant; + const bankAccount = person?.employee?.bankAccount || null; + return { + kind: row.prepaid ? ('PAYER' as const) : ('APPLICANT' as const), + name: person?.employee?.name || person?.displayName || null, + bankName: person?.employee?.bankName || null, + bankAccount: fullAccount ? bankAccount : maskBankAccount(bankAccount), + }; + } + + private async resolvePayee(userId: string | null | undefined, kind: 'APPLICANT' | 'PAYER') { + if (!userId) return null; + const user = await this.prisma.user.findFirst({ + where: { id: userId, tenantId: '1' }, + include: { employee: { select: { name: true, bankName: true, bankAccount: true } } }, + }); + if (!user) return null; + return { + payeeUserId: user.id, + payeeKind: kind, + payeeName: user.employee?.name || user.displayName, + bankName: user.employee?.bankName || null, + bankAccount: user.employee?.bankAccount?.replace(/\s/g, '') || null, + }; + } + + private async enqueueExpensePayment( + tx: Tx, + row: { id: string; claimNo: string; amount: unknown; prepaid: boolean; payerId: string | null; applicantId: string }, + actorId: string, + ) { + const existed = await tx.paymentRecord.findFirst({ where: { kind: 'EXPENSE', refId: row.id } }); + if (existed) return; + const payee = await this.resolvePayee(row.prepaid ? row.payerId : row.applicantId, row.prepaid ? 'PAYER' : 'APPLICANT'); + const payNo = await this.nextPayNo(tx); + await tx.paymentRecord.create({ + data: { + tenantId: '1', + payNo, + kind: 'EXPENSE', + refId: row.id, + amount: row.amount as number, + remark: `报销 ${row.claimNo}${row.prepaid ? ' · 代付' : ''}`, + createdById: actorId, + status: 'TODO', + payeeUserId: payee?.payeeUserId || null, + payeeName: payee?.payeeName || null, + payeeKind: payee?.payeeKind || null, + bankName: payee?.bankName || null, + bankAccount: payee?.bankAccount || null, + }, + }); + } + + private async nextPayNo(db: { paymentRecord: { count: (args: { where: { payNo: { startsWith: string } } }) => Promise } }) { + const year = new Date().getFullYear(); + const count = await db.paymentRecord.count({ where: { payNo: { startsWith: `PAY-${year}-` } } }); + return `PAY-${year}-${String(count + 1).padStart(5, '0')}`; + } +} diff --git a/apps/api/src/health/health.controller.ts b/apps/api/src/health/health.controller.ts new file mode 100644 index 0000000..c85fc71 --- /dev/null +++ b/apps/api/src/health/health.controller.ts @@ -0,0 +1,58 @@ +import { Controller, Get } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ApiTags } from '@nestjs/swagger'; +import Redis from 'ioredis'; +import { PrismaService } from '../prisma/prisma.service'; +import { SkipThrottle } from '@nestjs/throttler'; +import { Public } from '../common/decorators/auth.decorators'; + +@ApiTags('health') +@SkipThrottle() +@Controller('health') +export class HealthController { + constructor( + private readonly prisma: PrismaService, + private readonly config: ConfigService, + ) {} + + @Public() + @Get('live') + live() { + return { status: 'ok' }; + } + + @Public() + @Get('ready') + async ready() { + const checks: Record = {}; + try { + await this.prisma.$queryRaw`SELECT 1`; + checks.postgres = 'up'; + } catch { + checks.postgres = 'down'; + } + const redis = new Redis(this.config.get('REDIS_URL')!, { + maxRetriesPerRequest: 1, + lazyConnect: true, + }); + try { + await redis.connect(); + const pong = await redis.ping(); + checks.redis = pong === 'PONG' ? 'up' : 'down'; + } catch { + checks.redis = 'down'; + } finally { + redis.disconnect(); + } + try { + const host = this.config.get('MINIO_ENDPOINT'); + const port = this.config.get('MINIO_PORT'); + const res = await fetch(`http://${host}:${port}/minio/health/live`); + checks.objectStorage = res.ok ? 'up' : 'down'; + } catch { + checks.objectStorage = 'down'; + } + const ready = Object.values(checks).every((v) => v === 'up'); + return { status: ready ? 'ok' : 'degraded', checks }; + } +} diff --git a/apps/api/src/hr/hr-workflow.spec.ts b/apps/api/src/hr/hr-workflow.spec.ts new file mode 100644 index 0000000..003a4ed --- /dev/null +++ b/apps/api/src/hr/hr-workflow.spec.ts @@ -0,0 +1,109 @@ +import { + calendarDays, + canAdjustPerformance, + canConfirmPayroll, + canManageHr, + canSeeCompanyPayroll, + canViewPayroll, + computePayslip, + housingFund, + incomeTax, + nextStatusForKind, + overtimeToCompHours, + prorationFactor, + scheduledDays, + socialPersonal, + statusPayFactor, + statusPerfFactor, +} from './hr-workflow'; + +describe('hr-workflow', () => { + it('lets hr maintain roster and directors adjust performance', () => { + expect(canManageHr(['hr'], ['hr:org'])).toBe(true); + expect(canManageHr(['biz_director'], [])).toBe(true); + expect(canManageHr(['employee'], ['office'])).toBe(false); + expect(canManageHr(['tech_director'], ['hr:performance'])).toBe(false); + expect(canAdjustPerformance(['tech_director'])).toBe(true); + expect(canAdjustPerformance(['employee'])).toBe(false); + expect(canViewPayroll(['finance'], [])).toBe(true); + expect(canViewPayroll(['biz_staff'], [])).toBe(true); + expect(canSeeCompanyPayroll(['biz_staff'])).toBe(true); + expect(canConfirmPayroll(['biz_director'])).toBe(true); + expect(canConfirmPayroll(['tech_director'])).toBe(false); + expect(canViewPayroll(['employee'], ['office'])).toBe(false); + }); + + it('prorates mid-month hire and uses 单休 / 双休 scheduled days', () => { + expect(calendarDays('2026-08')).toBe(31); + expect(calendarDays('2026-02')).toBe(28); + expect(prorationFactor({ yearMonth: '2026-08', hiredAt: '2026-08-16' })).toBeCloseTo(16 / 31); + expect(prorationFactor({ yearMonth: '2026-08', hiredAt: '2026-07-01' })).toBe(1); + expect(prorationFactor({ yearMonth: '2026-08', leftAt: '2026-08-10' })).toBeCloseTo(10 / 31); + expect(scheduledDays('DOUBLE')).toBe(21.75); + expect(scheduledDays('SINGLE')).toBe(25.75); + const bonusGone = computePayslip({ + baseSalary: 8000, + attendanceBonus: 500, + perfBase: 2000, + socialBase: 8000, + restSchedule: 'DOUBLE', + employmentStatus: 'REGULAR', + yearMonth: '2026-08', + leaveDays: 1, + performanceRatio: 120, + }); + expect(bonusGone.performanceRatio).toBe(120); + expect(bonusGone.leaveDeduct).toBeGreaterThan(0); + expect(statusPayFactor('INTERN')).toBe(0.8); + expect(statusPerfFactor('INTERN')).toBe(0); + expect(statusPerfFactor('PROBATION')).toBe(0.5); + expect(nextStatusForKind('REGULARIZE', 'PROBATION')).toBe('REGULAR'); + expect(nextStatusForKind('LEAVE', 'REGULAR')).toBe('LEFT'); + }); + + it('converts overtime to 调休, deducts leave, withholds social tax and never housing fund', () => { + expect(overtimeToCompHours(8)).toBe(8); + expect(housingFund(10000)).toBe(0); + expect(socialPersonal(10000, 1)).toBe(1050); + expect(incomeTax(1000)).toBe(30); + expect(incomeTax(0)).toBe(0); + + const slip = computePayslip({ + baseSalary: 10000, + perfBase: 3000, + socialBase: 10000, + restSchedule: 'SINGLE', + employmentStatus: 'REGULAR', + hiredAt: '2026-08-16', + yearMonth: '2026-08', + leaveDays: 1, + overtimeHours: 8, + performanceRatio: 100, + prevCompLeaveHours: 0, + }); + expect(slip.housingFund).toBe(0); + expect(slip.compLeaveHours).toBe(8); + expect(slip.leaveDeduct).toBeGreaterThan(0); + expect(slip.socialPersonal).toBeGreaterThan(0); + expect(slip.tax).toBeGreaterThan(0); + expect(slip.grossPay).toBeGreaterThan(slip.netPay); + expect(slip.performancePay).toBeGreaterThan(0); + + const cut = computePayslip({ + baseSalary: 10000, + perfBase: 3000, + socialBase: 10000, + restSchedule: 'SINGLE', + employmentStatus: 'REGULAR', + hiredAt: '2026-08-16', + yearMonth: '2026-08', + leaveDays: 1, + overtimeHours: 8, + performanceRatio: 80, + prevCompLeaveHours: 0, + }); + expect(cut.performancePay).toBeLessThan(slip.performancePay); + expect(cut.grossPay).toBeLessThan(slip.grossPay); + expect(cut.housingFund).toBe(0); + }); +}); diff --git a/apps/api/src/hr/hr-workflow.ts b/apps/api/src/hr/hr-workflow.ts new file mode 100644 index 0000000..f82f493 --- /dev/null +++ b/apps/api/src/hr/hr-workflow.ts @@ -0,0 +1,429 @@ +import { isTechLead } from '../common/role-groups'; + +export const REST = { + SINGLE: 'SINGLE', + DOUBLE: 'DOUBLE', +} as const; + +export const EMP_STATUS = { + INTERN: 'INTERN', + PROBATION: 'PROBATION', + REGULAR: 'REGULAR', + LEFT: 'LEFT', +} as const; + +export const HR_BIZ = { + LEAVE: 'HR_LEAVE', +} as const; + +export const LEAVE_KIND = { + STATUTORY: 'STATUTORY', + ANNUAL: 'ANNUAL', + SICK: 'SICK', + WORK_INJURY: 'WORK_INJURY', + MATERNITY: 'MATERNITY', + PRENATAL: 'PRENATAL', + BREASTFEEDING: 'BREASTFEEDING', + PATERNITY: 'PATERNITY', + PARENTAL: 'PARENTAL', + ONLY_CHILD_CARE: 'ONLY_CHILD_CARE', + MARRIAGE: 'MARRIAGE', + BEREAVEMENT: 'BEREAVEMENT', + PERSONAL: 'PERSONAL', + COMP: 'COMP', +} as const; + +export const HR_APPLY_KIND = { + BUSINESS: 'BUSINESS', + OUT: 'OUT', + OVERTIME: 'OVERTIME', + MAKEUP: 'MAKEUP', + REGULARIZE: 'REGULARIZE', + PROMOTE: 'PROMOTE', + TRANSFER: 'TRANSFER', + RESIGN: 'RESIGN', +} as const; + +export const HR_APPLY_LABEL: Record = { + STATUTORY: '法定节假日', + ANNUAL: '带薪年休假', + SICK: '病假', + WORK_INJURY: '工伤假', + MATERNITY: '产假', + PRENATAL: '产前检查假', + BREASTFEEDING: '哺乳假', + PATERNITY: '陪产假', + PARENTAL: '育儿假', + ONLY_CHILD_CARE: '独生子女护理假', + MARRIAGE: '婚假', + BEREAVEMENT: '丧假', + PERSONAL: '事假', + COMP: '调休', + BUSINESS: '出差', + OUT: '外出', + OVERTIME: '加班', + MAKEUP: '补卡', + REGULARIZE: '转正', + PROMOTE: '晋升', + TRANSFER: '转岗', + RESIGN: '离职', +}; + +export function isLeaveKind(kind: string) { + return (Object.values(LEAVE_KIND) as string[]).includes(kind); +} + +export function leavePayBucket(kind: string): 'unpaid' | 'sick' | 'comp' | 'paid' | null { + if (kind === LEAVE_KIND.PERSONAL) return 'unpaid'; + if (kind === LEAVE_KIND.SICK) return 'sick'; + if (kind === LEAVE_KIND.COMP) return 'comp'; + if (isLeaveKind(kind)) return 'paid'; + return null; +} + +export function isHrApplyKind(kind: string) { + return isLeaveKind(kind) || (Object.values(HR_APPLY_KIND) as string[]).includes(kind); +} + +export const LEAVE_STATUS = { + PENDING: 'PENDING', + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', +} as const; + +export const PUNCH_KIND = { + IN: 'IN', + OUT: 'OUT', +} as const; + +export const LIFE_KIND = { + HIRE: 'HIRE', + INTERN: 'INTERN', + PROBATION: 'PROBATION', + REGULARIZE: 'REGULARIZE', + TRANSFER: 'TRANSFER', + PROMOTE: 'PROMOTE', + LEAVE: 'LEAVE', +} as const; + +export const PAYROLL_STATUS = { + DRAFT: 'DRAFT', + CONFIRMED: 'CONFIRMED', +} as const; + +export const DEPT_TYPES = ['ADMIN', 'BUSINESS', 'TECH', 'FINANCE', 'HR'] as const; + +export const SCHEDULED_DAYS = { + DOUBLE: 21.75, + SINGLE: 25.75, +} as const; + +export const SOCIAL_RATE = 0.105; +export const HOUSING_FUND_RATE = 0; +export const TAX_THRESHOLD = 5000; +export const HOURS_PER_DAY = 8; +export const SICK_PAY_RATIO = 0.6; +export const INTERN_PAY_FACTOR = 0.8; +export const PROBATION_PERF_FACTOR = 0.5; + +const TAX_BRACKETS = [ + { max: 3000, rate: 0.03, deduct: 0 }, + { max: 12000, rate: 0.1, deduct: 210 }, + { max: 25000, rate: 0.2, deduct: 1410 }, + { max: 35000, rate: 0.25, deduct: 2660 }, + { max: 55000, rate: 0.3, deduct: 4410 }, + { max: 80000, rate: 0.35, deduct: 7160 }, + { max: Infinity, rate: 0.45, deduct: 15160 }, +]; + +export function roundMoney(n: number) { + return Math.round((n + Number.EPSILON) * 100) / 100; +} + +export function canAccessHr(roles: string[], permissions: string[]) { + if (roles.includes('admin')) return true; + return permissions.some((p) => p === 'hr' || p.startsWith('hr:')); +} + +export function canManageHr(roles: string[], permissions: string[]) { + if (['admin', 'owner', 'hr', 'biz_director'].some((r) => roles.includes(r))) return true; + return canAccessHr(roles, permissions) && (roles.includes('hr') || roles.includes('biz_director') || roles.includes('biz_staff')); +} + +export function canAdjustPerformance(roles: string[]) { + return ['admin', 'owner', 'hr', 'biz_director'].some((r) => roles.includes(r)) || isTechLead(roles); +} + +/** 商务部兼人事/财务,看全公司工资条;总监只看本部门。 */ +export function canSeeCompanyPayroll(roles: string[]) { + return ['admin', 'owner', 'hr', 'finance', 'biz_director', 'biz_staff'].some((r) => roles.includes(r)); +} + +export function canConfirmPayroll(roles: string[]) { + return ['admin', 'owner', 'hr', 'biz_director'].some((r) => roles.includes(r)); +} + +export function canViewPayroll(roles: string[], permissions: string[]) { + if (canAdjustPerformance(roles) || canSeeCompanyPayroll(roles)) return true; + return canAccessHr(roles, permissions); +} + +export function assertYearMonth(ym?: string) { + if (!ym || !/^\d{4}-(0[1-9]|1[0-2])$/.test(ym)) { + throw new Error('请填写年月,格式 YYYY-MM'); + } + return ym; +} + +export function parseYearMonth(ym: string) { + const value = assertYearMonth(ym); + const [year, month] = value.split('-').map(Number); + return { year, month }; +} + +export function calendarDays(yearMonth: string) { + const { year, month } = parseYearMonth(yearMonth); + return new Date(year, month, 0).getDate(); +} + +export function currentYearMonth(now = new Date()) { + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`; +} + +function asDate(value?: Date | string | null) { + if (!value) return null; + const d = value instanceof Date ? value : new Date(value); + if (Number.isNaN(d.getTime())) return null; + return new Date(d.getFullYear(), d.getMonth(), d.getDate()); +} + +export function prorationFactor(opts: { + hiredAt?: Date | string | null; + leftAt?: Date | string | null; + yearMonth: string; +}) { + const days = calendarDays(opts.yearMonth); + const { year, month } = parseYearMonth(opts.yearMonth); + const monthStart = new Date(year, month - 1, 1); + const monthEnd = new Date(year, month - 1, days); + const hired = asDate(opts.hiredAt); + const left = asDate(opts.leftAt); + let from = monthStart; + let to = monthEnd; + if (hired) { + if (hired > monthEnd) return 0; + if (hired > monthStart) from = hired; + } + if (left) { + if (left < monthStart) return 0; + if (left < monthEnd) to = left; + } + if (to < from) return 0; + const onPayroll = Math.round((to.getTime() - from.getTime()) / 86400000) + 1; + return Math.min(1, Math.max(0, onPayroll / days)); +} + +export function scheduledDays(rest?: string) { + return rest === REST.DOUBLE ? SCHEDULED_DAYS.DOUBLE : SCHEDULED_DAYS.SINGLE; +} + +export function statusPayFactor(status?: string) { + if (status === EMP_STATUS.INTERN) return INTERN_PAY_FACTOR; + return 1; +} + +export function statusPerfFactor(status?: string) { + if (status === EMP_STATUS.INTERN) return 0; + if (status === EMP_STATUS.PROBATION) return PROBATION_PERF_FACTOR; + return 1; +} + +export function dailyRate(amount: number, rest?: string) { + return roundMoney(amount / scheduledDays(rest)); +} + +export function wagePackage(input: { + baseSalary?: number; + postSalary?: number; + phoneSubsidy?: number; + transportSubsidy?: number; + mealSubsidy?: number; +}) { + return roundMoney( + (input.baseSalary || 0) + + (input.postSalary || 0) + + (input.phoneSubsidy || 0) + + (input.transportSubsidy || 0) + + (input.mealSubsidy || 0), + ); +} + +export function internalHourRate(input: { + baseSalary?: number; + postSalary?: number; + phoneSubsidy?: number; + transportSubsidy?: number; + mealSubsidy?: number; + perfBase?: number; + restSchedule?: string; +}) { + const days = scheduledDays(input.restSchedule); + return roundMoney((wagePackage(input) + (input.perfBase || 0)) / days / HOURS_PER_DAY); +} + +export function socialPersonal(socialBase: number, factor = 1) { + return roundMoney(Math.max(0, socialBase) * SOCIAL_RATE * factor); +} + +export function housingFund(_socialBase?: number) { + return 0 * HOUSING_FUND_RATE; +} + +export function incomeTax(taxable: number) { + if (taxable <= 0) return 0; + const bracket = TAX_BRACKETS.find((b) => taxable <= b.max) ?? TAX_BRACKETS[TAX_BRACKETS.length - 1]; + return roundMoney(taxable * bracket.rate - bracket.deduct); +} + +export function overtimeToCompHours(overtimeHours: number) { + return roundMoney(Math.max(0, overtimeHours)); +} + +export function nextStatusForKind(kind: string, current: string) { + if (kind === LIFE_KIND.INTERN) return EMP_STATUS.INTERN; + if (kind === LIFE_KIND.PROBATION) return EMP_STATUS.PROBATION; + if (kind === LIFE_KIND.REGULARIZE) return EMP_STATUS.REGULAR; + if (kind === LIFE_KIND.LEAVE) return EMP_STATUS.LEFT; + if (kind === LIFE_KIND.HIRE) return current || EMP_STATUS.PROBATION; + if (kind === LIFE_KIND.TRANSFER || kind === LIFE_KIND.PROMOTE) return current; + return null; +} + +export type PayslipInput = { + baseSalary: number; + postSalary?: number; + attendanceBonus?: number; + phoneSubsidy?: number; + transportSubsidy?: number; + mealSubsidy?: number; + probationSalary?: number; + perfBase: number; + socialBase: number; + restSchedule: string; + employmentStatus: string; + hiredAt?: Date | string | null; + leftAt?: Date | string | null; + regularizeAt?: Date | string | null; + yearMonth: string; + leaveDays?: number; + sickDays?: number; + overtimeHours?: number; + compLeaveDays?: number; + performanceRatio?: number; + prevCompLeaveHours?: number; +}; + +function packageForStatus(input: PayslipInput, status: string) { + const regular = wagePackage(input); + if (status === EMP_STATUS.INTERN || status === EMP_STATUS.PROBATION) { + if (input.probationSalary && input.probationSalary > 0) return input.probationSalary; + return roundMoney(regular * statusPayFactor(status)); + } + return regular; +} + +export function computePayslip(input: PayslipInput) { + const factor = prorationFactor(input); + const rest = input.restSchedule === REST.DOUBLE ? REST.DOUBLE : REST.SINGLE; + const divisor = scheduledDays(rest); + const leaveDays = Math.max(0, Number(input.leaveDays ?? 0)); + const sickDays = Math.max(0, Number(input.sickDays ?? 0)); + const hasLeave = leaveDays + sickDays > 0; + const overtimeHours = overtimeToCompHours(Number(input.overtimeHours ?? 0)); + const requestedComp = Math.max(0, Number(input.compLeaveDays ?? 0)); + const prev = Math.max(0, Number(input.prevCompLeaveHours ?? 0)); + const maxCompDays = (prev + overtimeHours) / HOURS_PER_DAY; + const usedCompDays = roundMoney(Math.min(requestedComp, maxCompDays)); + const usedHours = roundMoney(usedCompDays * HOURS_PER_DAY); + const nextBalance = roundMoney(prev + overtimeHours - usedHours); + const ratioPct = Math.min(200, Math.max(0, Number(input.performanceRatio ?? 100))); + const attendancePay = hasLeave ? 0 : roundMoney((input.attendanceBonus || 0) * factor); + + const { year, month } = parseYearMonth(input.yearMonth); + const monthStart = new Date(year, month - 1, 1); + const monthEnd = new Date(year, month - 1, calendarDays(input.yearMonth)); + const regularize = asDate(input.regularizeAt); + const inThisMonth = + regularize && regularize >= monthStart && regularize <= monthEnd && regularize > monthStart; + + const perfOf = (status: string) => + roundMoney(input.perfBase * statusPerfFactor(status) * (ratioPct / 100)); + + let basePay: number; + let performancePay: number; + let daily: number; + let leaveDeduct: number; + let sickDeduct: number; + + if (inThisMonth && regularize) { + const internEnd = new Date(regularize); + internEnd.setDate(internEnd.getDate() - 1); + const internDays = Math.max(0, Math.round((internEnd.getTime() - monthStart.getTime()) / 86400000) + 1); + const regularDays = Math.max(0, Math.round((monthEnd.getTime() - regularize.getTime()) / 86400000) + 1); + const total = internDays + regularDays || 1; + const internLeave = roundMoney(leaveDays * (internDays / total)); + const regularLeave = roundMoney(leaveDays - internLeave); + const internSick = roundMoney(sickDays * (internDays / total)); + const regularSick = roundMoney(sickDays - internSick); + const internPkg = packageForStatus(input, EMP_STATUS.PROBATION); + const regularPkg = packageForStatus(input, EMP_STATUS.REGULAR); + const internDaily = dailyRate(internPkg + perfOf(EMP_STATUS.PROBATION), rest); + const regularDaily = dailyRate(regularPkg + perfOf(EMP_STATUS.REGULAR), rest); + daily = roundMoney((internDaily * internDays + regularDaily * regularDays) / total); + leaveDeduct = roundMoney(internDaily * internLeave + regularDaily * regularLeave); + sickDeduct = roundMoney( + internDaily * internSick * (1 - SICK_PAY_RATIO) + regularDaily * regularSick * (1 - SICK_PAY_RATIO), + ); + const internAttend = Math.max(0, internDays - internLeave - internSick); + const regularAttend = Math.max(0, regularDays - regularLeave - regularSick); + performancePay = roundMoney( + perfOf(EMP_STATUS.PROBATION) * (internAttend / divisor) + perfOf(EMP_STATUS.REGULAR) * (regularAttend / divisor), + ); + basePay = roundMoney( + internPkg * (internAttend / divisor) + regularPkg * (regularAttend / divisor) + attendancePay, + ); + } else { + const pkg = packageForStatus(input, input.employmentStatus); + const perf = perfOf(input.employmentStatus); + daily = dailyRate(pkg + perf, rest); + leaveDeduct = roundMoney(daily * leaveDays); + sickDeduct = roundMoney(daily * sickDays * (1 - SICK_PAY_RATIO)); + basePay = roundMoney(Math.max(0, pkg * factor - leaveDeduct - sickDeduct) + attendancePay); + performancePay = roundMoney(perf * factor); + } + + const grossPay = roundMoney(basePay + performancePay); + const social = socialPersonal(input.socialBase, factor); + const housing = housingFund(input.socialBase); + const taxable = roundMoney(Math.max(0, grossPay - social - housing - TAX_THRESHOLD)); + const tax = incomeTax(taxable); + const netPay = roundMoney(grossPay - social - housing - tax); + return { + prorationFactor: factor, + scheduledDays: divisor, + dailyRate: daily, + basePay, + leaveDeduct, + sickDeduct, + performancePay, + performanceRatio: roundMoney(ratioPct), + grossPay, + socialPersonal: social, + housingFund: housing, + tax, + netPay, + overtimeHours, + compLeaveUsedDays: usedCompDays, + compLeaveHours: nextBalance, + }; +} diff --git a/apps/api/src/hr/hr.controller.ts b/apps/api/src/hr/hr.controller.ts new file mode 100644 index 0000000..57450f9 --- /dev/null +++ b/apps/api/src/hr/hr.controller.ts @@ -0,0 +1,461 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator'; +import { HrService } from './hr.service'; + +@ApiTags('hr') +@ApiBearerAuth() +@Controller() +export class HrController { + constructor(private readonly hr: HrService) {} + + @Get('departments') + departments() { + return this.hr.listDepartments(); + } + + @Post('departments') + createDept( + @CurrentUser() user: AuthUser, + @Body() + body: { name: string; code: string; deptType: string; parentId?: string; categoryCode?: string; sortNo?: number }, + ) { + return this.hr.createDepartment(user, body); + } + + @Patch('departments/:id') + updateDept( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() + body: { name?: string; code?: string; deptType?: string; parentId?: string | null; categoryCode?: string; sortNo?: number }, + ) { + return this.hr.updateDepartment(id, user, body); + } + + @Delete('departments/:id') + deleteDept(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.hr.deleteDepartment(id, user); + } + + @Get('hr-settings') + hrSettings() { + return this.hr.getSettings(); + } + + @Patch('hr-settings') + updateHrSettings( + @CurrentUser() user: AuthUser, + @Body() + body: { + idMask?: boolean; + autoAccount?: boolean; + probationMonths?: number; + defaultRest?: string; + workStart?: string; + workEnd?: string; + }, + ) { + return this.hr.updateSettings(user, body); + } + + @Get('attendance-rules') + attendanceRules() { + return this.hr.listAttendanceRules(); + } + + @Post('attendance-rules') + createAttendanceRule( + @CurrentUser() user: AuthUser, + @Body() + body: { + name: string; + restSchedule?: string; + workStart: string; + workEnd: string; + lateGraceMinutes?: number; + punchPlace?: string | null; + punchAddress?: string | null; + punchLat?: number | null; + punchLng?: number | null; + punchRadiusMeters?: number; + enabled?: boolean; + sortNo?: number; + remark?: string | null; + }, + ) { + return this.hr.saveAttendanceRule(user, undefined, body); + } + + @Patch('attendance-rules/:id') + updateAttendanceRule( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() + body: { + name: string; + restSchedule?: string; + workStart: string; + workEnd: string; + lateGraceMinutes?: number; + punchPlace?: string | null; + punchAddress?: string | null; + punchLat?: number | null; + punchLng?: number | null; + punchRadiusMeters?: number; + enabled?: boolean; + sortNo?: number; + remark?: string | null; + }, + ) { + return this.hr.saveAttendanceRule(user, id, body); + } + + @Delete('attendance-rules/:id') + deleteAttendanceRule(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.hr.deleteAttendanceRule(id, user); + } + + @Get('employees') + employees(@CurrentUser() user: AuthUser) { + return this.hr.listEmployees(user); + } + + @Get('employees/:id') + employee(@Param('id') id: string) { + return this.hr.getEmployee(id); + } + + @Delete('employees/:id') + deleteEmp(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.hr.deleteEmployee(id, user); + } + + @Post('employees') + createEmp( + @CurrentUser() user: AuthUser, + @Body() + body: { + name: string; + employeeNo?: string; + departmentId: string; + title?: string; + restSchedule?: string; + employmentStatus?: string; + hiredAt?: string; + internStartAt?: string; + probationEndAt?: string; + managerId?: string | string[] | null; + gender?: string; + email?: string; + mobile?: string; + idNo?: string; + idType?: string; + education?: string; + birthday?: string; + ethnicity?: string; + maritalStatus?: string; + hukouAddress?: string; + liveAddress?: string; + emergencyName?: string; + emergencyPhone?: string; + idIssuer?: string; + idValidFrom?: string; + idValidTo?: string; + personCategory?: string; + ccTo?: string; + shortMobile?: string; + workMobile?: string; + school?: string; + major?: string; + graduatedAt?: string; + professionalTitle?: string; + certificates?: string; + companyName?: string; + jobRole?: string; + jobLevel?: string; + attendGroup?: string; + probationMonths?: number; + contractStartAt?: string; + contractEndAt?: string; + recruitSource?: string; + landline?: string; + landlineExt?: string; + groupShortNo?: string; + socialNo?: string; + housingFundNo?: string; + familyNote?: string; + jobType?: string; + ccDaily?: string; + ccWeekly?: string; + ccMonthly?: string; + bankName?: string; + bankAccount?: string; + remark?: string; + baseSalary?: number; + postSalary?: number; + attendanceBonus?: number; + phoneSubsidy?: number; + transportSubsidy?: number; + mealSubsidy?: number; + probationSalary?: number; + perfBase?: number; + socialBase?: number; + }, + ) { + return this.hr.createEmployee(user, body); + } + + @Patch('employees/:id') + updateEmp( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() + body: { + name?: string; + departmentId?: string; + title?: string; + restSchedule?: string; + managerId?: string | string[] | null; + baseSalary?: number; + postSalary?: number; + attendanceBonus?: number; + phoneSubsidy?: number; + transportSubsidy?: number; + mealSubsidy?: number; + probationSalary?: number; + perfBase?: number; + socialBase?: number; + internStartAt?: string | null; + probationEndAt?: string | null; + gender?: string | null; + email?: string | null; + mobile?: string | null; + idNo?: string | null; + idType?: string | null; + education?: string | null; + birthday?: string | null; + ethnicity?: string | null; + maritalStatus?: string | null; + hukouAddress?: string | null; + liveAddress?: string | null; + emergencyName?: string | null; + emergencyPhone?: string | null; + idIssuer?: string | null; + idValidFrom?: string | null; + idValidTo?: string | null; + personCategory?: string | null; + ccTo?: string | null; + shortMobile?: string | null; + workMobile?: string | null; + school?: string | null; + major?: string | null; + graduatedAt?: string | null; + professionalTitle?: string | null; + certificates?: string | null; + companyName?: string | null; + jobRole?: string | null; + jobLevel?: string | null; + attendGroup?: string | null; + probationMonths?: number | null; + contractStartAt?: string | null; + contractEndAt?: string | null; + recruitSource?: string | null; + landline?: string | null; + landlineExt?: string | null; + groupShortNo?: string | null; + socialNo?: string | null; + housingFundNo?: string | null; + familyNote?: string | null; + jobType?: string | null; + ccDaily?: string | null; + ccWeekly?: string | null; + ccMonthly?: string | null; + bankName?: string | null; + bankAccount?: string | null; + remark?: string | null; + }, + ) { + return this.hr.updateEmployee(id, user, body); + } + + @Get('employment-events') + events(@CurrentUser() user: AuthUser, @Query('employeeId') employeeId?: string) { + return this.hr.listEvents(user, employeeId); + } + + @Post('employment-events') + createEvent( + @CurrentUser() user: AuthUser, + @Body() body: { employeeId: string; kind: string; effectiveAt?: string; toDepartmentId?: string; comment?: string }, + ) { + return this.hr.createEvent(user, body); + } + + @Get('attendance') + attendance(@CurrentUser() user: AuthUser, @Query('yearMonth') yearMonth?: string) { + return this.hr.listAttendance(user, yearMonth); + } + + @Post('attendance') + upsertAttendance( + @CurrentUser() user: AuthUser, + @Body() + body: { + employeeId: string; + yearMonth: string; + workDays?: number; + leaveDays?: number; + sickDays?: number; + overtimeHours?: number; + compLeaveDays?: number; + remark?: string; + }, + ) { + return this.hr.upsertAttendance(user, body); + } + + @Post('attendance/import') + importAttendance( + @CurrentUser() user: AuthUser, + @Body() + body: { + yearMonth: string; + items: { + employeeId?: string; + employeeNo?: string; + workDays?: number; + leaveDays?: number; + sickDays?: number; + overtimeHours?: number; + compLeaveDays?: number; + remark?: string; + }[]; + }, + ) { + return this.hr.importAttendance(user, body); + } + + @Post('attendance/punch') + punch(@CurrentUser() user: AuthUser, @Body() body: { kind?: string }) { + return this.hr.punch(user, body.kind || 'IN'); + } + + @Get('attendance/punches/mine') + myPunches(@CurrentUser() user: AuthUser, @Query('date') date?: string) { + return this.hr.myPunches(user, date); + } + + @Get('attendance/punches') + punches(@CurrentUser() user: AuthUser, @Query('yearMonth') yearMonth?: string) { + return this.hr.listPunches(user, yearMonth); + } + + @Get('attendance/comp-balance') + compBalance(@CurrentUser() user: AuthUser) { + return this.hr.compBalance(user); + } + + @Get('leave-requests') + leaves(@CurrentUser() user: AuthUser, @Query('bucket') bucket?: string) { + return this.hr.listLeaves(user, bucket); + } + + @Get('leave-requests/:id') + leaveOne(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.hr.getLeave(id, user); + } + + @Post('leave-requests') + createLeave( + @CurrentUser() user: AuthUser, + @Body() + body: { + kind: string; + startAt?: string; + endAt?: string; + days?: number; + reason: string; + toDeptId?: string; + targetTitle?: string; + punchKind?: string; + }, + ) { + return this.hr.createLeave(user, body); + } + + @Post('leave-requests/:id/review') + reviewLeave( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { result: 'APPROVED' | 'REJECTED'; comment?: string }, + ) { + return this.hr.reviewLeave(id, user, body); + } + + @Delete('leave-requests/:id') + deleteLeave(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.hr.deleteLeave(id, user); + } + + @Get('payroll') + payroll(@CurrentUser() user: AuthUser, @Query('yearMonth') yearMonth?: string) { + return this.hr.getPayroll(user, yearMonth); + } + + @Post('payroll/run') + runPayroll(@CurrentUser() user: AuthUser, @Body() body: { yearMonth?: string }) { + return this.hr.runPayroll(user, body); + } + + @Post('payroll/:id/confirm') + confirm(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.hr.confirmPayroll(id, user); + } + + @Post('payroll/:id/unconfirm') + unconfirm(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.hr.unconfirmPayroll(id, user); + } + + @Patch('payroll-items/:id') + adjust( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { performanceRatio: number }, + ) { + return this.hr.adjustPerformance(id, user, body); + } + + @Get('labor-contracts') + laborContracts(@CurrentUser() user: AuthUser, @Query('keyword') keyword?: string) { + return this.hr.listLaborContracts(user, keyword); + } + + @Get('labor-contracts/:id') + laborContract(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.hr.getLaborContract(id, user); + } + + @Post('labor-contracts') + createLabor( + @CurrentUser() user: AuthUser, + @Body() body: { employeeId: string; contractNo?: string; startAt?: string; endAt?: string; remark?: string }, + ) { + return this.hr.createLaborContract(user, body); + } + + @Patch('labor-contracts/:id') + updateLabor( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { contractNo?: string; startAt?: string | null; endAt?: string | null; remark?: string }, + ) { + return this.hr.updateLaborContract(id, user, body); + } + + @Delete('labor-contracts/:id') + deleteLabor(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.hr.deleteLaborContract(id, user); + } +} diff --git a/apps/api/src/hr/hr.service.ts b/apps/api/src/hr/hr.service.ts new file mode 100644 index 0000000..8b73377 --- /dev/null +++ b/apps/api/src/hr/hr.service.ts @@ -0,0 +1,1997 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { descendantDeptIds, effectiveScope } from '../common/data-scope'; +import { ancestorDeptIds } from '../common/org-chain'; +import { nextExpenseClaimNo } from '../common/doc-no'; +import { emitSms } from '../sms/sms-bus'; +import { canAccessContract } from '../contract/contract-workflow'; +import { + DEPT_TYPES, + EMP_STATUS, + HR_APPLY_KIND, + HR_APPLY_LABEL, + HR_BIZ, + isHrApplyKind, + isLeaveKind, + leavePayBucket, + LEAVE_KIND, + LEAVE_STATUS, + LIFE_KIND, + PAYROLL_STATUS, + PUNCH_KIND, + REST, + assertYearMonth, + canAdjustPerformance, + canConfirmPayroll, + canSeeCompanyPayroll, + canManageHr, + canViewPayroll, + computePayslip, + currentYearMonth, + HOURS_PER_DAY, + nextStatusForKind, + prorationFactor, +} from './hr-workflow'; + +const EMP_INCLUDE = { + department: true, + manager: { select: { id: true, name: true, employeeNo: true } }, + users: { + select: { + id: true, + username: true, + status: true, + roles: { select: { role: { select: { code: true } } } }, + }, + }, +} satisfies Prisma.EmployeeInclude; + +type Tx = Prisma.TransactionClient; + +type ArchiveFields = { + gender?: string | null; + email?: string | null; + mobile?: string | null; + idNo?: string | null; + idType?: string | null; + education?: string | null; + birthday?: string | null; + ethnicity?: string | null; + maritalStatus?: string | null; + hukouAddress?: string | null; + liveAddress?: string | null; + emergencyName?: string | null; + emergencyPhone?: string | null; + idIssuer?: string | null; + idValidFrom?: string | null; + idValidTo?: string | null; + personCategory?: string | null; + ccTo?: string | null; + shortMobile?: string | null; + workMobile?: string | null; + school?: string | null; + major?: string | null; + graduatedAt?: string | null; + professionalTitle?: string | null; + certificates?: string | null; + companyName?: string | null; + jobRole?: string | null; + jobLevel?: string | null; + attendGroup?: string | null; + probationMonths?: number | null; + contractStartAt?: string | null; + contractEndAt?: string | null; + recruitSource?: string | null; + landline?: string | null; + landlineExt?: string | null; + groupShortNo?: string | null; + socialNo?: string | null; + housingFundNo?: string | null; + familyNote?: string | null; + jobType?: string | null; + ccDaily?: string | string[] | null; + ccWeekly?: string | string[] | null; + ccMonthly?: string | string[] | null; + bankName?: string | null; + bankAccount?: string | null; + remark?: string | null; +}; + +const ARCHIVE_TEXT = [ + 'ethnicity', + 'maritalStatus', + 'hukouAddress', + 'liveAddress', + 'emergencyName', + 'emergencyPhone', + 'idIssuer', + 'idType', + 'personCategory', + 'ccTo', + 'shortMobile', + 'workMobile', + 'school', + 'major', + 'professionalTitle', + 'certificates', + 'companyName', + 'jobRole', + 'jobLevel', + 'attendGroup', + 'recruitSource', + 'landline', + 'landlineExt', + 'groupShortNo', + 'socialNo', + 'housingFundNo', + 'familyNote', + 'jobType', + 'bankName', + 'bankAccount', + 'remark', + 'email', + 'mobile', + 'idNo', +] as const; + +const ARCHIVE_DATE = ['birthday', 'idValidFrom', 'idValidTo', 'graduatedAt', 'contractStartAt', 'contractEndAt'] as const; + +function archiveData(dto: ArchiveFields, mode: 'create' | 'update') { + const out: Record = {}; + for (const key of ARCHIVE_TEXT) { + if (mode === 'create') out[key] = cleanText(dto[key]) ?? null; + else if (dto[key] !== undefined) out[key] = cleanText(dto[key]); + } + for (const key of ARCHIVE_DATE) { + if (mode === 'create') out[key] = parseDate(dto[key]); + else if (dto[key] !== undefined) out[key] = parseDate(dto[key]); + } + if (mode === 'create') out.probationMonths = dto.probationMonths ?? null; + else if (dto.probationMonths !== undefined) out.probationMonths = dto.probationMonths; + for (const key of ['ccDaily', 'ccWeekly', 'ccMonthly'] as const) { + if (mode === 'create') out[key] = joinPeopleIds(dto[key]) ?? null; + else if (dto[key] !== undefined) out[key] = joinPeopleIds(dto[key]); + } + return out; +} + +const GENDERS = ['MALE', 'FEMALE'] as const; +const EDUCATIONS = ['HIGH_SCHOOL', 'COLLEGE', 'BACHELOR', 'MASTER', 'DOCTOR'] as const; +const TIME_RE = /^([01]\d|2[0-3]):[0-5]\d$/; + +type EmpDto = { + name: string; + employeeNo?: string; + departmentId: string; + title?: string; + restSchedule?: string; + employmentStatus?: string; + hiredAt?: string; + internStartAt?: string; + probationEndAt?: string; + managerId?: string | string[] | null; + baseSalary?: number; + postSalary?: number; + attendanceBonus?: number; + phoneSubsidy?: number; + transportSubsidy?: number; + mealSubsidy?: number; + probationSalary?: number; + perfBase?: number; + socialBase?: number; +} & ArchiveFields; + +function n(v: Prisma.Decimal | number | string | null | undefined) { + return Number(v ?? 0); +} + +function parseDate(value?: string | Date | null) { + if (!value) return null; + const d = value instanceof Date ? value : new Date(value); + if (Number.isNaN(d.getTime())) throw new BadRequestException('日期格式不正确'); + return d; +} + +function maskValue(value?: string | null) { + if (!value) return value ?? null; + if (value.length <= 4) return '****'; + return `${value.slice(0, 2)}****${value.slice(-2)}`; +} + +function addMonths(d: Date, months: number) { + const next = new Date(d); + next.setMonth(next.getMonth() + months); + return next; +} + +function cleanText(v?: string | null) { + if (v === undefined) return undefined; + if (v === null) return null; + const t = v.trim(); + return t || null; +} + +const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; +const LEAD_TITLE = /经理|总监|主管|负责人/; +const LEAD_ROLES = new Set(['owner', 'pm', 'admin', 'biz_director']); + +function joinPeopleIds(v: unknown) { + if (v === undefined) return undefined; + if (v === null || v === '') return null; + const parts = Array.isArray(v) ? v : String(v).split(/[,,、\s]+/); + const ids = [...new Set(parts.map((s) => String(s).trim()).filter((s) => UUID_RE.test(s)))]; + return ids.length ? ids.join(',') : null; +} + +function peopleIdList(v: unknown): string[] { + const joined = joinPeopleIds(v); + return joined ? joined.split(',') : []; +} + +function managerIdList(emp: { managerId?: string | null; managerIds?: unknown }): string[] { + const extra = peopleIdList(emp.managerIds); + if (extra.length) return extra; + return emp.managerId ? [emp.managerId] : []; +} + +function splitManagers(v: unknown): { primary: string | null; joined: string | null; all: string[] } { + const all = peopleIdList(v); + return { primary: all[0] || null, joined: all.length ? all.join(',') : null, all }; +} + +function reportsTo(emp: { managerId?: string | null; managerIds?: unknown }, employeeId?: string | null) { + return Boolean(employeeId && managerIdList(emp).includes(employeeId)); +} + +function accountRolesOf(users?: { roles?: { role: { code: string } }[] }[]) { + return [...new Set((users || []).flatMap((u) => (u.roles || []).map((r) => r.role.code)))]; +} + +function isDeptLead(emp: { + title?: string | null; + jobRole?: string | null; + professionalTitle?: string | null; + users?: { roles?: { role: { code: string } }[] }[]; +}) { + const text = `${emp.title || ''}${emp.jobRole || ''}${emp.professionalTitle || ''}`; + if (LEAD_TITLE.test(text)) return true; + return accountRolesOf(emp.users).some((r) => LEAD_ROLES.has(r) || r.endsWith('_director')); +} + +function isDirectorLead(emp: { + title?: string | null; + jobRole?: string | null; + users?: { roles?: { role: { code: string } }[] }[]; +}) { + if (/总监/.test(`${emp.title || ''}${emp.jobRole || ''}`)) return true; + return accountRolesOf(emp.users).some((r) => r.endsWith('_director') || r === 'biz_director' || r === 'owner'); +} + +function assertGender(v?: string | null) { + if (v === undefined || v === null || v === '') return null; + if (!GENDERS.includes(v as (typeof GENDERS)[number])) throw new BadRequestException('性别须是男或女'); + return v; +} + +function assertEducation(v?: string | null) { + if (v === undefined || v === null || v === '') return null; + if (!EDUCATIONS.includes(v as (typeof EDUCATIONS)[number])) throw new BadRequestException('学历选项无效'); + return v; +} + +function assertClock(v: string | undefined, label: string) { + if (!v) throw new BadRequestException(`请填写${label}`); + if (!TIME_RE.test(v)) throw new BadRequestException(`${label}须是 HH:mm`); + return v; +} + +@Injectable() +export class HrService { + constructor(private readonly prisma: PrismaService) {} + + assertManage(user: AuthUser) { + if (!canManageHr(user.roles, user.permissions)) { + throw new ForbiddenException('无权维护组织人事'); + } + } + + assertViewPayroll(user: AuthUser) { + if (!canViewPayroll(user.roles, user.permissions)) { + throw new ForbiddenException('无权查看薪酬'); + } + } + + listDepartments() { + return this.prisma.department.findMany({ + where: { tenantId: '1' }, + include: { parent: true, _count: { select: { employees: true, children: true } } }, + orderBy: { sortNo: 'asc' }, + }); + } + + async createDepartment( + user: AuthUser, + dto: { name: string; code: string; deptType: string; parentId?: string; categoryCode?: string; sortNo?: number }, + ) { + this.assertManage(user); + if (!dto.name?.trim() || !dto.code?.trim()) throw new BadRequestException('请填写部门名称和编码'); + if (!DEPT_TYPES.includes(dto.deptType as (typeof DEPT_TYPES)[number])) { + throw new BadRequestException('部门类型须是 ADMIN / BUSINESS / TECH / FINANCE / HR'); + } + if (dto.parentId) { + const parent = await this.prisma.department.findFirst({ where: { id: dto.parentId, tenantId: '1' } }); + if (!parent) throw new BadRequestException('上级部门不存在'); + } + try { + return await this.prisma.department.create({ + data: { + tenantId: '1', + name: dto.name.trim(), + code: dto.code.trim().toUpperCase(), + deptType: dto.deptType, + parentId: dto.parentId || null, + categoryCode: dto.categoryCode?.trim() || null, + sortNo: dto.sortNo ?? 99, + }, + include: { parent: true, _count: { select: { employees: true, children: true } } }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { + throw new BadRequestException('部门编码已存在'); + } + throw e; + } + } + + async updateDepartment( + id: string, + user: AuthUser, + dto: { name?: string; code?: string; deptType?: string; parentId?: string | null; categoryCode?: string; sortNo?: number }, + ) { + this.assertManage(user); + const row = await this.prisma.department.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('部门不存在'); + if (dto.deptType && !DEPT_TYPES.includes(dto.deptType as (typeof DEPT_TYPES)[number])) { + throw new BadRequestException('部门类型须是 ADMIN / BUSINESS / TECH / FINANCE / HR'); + } + if (dto.parentId === id) throw new BadRequestException('不能把部门设为自己的上级'); + try { + return await this.prisma.department.update({ + where: { id }, + data: { + name: dto.name?.trim() || undefined, + code: dto.code ? dto.code.trim().toUpperCase() : undefined, + deptType: dto.deptType || undefined, + parentId: dto.parentId === undefined ? undefined : dto.parentId || null, + categoryCode: dto.categoryCode === undefined ? undefined : dto.categoryCode?.trim() || null, + sortNo: dto.sortNo, + }, + include: { parent: true, _count: { select: { employees: true, children: true } } }, + }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { + throw new BadRequestException('部门编码已存在'); + } + throw e; + } + } + + async deleteDepartment(id: string, user: AuthUser) { + this.assertManage(user); + const row = await this.prisma.department.findFirst({ + where: { id, tenantId: '1' }, + include: { _count: { select: { employees: true, children: true } } }, + }); + if (!row) throw new NotFoundException('部门不存在'); + if (row._count.children) throw new BadRequestException('请先删完下级部门'); + if (row._count.employees) throw new BadRequestException('部门下还有员工,请先办理调岗或离职'); + await this.prisma.department.delete({ where: { id } }); + return { ok: true }; + } + + async getSettings() { + const row = await this.prisma.hrSetting.findFirst({ where: { tenantId: '1' } }); + if (row) return row; + return this.prisma.hrSetting.create({ + data: { tenantId: '1', idMask: true, autoAccount: false, probationMonths: 3, defaultRest: REST.SINGLE, workStart: '09:00', workEnd: '18:00' }, + }); + } + + async updateSettings( + user: AuthUser, + dto: { + idMask?: boolean; + autoAccount?: boolean; + probationMonths?: number; + defaultRest?: string; + workStart?: string; + workEnd?: string; + }, + ) { + this.assertManage(user); + const current = await this.getSettings(); + if (dto.probationMonths !== undefined && (dto.probationMonths < 0 || dto.probationMonths > 12)) { + throw new BadRequestException('试用期须在 0 到 12 个月'); + } + const rest = + dto.defaultRest === undefined ? undefined : dto.defaultRest === REST.DOUBLE ? REST.DOUBLE : REST.SINGLE; + return this.prisma.hrSetting.update({ + where: { id: current.id }, + data: { + idMask: dto.idMask, + autoAccount: dto.autoAccount === undefined ? false : Boolean(dto.autoAccount), + probationMonths: dto.probationMonths, + defaultRest: rest, + workStart: dto.workStart ? assertClock(dto.workStart, '上班时间') : undefined, + workEnd: dto.workEnd ? assertClock(dto.workEnd, '下班时间') : undefined, + }, + }); + } + + listAttendanceRules() { + return this.prisma.attendanceRule.findMany({ where: { tenantId: '1' }, orderBy: { sortNo: 'asc' } }); + } + + async saveAttendanceRule( + user: AuthUser, + id: string | undefined, + dto: { + name: string; + restSchedule?: string; + workStart: string; + workEnd: string; + lateGraceMinutes?: number; + punchPlace?: string | null; + punchAddress?: string | null; + punchLat?: number | null; + punchLng?: number | null; + punchRadiusMeters?: number; + enabled?: boolean; + sortNo?: number; + remark?: string | null; + }, + ) { + this.assertManage(user); + if (!dto.name?.trim()) throw new BadRequestException('请填写班次名称'); + const rest = dto.restSchedule === REST.DOUBLE ? REST.DOUBLE : REST.SINGLE; + const data = { + name: dto.name.trim(), + restSchedule: rest, + workStart: assertClock(dto.workStart, '上班时间'), + workEnd: assertClock(dto.workEnd, '下班时间'), + lateGraceMinutes: dto.lateGraceMinutes ?? 0, + punchPlace: cleanText(dto.punchPlace) ?? null, + punchAddress: cleanText(dto.punchAddress) ?? null, + punchLat: dto.punchLat == null || Number.isNaN(Number(dto.punchLat)) ? null : Number(dto.punchLat), + punchLng: dto.punchLng == null || Number.isNaN(Number(dto.punchLng)) ? null : Number(dto.punchLng), + punchRadiusMeters: dto.punchRadiusMeters ?? 300, + enabled: dto.enabled ?? true, + sortNo: dto.sortNo ?? 99, + remark: cleanText(dto.remark) ?? null, + }; + try { + if (id) { + const row = await this.prisma.attendanceRule.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('班次不存在'); + return this.prisma.attendanceRule.update({ where: { id }, data }); + } + return this.prisma.attendanceRule.create({ data: { tenantId: '1', ...data } }); + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { + throw new BadRequestException('班次名称已存在'); + } + throw e; + } + } + + async deleteAttendanceRule(id: string, user: AuthUser) { + this.assertManage(user); + const row = await this.prisma.attendanceRule.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('班次不存在'); + await this.prisma.attendanceRule.delete({ where: { id } }); + return { ok: true }; + } + + async listEmployees(user: AuthUser) { + const where: Prisma.EmployeeWhereInput = { tenantId: '1' }; + const scope = effectiveScope(user); + if (scope === 'SELF') where.id = user.employeeId || '__none__'; + if (scope === 'DEPT') { + const deptIds = await descendantDeptIds(this.prisma, user.departmentId); + where.OR = [ + { departmentId: { in: deptIds } }, + ...(user.employeeId + ? [{ managerId: user.employeeId }, { managerIds: { contains: user.employeeId } }] + : []), + ]; + } + const [rows, settings] = await Promise.all([ + this.prisma.employee.findMany({ + where, + include: EMP_INCLUDE, + orderBy: { employeeNo: 'asc' }, + }), + this.getSettings(), + ]); + const items = rows.map((r) => this.serializeEmployee(r, settings.idMask)); + return { items: await this.attachPhotos(await this.attachManagerNames(items)), total: rows.length }; + } + + async getEmployee(id: string) { + const row = await this.prisma.employee.findFirst({ where: { id, tenantId: '1' }, include: EMP_INCLUDE }); + if (!row) throw new NotFoundException('员工不存在'); + const [item] = await this.attachPhotos(await this.attachManagerNames([this.serializeEmployee(row, false)])); + return item; + } + + async deleteEmployee(id: string, user: AuthUser) { + this.assertManage(user); + const row = await this.assertEmployee(id); + const [payroll, att, punches, leaves] = await Promise.all([ + this.prisma.payrollItem.count({ where: { employeeId: id } }), + this.prisma.attendanceMonth.count({ where: { employeeId: id } }), + this.prisma.attendancePunch.count({ where: { employeeId: id } }), + this.prisma.leaveRequest.count({ where: { employeeId: id, status: LEAVE_STATUS.APPROVED } }), + ]); + if (payroll || att || punches || leaves) { + throw new BadRequestException('该员工已有薪酬或假勤记录,请走入转调离,不能直接删除'); + } + await this.prisma.$transaction(async (tx) => { + await tx.user.updateMany({ where: { employeeId: id }, data: { employeeId: null } }); + await tx.employmentEvent.deleteMany({ where: { employeeId: id } }); + await tx.leaveRequest.deleteMany({ where: { employeeId: id } }); + await tx.fileAsset.deleteMany({ + where: { tenantId: '1', bizType: 'EMPLOYEE_ID_PHOTO', bizId: id }, + }); + await tx.employee.delete({ where: { id } }); + }); + return { ok: true, name: row.name }; + } + + private async nextEmployeeNo() { + const prefix = `E-${new Date().toISOString().slice(0, 10).replace(/-/g, '')}-`; + const last = await this.prisma.employee.findFirst({ + where: { tenantId: '1', employeeNo: { startsWith: prefix } }, + orderBy: { employeeNo: 'desc' }, + select: { employeeNo: true }, + }); + const seq = last ? Number(last.employeeNo.slice(prefix.length)) + 1 : 1; + return `${prefix}${String(Number.isFinite(seq) && seq > 0 ? seq : 1).padStart(3, '0')}`; + } + + async createEmployee( + user: AuthUser, + dto: EmpDto, + ) { + this.assertManage(user); + if (!dto.name?.trim()) throw new BadRequestException('请填写姓名'); + await this.assertDept(dto.departmentId); + const mgrs = splitManagers(dto.managerId); + for (const mid of mgrs.all) await this.assertDeptLead(mid, dto.departmentId, undefined); + await this.assertCcLeads(dto, dto.departmentId, undefined, mgrs.all); + const settings = await this.getSettings(); + const employeeNo = dto.employeeNo?.trim() + ? dto.employeeNo.trim().toUpperCase() + : await this.nextEmployeeNo(); + const rest = dto.restSchedule === REST.DOUBLE || dto.restSchedule === REST.SINGLE ? dto.restSchedule : settings.defaultRest; + const status = Object.values(EMP_STATUS).includes(dto.employmentStatus as (typeof EMP_STATUS)[keyof typeof EMP_STATUS]) + ? dto.employmentStatus! + : EMP_STATUS.PROBATION; + const hiredAt = parseDate(dto.hiredAt) || new Date(); + const probationEndAt = + parseDate(dto.probationEndAt) || + (status === EMP_STATUS.PROBATION ? addMonths(hiredAt, settings.probationMonths) : null); + try { + const created = await this.prisma.$transaction(async (tx) => { + const row = await tx.employee.create({ + data: { + tenantId: '1', + name: dto.name.trim(), + employeeNo, + departmentId: dto.departmentId, + title: dto.title?.trim() || null, + restSchedule: rest, + employmentStatus: status, + hiredAt, + internStartAt: status === EMP_STATUS.INTERN ? parseDate(dto.internStartAt) || hiredAt : parseDate(dto.internStartAt), + probationEndAt, + managerId: mgrs.primary, + managerIds: mgrs.joined, + gender: assertGender(dto.gender), + education: assertEducation(dto.education), + ...archiveData(dto, 'create'), + baseSalary: dto.baseSalary ?? 0, + postSalary: dto.postSalary ?? 0, + attendanceBonus: dto.attendanceBonus ?? 0, + phoneSubsidy: dto.phoneSubsidy ?? 0, + transportSubsidy: dto.transportSubsidy ?? 0, + mealSubsidy: dto.mealSubsidy ?? 0, + probationSalary: dto.probationSalary ?? 0, + perfBase: dto.perfBase ?? 0, + socialBase: dto.socialBase ?? dto.baseSalary ?? 0, + } as Prisma.EmployeeUncheckedCreateInput, + include: EMP_INCLUDE, + }); + await tx.employmentEvent.create({ + data: { + tenantId: '1', + employeeId: row.id, + kind: LIFE_KIND.HIRE, + toStatus: status, + toDeptId: row.departmentId, + effectiveAt: hiredAt, + comment: '入职', + actorId: user.id, + }, + }); + return row; + }); + return (await this.attachManagerNames([this.serializeEmployee(created)]))[0]; + } catch (e) { + if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') { + throw new BadRequestException('工号已存在'); + } + throw e; + } + } + + async updateEmployee( + id: string, + user: AuthUser, + dto: { + name?: string; + departmentId?: string; + title?: string; + restSchedule?: string; + managerId?: string | string[] | null; + baseSalary?: number; + postSalary?: number; + attendanceBonus?: number; + phoneSubsidy?: number; + transportSubsidy?: number; + mealSubsidy?: number; + probationSalary?: number; + perfBase?: number; + socialBase?: number; + internStartAt?: string | null; + probationEndAt?: string | null; + } & ArchiveFields, + ) { + this.assertManage(user); + const row = await this.assertEmployee(id); + if (row.employmentStatus === EMP_STATUS.LEFT) throw new BadRequestException('已离职员工请走入转调离,不能直接改档案'); + const deptId = dto.departmentId || row.departmentId; + const mgrs = dto.managerId === undefined ? null : splitManagers(dto.managerId); + if (mgrs) { + for (const mid of mgrs.all) await this.assertDeptLead(mid, deptId, id); + } + await this.assertCcLeads(dto, deptId, id, mgrs ? mgrs.all : managerIdList(row)); + if (dto.departmentId && dto.departmentId !== row.departmentId) { + await this.assertDept(dto.departmentId); + } + const rest = + dto.restSchedule === undefined ? undefined : dto.restSchedule === REST.DOUBLE ? REST.DOUBLE : REST.SINGLE; + const updated = await this.prisma.employee.update({ + where: { id }, + data: { + name: dto.name?.trim() || undefined, + departmentId: dto.departmentId && dto.departmentId !== row.departmentId ? dto.departmentId : undefined, + title: dto.title === undefined ? undefined : dto.title?.trim() || null, + restSchedule: rest, + managerId: mgrs ? mgrs.primary : undefined, + managerIds: mgrs ? mgrs.joined : undefined, + baseSalary: dto.baseSalary, + postSalary: dto.postSalary, + attendanceBonus: dto.attendanceBonus, + phoneSubsidy: dto.phoneSubsidy, + transportSubsidy: dto.transportSubsidy, + mealSubsidy: dto.mealSubsidy, + probationSalary: dto.probationSalary, + perfBase: dto.perfBase, + socialBase: dto.socialBase, + internStartAt: dto.internStartAt === undefined ? undefined : parseDate(dto.internStartAt), + probationEndAt: dto.probationEndAt === undefined ? undefined : parseDate(dto.probationEndAt), + gender: dto.gender === undefined ? undefined : assertGender(dto.gender), + education: dto.education === undefined ? undefined : assertEducation(dto.education), + ...archiveData(dto, 'update'), + } as Prisma.EmployeeUncheckedUpdateInput, + include: EMP_INCLUDE, + }); + return (await this.attachManagerNames([this.serializeEmployee(updated, false)]))[0]; + } + + async listEvents(user: AuthUser, employeeId?: string) { + this.assertManage(user); + const rows = await this.prisma.employmentEvent.findMany({ + where: { tenantId: '1', ...(employeeId ? { employeeId } : {}) }, + include: { + employee: { include: { department: true } }, + fromDept: true, + toDept: true, + actor: { select: { id: true, displayName: true } }, + }, + orderBy: { createdAt: 'desc' }, + take: 200, + }); + return { items: rows, total: rows.length }; + } + + async createEvent( + user: AuthUser, + dto: { employeeId: string; kind: string; effectiveAt?: string; toDepartmentId?: string; comment?: string }, + ) { + this.assertManage(user); + const kind = dto.kind?.toUpperCase(); + if (!Object.values(LIFE_KIND).includes(kind as (typeof LIFE_KIND)[keyof typeof LIFE_KIND])) { + throw new BadRequestException('请选择实习、试用、转正、调岗、晋升或离职'); + } + const emp = await this.assertEmployee(dto.employeeId); + if (emp.employmentStatus === EMP_STATUS.LEFT && kind !== LIFE_KIND.HIRE) { + throw new BadRequestException('该员工已离职'); + } + const nextStatus = nextStatusForKind(kind, emp.employmentStatus); + if (!nextStatus) throw new BadRequestException('不支持的异动类型'); + const effectiveAt = parseDate(dto.effectiveAt) || new Date(); + let toDeptId = emp.departmentId; + if (kind === LIFE_KIND.TRANSFER) { + if (!dto.toDepartmentId) throw new BadRequestException('调岗请选择转入部门'); + await this.assertDept(dto.toDepartmentId); + toDeptId = dto.toDepartmentId; + } + if (kind === LIFE_KIND.LEAVE && effectiveAt < (emp.hiredAt || effectiveAt)) { + throw new BadRequestException('离职日不能早于入职日'); + } + const updated = await this.prisma.$transaction(async (tx) => { + await tx.employmentEvent.create({ + data: { + tenantId: '1', + employeeId: emp.id, + kind, + fromStatus: emp.employmentStatus, + toStatus: nextStatus, + fromDeptId: emp.departmentId, + toDeptId, + effectiveAt, + comment: dto.comment?.trim() || null, + actorId: user.id, + }, + }); + return tx.employee.update({ + where: { id: emp.id }, + data: { + employmentStatus: nextStatus, + departmentId: toDeptId, + internStartAt: kind === LIFE_KIND.INTERN ? effectiveAt : undefined, + probationEndAt: kind === LIFE_KIND.PROBATION ? emp.probationEndAt : undefined, + leftAt: kind === LIFE_KIND.LEAVE ? effectiveAt : undefined, + }, + include: EMP_INCLUDE, + }); + }); + return this.serializeEmployee(updated); + } + + async listAttendance(user: AuthUser, yearMonth?: string) { + this.assertManage(user); + const ym = yearMonth ? assertYearMonthSafe(yearMonth) : currentYearMonth(); + const rows = await this.prisma.attendanceMonth.findMany({ + where: { tenantId: '1', yearMonth: ym }, + include: { employee: { include: { department: true } }, importedBy: { select: { displayName: true } } }, + orderBy: { importedAt: 'desc' }, + }); + return { + yearMonth: ym, + items: rows.map((r) => ({ + ...r, + workDays: n(r.workDays), + leaveDays: n(r.leaveDays), + sickDays: n(r.sickDays), + overtimeHours: n(r.overtimeHours), + compLeaveDays: n(r.compLeaveDays), + compLeaveHours: n(r.employee.compLeaveHours), + })), + total: rows.length, + }; + } + + async upsertAttendance( + user: AuthUser, + dto: { + employeeId: string; + yearMonth: string; + workDays?: number; + leaveDays?: number; + sickDays?: number; + overtimeHours?: number; + compLeaveDays?: number; + remark?: string; + }, + ) { + this.assertManage(user); + const ym = assertYearMonthSafe(dto.yearMonth); + const emp = await this.assertEmployee(dto.employeeId); + return this.prisma.$transaction((tx) => this.writeAttendance(tx, user.id, emp, ym, dto)); + } + + async importAttendance( + user: AuthUser, + dto: { + yearMonth: string; + items: { + employeeId?: string; + employeeNo?: string; + workDays?: number; + leaveDays?: number; + sickDays?: number; + overtimeHours?: number; + compLeaveDays?: number; + remark?: string; + }[]; + }, + ) { + this.assertManage(user); + const ym = assertYearMonthSafe(dto.yearMonth); + if (!dto.items?.length) throw new BadRequestException('请提供假勤明细'); + const written = await this.prisma.$transaction(async (tx) => { + const out = []; + for (const line of dto.items) { + let emp = line.employeeId + ? await tx.employee.findFirst({ where: { id: line.employeeId, tenantId: '1' } }) + : line.employeeNo + ? await tx.employee.findFirst({ + where: { tenantId: '1', employeeNo: line.employeeNo.trim().toUpperCase() }, + }) + : null; + if (!emp) throw new BadRequestException(`找不到员工 ${line.employeeNo || line.employeeId || ''}`); + out.push(await this.writeAttendance(tx, user.id, emp, ym, line)); + } + return out; + }); + return { yearMonth: ym, items: written, total: written.length }; + } + + async punch(user: AuthUser, kind: string) { + const employeeId = user.employeeId; + if (!employeeId) throw new BadRequestException('当前账号未关联员工档案,不能打卡'); + const k = kind === PUNCH_KIND.OUT ? PUNCH_KIND.OUT : PUNCH_KIND.IN; + const start = new Date(); + start.setHours(0, 0, 0, 0); + const end = new Date(start); + end.setDate(end.getDate() + 1); + const today = await this.prisma.attendancePunch.findMany({ + where: { employeeId, punchedAt: { gte: start, lt: end } }, + orderBy: { punchedAt: 'asc' }, + }); + if (k === PUNCH_KIND.IN && today.some((p) => p.kind === PUNCH_KIND.IN)) { + throw new BadRequestException('今天已经打过上班卡'); + } + if (k === PUNCH_KIND.OUT && !today.some((p) => p.kind === PUNCH_KIND.IN)) { + throw new BadRequestException('请先打上班卡'); + } + if (k === PUNCH_KIND.OUT && today.some((p) => p.kind === PUNCH_KIND.OUT)) { + throw new BadRequestException('今天已经打过下班卡'); + } + await this.prisma.attendancePunch.create({ data: { employeeId, kind: k } }); + return this.myPunches(user); + } + + async myPunches(user: AuthUser, date?: string) { + const employeeId = user.employeeId; + if (!employeeId) return { employeeId: null, items: [], clockIn: null, clockOut: null }; + const day = date ? new Date(`${date}T00:00:00`) : new Date(); + day.setHours(0, 0, 0, 0); + const next = new Date(day); + next.setDate(next.getDate() + 1); + const items = await this.prisma.attendancePunch.findMany({ + where: { employeeId, punchedAt: { gte: day, lt: next } }, + orderBy: { punchedAt: 'asc' }, + }); + return { + employeeId, + items, + clockIn: items.find((p) => p.kind === PUNCH_KIND.IN)?.punchedAt || null, + clockOut: [...items].reverse().find((p) => p.kind === PUNCH_KIND.OUT)?.punchedAt || null, + }; + } + + async listPunches(user: AuthUser, yearMonth?: string) { + this.assertManage(user); + const ym = yearMonth ? assertYearMonthSafe(yearMonth) : currentYearMonth(); + const start = new Date(`${ym}-01T00:00:00`); + const end = new Date(start); + end.setMonth(end.getMonth() + 1); + const items = await this.prisma.attendancePunch.findMany({ + where: { punchedAt: { gte: start, lt: end } }, + include: { employee: { select: { id: true, name: true, employeeNo: true, department: { select: { name: true } } } } }, + orderBy: { punchedAt: 'desc' }, + }); + return { yearMonth: ym, items }; + } + + async compBalance(user: AuthUser, employeeId?: string) { + const id = employeeId && canManageHr(user.roles, user.permissions) ? employeeId : user.employeeId; + if (!id) { + return { + hours: 0, + days: 0, + usableHours: 0, + usableDays: 0, + pendingCompDays: 0, + pendingOvertimeHours: 0, + employmentStatus: null, + probationEndAt: null, + canRegularize: false, + }; + } + const emp = await this.assertEmployee(id); + const pending = await this.prisma.leaveRequest.findMany({ + where: { employeeId: id, status: LEAVE_STATUS.PENDING, kind: { in: [LEAVE_KIND.COMP, HR_APPLY_KIND.OVERTIME] } }, + select: { kind: true, days: true }, + }); + const pendingCompDays = pending.filter((p) => p.kind === LEAVE_KIND.COMP).reduce((s, p) => s + n(p.days), 0); + const pendingOvertimeHours = pending + .filter((p) => p.kind === HR_APPLY_KIND.OVERTIME) + .reduce((s, p) => s + n(p.days), 0); + const hours = n(emp.compLeaveHours); + const usableHours = Math.max(0, Math.round((hours - pendingCompDays * HOURS_PER_DAY) * 100) / 100); + return { + hours, + days: Math.round((hours / HOURS_PER_DAY) * 100) / 100, + usableHours, + usableDays: Math.round((usableHours / HOURS_PER_DAY) * 100) / 100, + pendingCompDays, + pendingOvertimeHours, + employmentStatus: emp.employmentStatus, + probationEndAt: emp.probationEndAt, + canRegularize: emp.employmentStatus === EMP_STATUS.PROBATION, + }; + } + + async createLeave( + user: AuthUser, + dto: { + kind: string; + startAt?: string; + endAt?: string; + days?: number; + reason: string; + toDeptId?: string; + targetTitle?: string; + punchKind?: string; + }, + ) { + const employeeId = user.employeeId; + if (!employeeId) throw new BadRequestException('当前账号未关联员工档案,不能提交人事申请'); + if (!isHrApplyKind(dto.kind)) throw new BadRequestException('请选择有效的申请类型'); + const kind = dto.kind; + const startAt = new Date(dto.startAt || Date.now()); + const endAt = new Date(dto.endAt || dto.startAt || Date.now()); + if (Number.isNaN(startAt.getTime()) || Number.isNaN(endAt.getTime()) || endAt < startAt) { + throw new BadRequestException('请填写正确的日期'); + } + if (!dto.reason?.trim()) throw new BadRequestException('请填写事由'); + if (kind === HR_APPLY_KIND.TRANSFER) { + if (!dto.toDeptId) throw new BadRequestException('转岗请选择转入部门'); + await this.assertDept(dto.toDeptId); + } + if (kind === HR_APPLY_KIND.PROMOTE && !dto.targetTitle?.trim()) { + throw new BadRequestException('晋升请填写拟任职务'); + } + if (kind === HR_APPLY_KIND.MAKEUP && !['IN', 'OUT'].includes(dto.punchKind || '')) { + throw new BadRequestException('补卡请选择上班或下班'); + } + if (kind === HR_APPLY_KIND.OVERTIME && !(dto.days && dto.days > 0)) { + throw new BadRequestException('加班请填写小时数,通过后转入调休'); + } + const needsSpan = isLeaveKind(kind) || kind === HR_APPLY_KIND.BUSINESS || kind === HR_APPLY_KIND.OUT; + const days = + kind === HR_APPLY_KIND.OVERTIME + ? dto.days! + : needsSpan + ? dto.days && dto.days > 0 + ? dto.days + : Math.max(0.5, Math.round(((endAt.getTime() - startAt.getTime()) / 86400000) * 2) / 2) + : 0; + const emp = await this.assertEmployee(employeeId); + if (kind === HR_APPLY_KIND.REGULARIZE) { + if (emp.employmentStatus !== EMP_STATUS.PROBATION) { + throw new BadRequestException('仅试用期员工可申请转正'); + } + const pending = await this.prisma.leaveRequest.findFirst({ + where: { employeeId, kind: HR_APPLY_KIND.REGULARIZE, status: LEAVE_STATUS.PENDING }, + }); + if (pending) throw new BadRequestException('已有待批转正申请'); + } + if (kind === LEAVE_KIND.COMP) { + const bal = await this.compBalance(user); + if (days * HOURS_PER_DAY - bal.usableHours > 0.001) { + throw new BadRequestException( + `调休余额不足:可用 ${bal.usableHours} 小时(${bal.usableDays} 天),本次 ${days} 天`, + ); + } + } + const created = await this.prisma.leaveRequest.create({ + data: { + employeeId, + kind, + startAt, + endAt, + days: new Prisma.Decimal(days), + reason: dto.reason.trim(), + toDeptId: kind === HR_APPLY_KIND.TRANSFER ? dto.toDeptId : null, + targetTitle: + kind === HR_APPLY_KIND.PROMOTE + ? dto.targetTitle?.trim() || null + : kind === HR_APPLY_KIND.MAKEUP + ? dto.punchKind || null + : null, + status: LEAVE_STATUS.PENDING, + }, + }); + const reviewers = await this.leaveReviewers(emp, kind); + if (reviewers.length) { + await this.prisma.approvalTask.createMany({ + data: reviewers.map((id) => ({ + title: `人事申请:${emp.name} ${HR_APPLY_LABEL[kind] || kind}`, + bizType: HR_BIZ.LEAVE, + bizId: created.id, + assigneeId: id, + })), + skipDuplicates: true, + }); + emitSms('HR_LEAVE', { + userIds: reviewers, + params: { content: `人事:${emp.name}${HR_APPLY_LABEL[kind] || kind}` }, + bizType: HR_BIZ.LEAVE, + bizId: created.id, + }); + } + return this.getLeave(created.id, user); + } + + async deleteLeave(id: string, user: AuthUser) { + const row = await this.prisma.leaveRequest.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('申请不存在'); + const mine = Boolean(user.employeeId && row.employeeId === user.employeeId); + if (!mine && !canManageHr(user.roles, user.permissions)) { + throw new ForbiddenException('只能删除自己的人事申请'); + } + if (row.status !== LEAVE_STATUS.PENDING) { + throw new BadRequestException('已审批的申请不能删除'); + } + await this.prisma.$transaction(async (tx) => { + await tx.approvalTask.updateMany({ + where: { bizId: id, bizType: HR_BIZ.LEAVE, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + await tx.leaveRequest.delete({ where: { id } }); + }); + return { ok: true }; + } + + async listLeaves(user: AuthUser, bucket?: string) { + const where: Prisma.LeaveRequestWhereInput = { tenantId: '1' }; + if (!canManageHr(user.roles, user.permissions) || bucket === 'mine') { + if (!user.employeeId) return { items: [], balance: await this.compBalance(user) }; + where.employeeId = user.employeeId; + } + if (bucket === 'pending') where.status = LEAVE_STATUS.PENDING; + else if (bucket === 'approved') where.status = LEAVE_STATUS.APPROVED; + else if (bucket === 'rejected') where.status = LEAVE_STATUS.REJECTED; + const items = await this.prisma.leaveRequest.findMany({ + where, + include: { + employee: { select: { id: true, name: true, employeeNo: true, department: { select: { name: true } } } }, + decidedBy: { select: { id: true, displayName: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + const balance = user.employeeId ? await this.compBalance(user) : null; + return { items, balance }; + } + + async reviewLeave(id: string, user: AuthUser, dto: { result: 'APPROVED' | 'REJECTED'; comment?: string }) { + const assigned = await this.prisma.approvalTask.findFirst({ + where: { bizType: HR_BIZ.LEAVE, bizId: id, assigneeId: user.id }, + }); + if (!canManageHr(user.roles, user.permissions) && !assigned) { + throw new ForbiddenException('无权审批该人事申请'); + } + const row = await this.prisma.leaveRequest.findFirst({ + where: { id, tenantId: '1' }, + include: { employee: true }, + }); + if (!row) throw new NotFoundException('申请不存在'); + if (row.status !== LEAVE_STATUS.PENDING) throw new BadRequestException('该申请已处理'); + const status = dto.result === 'APPROVED' ? LEAVE_STATUS.APPROVED : LEAVE_STATUS.REJECTED; + await this.prisma.$transaction(async (tx) => { + await tx.leaveRequest.update({ + where: { id }, + data: { status, comment: dto.comment?.trim() || null, decidedById: user.id, decidedAt: new Date() }, + }); + await tx.approvalTask.updateMany({ + where: { bizType: HR_BIZ.LEAVE, bizId: id, status: 'PENDING' }, + data: { status: dto.result }, + }); + if (status !== LEAVE_STATUS.APPROVED) return; + const ym = row.startAt.toISOString().slice(0, 7); + const month = await tx.attendanceMonth.findUnique({ + where: { employeeId_yearMonth: { employeeId: row.employeeId, yearMonth: ym } }, + }); + if (row.kind === HR_APPLY_KIND.OVERTIME) { + await this.writeAttendance(tx, user.id, row.employee, ym, { + overtimeHours: n(month?.overtimeHours) + Number(row.days), + remark: '加班申请转入调休', + }); + } else if (row.kind === LEAVE_KIND.COMP) { + await this.writeAttendance(tx, user.id, row.employee, ym, { + compLeaveDays: n(month?.compLeaveDays) + Number(row.days), + remark: '调休申请扣减余额', + }); + } else if (leavePayBucket(row.kind) === 'unpaid' || leavePayBucket(row.kind) === 'sick') { + const addLeave = leavePayBucket(row.kind) === 'unpaid' ? Number(row.days) : 0; + const addSick = leavePayBucket(row.kind) === 'sick' ? Number(row.days) : 0; + if (month) { + await tx.attendanceMonth.update({ + where: { id: month.id }, + data: { + leaveDays: { increment: addLeave }, + sickDays: { increment: addSick }, + }, + }); + } else { + await tx.attendanceMonth.create({ + data: { + employeeId: row.employeeId, + yearMonth: ym, + leaveDays: addLeave, + sickDays: addSick, + importedById: user.id, + remark: '请假审批自动计入', + }, + }); + } + } + if (row.kind === HR_APPLY_KIND.MAKEUP) { + await tx.attendancePunch.create({ + data: { + employeeId: row.employeeId, + kind: row.targetTitle === PUNCH_KIND.OUT ? PUNCH_KIND.OUT : PUNCH_KIND.IN, + punchedAt: row.startAt, + remark: `补卡:${row.reason}`, + }, + }); + } + if ( + row.kind === HR_APPLY_KIND.TRANSFER || + row.kind === HR_APPLY_KIND.RESIGN || + row.kind === HR_APPLY_KIND.PROMOTE || + row.kind === HR_APPLY_KIND.REGULARIZE + ) { + const lifeKind = + row.kind === HR_APPLY_KIND.TRANSFER + ? LIFE_KIND.TRANSFER + : row.kind === HR_APPLY_KIND.RESIGN + ? LIFE_KIND.LEAVE + : row.kind === HR_APPLY_KIND.REGULARIZE + ? LIFE_KIND.REGULARIZE + : LIFE_KIND.PROMOTE; + const nextStatus = nextStatusForKind(lifeKind, row.employee.employmentStatus); + const toDeptId = row.kind === HR_APPLY_KIND.TRANSFER ? row.toDeptId || row.employee.departmentId : row.employee.departmentId; + await tx.employmentEvent.create({ + data: { + tenantId: '1', + employeeId: row.employeeId, + kind: lifeKind, + fromStatus: row.employee.employmentStatus, + toStatus: nextStatus, + fromDeptId: row.employee.departmentId, + toDeptId, + effectiveAt: row.startAt, + comment: row.reason, + actorId: user.id, + }, + }); + await tx.employee.update({ + where: { id: row.employeeId }, + data: { + employmentStatus: nextStatus || row.employee.employmentStatus, + departmentId: toDeptId, + jobRole: row.kind === HR_APPLY_KIND.PROMOTE ? row.targetTitle : undefined, + leftAt: row.kind === HR_APPLY_KIND.RESIGN ? row.startAt : undefined, + probationEndAt: row.kind === HR_APPLY_KIND.REGULARIZE ? row.startAt : undefined, + }, + }); + } + if (row.kind === HR_APPLY_KIND.BUSINESS) { + const applicant = await tx.user.findFirst({ + where: { employeeId: row.employeeId, tenantId: '1' }, + }); + if (applicant) { + const claim = await tx.expenseClaim.create({ + data: { + tenantId: '1', + claimNo: await nextExpenseClaimNo(tx), + applicantId: applicant.id, + costType: 'ADMIN', + purpose: `出差报销:${row.reason || ''}`.trim(), + amount: 0, + remark: `人事出差申请通过后自动生成草稿,请补金额和票据后提交`, + status: 'DRAFT', + }, + }); + await tx.todoTask.create({ + data: { + tenantId: '1', + title: '填写出差报销', + bizType: 'TRAVEL_EXPENSE', + bizId: claim.id, + assigneeId: applicant.id, + }, + }); + } + } + }); + return this.getLeave(id, user); + } + + async decideLeaveApproval(taskId: string, user: AuthUser, dto: { result: 'APPROVED' | 'REJECTED'; comment?: string }) { + const task = await this.prisma.approvalTask.findFirst({ where: { id: taskId, assigneeId: user.id } }); + if (!task?.bizId) throw new NotFoundException('审批不存在'); + return this.reviewLeave(task.bizId, user, dto); + } + + async getLeave(id: string, user: AuthUser) { + const row = await this.prisma.leaveRequest.findFirst({ + where: { id, tenantId: '1' }, + include: { + employee: { select: { id: true, name: true, employeeNo: true, department: { select: { name: true } } } }, + decidedBy: { select: { id: true, displayName: true } }, + }, + }); + if (!row) throw new NotFoundException('请假单不存在'); + const assigned = await this.prisma.approvalTask.findFirst({ + where: { bizType: HR_BIZ.LEAVE, bizId: id, assigneeId: user.id }, + }); + if (!canManageHr(user.roles, user.permissions) && row.employeeId !== user.employeeId && !assigned) { + throw new ForbiddenException('无权查看该请假'); + } + return { ...row, days: Number(row.days) }; + } + + private async deptDirectorUserIds(departmentId: string, selfEmpId?: string) { + const chain = await ancestorDeptIds(this.prisma, departmentId); + if (!chain.length) return [] as string[]; + const inChain = await this.prisma.employee.findMany({ + where: { tenantId: '1', departmentId: { in: chain } }, + include: EMP_INCLUDE, + }); + const allowed: string[] = []; + for (const deptId of chain) { + allowed.push(deptId); + if (inChain.some((e) => e.departmentId === deptId && e.id !== selfEmpId && isDirectorLead(e))) break; + } + const allow = new Set(allowed); + const empIds = inChain + .filter((e) => allow.has(e.departmentId) && e.id !== selfEmpId && isDirectorLead(e)) + .map((e) => e.id); + if (!empIds.length) return []; + const users = await this.prisma.user.findMany({ + where: { employeeId: { in: empIds }, status: 'ACTIVE' }, + select: { id: true }, + }); + return users.map((u) => u.id); + } + + private async leaveReviewers( + emp: { id: string; departmentId: string; managerId?: string | null; managerIds?: unknown }, + kind?: string, + ) { + const ids = new Set(); + const mgrEmpIds = managerIdList(emp); + if (mgrEmpIds.length) { + const mgrs = await this.prisma.user.findMany({ + where: { employeeId: { in: mgrEmpIds }, status: 'ACTIVE' }, + select: { id: true }, + }); + for (const mgr of mgrs) ids.add(mgr.id); + } + for (const id of await this.deptDirectorUserIds(emp.departmentId, emp.id)) ids.add(id); + const needHr = ['REGULARIZE', 'PROMOTE', 'TRANSFER', 'RESIGN'].includes(kind || ''); + if (needHr || !ids.size) { + const hrs = await this.prisma.user.findMany({ + where: { status: 'ACTIVE', roles: { some: { role: { code: { in: needHr ? ['hr', 'owner'] : ['hr'] } } } } }, + select: { id: true }, + }); + for (const hr of hrs) ids.add(hr.id); + } + return [...ids]; + } + + async getPayroll(user: AuthUser, yearMonth?: string) { + this.assertViewPayroll(user); + const ym = yearMonth ? assertYearMonthSafe(yearMonth) : currentYearMonth(); + const run = await this.prisma.payrollRun.findFirst({ + where: { tenantId: '1', yearMonth: ym }, + include: { + items: { + include: { employee: { include: { department: true } } }, + orderBy: { employee: { employeeNo: 'asc' } }, + }, + }, + }); + if (!run) return { yearMonth: ym, run: null, items: [] }; + const items = this.filterPayrollItems(user, run.items).map((i) => this.serializeItem(i)); + return { yearMonth: ym, run: { id: run.id, status: run.status, confirmedAt: run.confirmedAt, createdAt: run.createdAt }, items }; + } + + async runPayroll(user: AuthUser, dto: { yearMonth?: string }) { + this.assertManage(user); + const ym = dto.yearMonth ? assertYearMonthSafe(dto.yearMonth) : currentYearMonth(); + return this.prisma.$transaction(async (tx) => { + let run = await tx.payrollRun.findFirst({ where: { tenantId: '1', yearMonth: ym } }); + if (run?.status === PAYROLL_STATUS.CONFIRMED) { + throw new BadRequestException('本月薪酬已确认,不能重算'); + } + if (!run) { + run = await tx.payrollRun.create({ + data: { tenantId: '1', yearMonth: ym, status: PAYROLL_STATUS.DRAFT, createdById: user.id }, + }); + } + const existing = await tx.payrollItem.findMany({ where: { runId: run.id } }); + const ratioMap = new Map(existing.map((i) => [i.employeeId, n(i.performanceRatio)])); + await tx.payrollItem.deleteMany({ where: { runId: run.id } }); + const depts = await tx.department.findMany({ where: { tenantId: '1' }, select: { id: true, name: true, parentId: true } }); + const byId = new Map(depts.map((d) => [d.id, d])); + const skipPayroll = (departmentId: string) => { + const seen = new Set(); + let cur = byId.get(departmentId); + while (cur && !seen.has(cur.id)) { + seen.add(cur.id); + if (cur.name === '外协部' || cur.name === '保障部') return true; + cur = cur.parentId ? byId.get(cur.parentId) : undefined; + } + return false; + }; + const employees = await tx.employee.findMany({ where: { tenantId: '1' } }); + const attendance = await tx.attendanceMonth.findMany({ where: { tenantId: '1', yearMonth: ym } }); + const attMap = new Map(attendance.map((a) => [a.employeeId, a])); + const regs = await tx.employmentEvent.findMany({ + where: { tenantId: '1', kind: LIFE_KIND.REGULARIZE }, + orderBy: { effectiveAt: 'desc' }, + }); + const regMap = new Map(); + for (const e of regs) { + if (!regMap.has(e.employeeId)) regMap.set(e.employeeId, e.effectiveAt); + } + for (const emp of employees) { + if (skipPayroll(emp.departmentId)) continue; + const factor = prorationFactor({ hiredAt: emp.hiredAt, leftAt: emp.leftAt, yearMonth: ym }); + if (factor <= 0) continue; + const att = attMap.get(emp.id); + const overtime = n(att?.overtimeHours); + const usedComp = n(att?.compLeaveDays); + const prevBalance = n(emp.compLeaveHours) - overtime + usedComp * HOURS_PER_DAY; + const slip = computePayslip({ + baseSalary: n(emp.baseSalary), + postSalary: n(emp.postSalary), + attendanceBonus: n(emp.attendanceBonus), + phoneSubsidy: n(emp.phoneSubsidy), + transportSubsidy: n(emp.transportSubsidy), + mealSubsidy: n(emp.mealSubsidy), + probationSalary: n(emp.probationSalary), + perfBase: n(emp.perfBase), + socialBase: n(emp.socialBase), + restSchedule: emp.restSchedule, + employmentStatus: emp.employmentStatus, + hiredAt: emp.hiredAt, + leftAt: emp.leftAt, + regularizeAt: regMap.get(emp.id) || null, + yearMonth: ym, + leaveDays: n(att?.leaveDays), + sickDays: n(att?.sickDays), + overtimeHours: overtime, + compLeaveDays: usedComp, + performanceRatio: ratioMap.get(emp.id) ?? 100, + prevCompLeaveHours: Math.max(0, prevBalance), + }); + await tx.payrollItem.create({ + data: { + runId: run.id, + employeeId: emp.id, + prorationFactor: slip.prorationFactor, + scheduledDays: slip.scheduledDays, + dailyRate: slip.dailyRate, + basePay: slip.basePay, + performancePay: slip.performancePay, + performanceRatio: slip.performanceRatio, + leaveDeduct: slip.leaveDeduct, + sickDeduct: slip.sickDeduct, + socialPersonal: slip.socialPersonal, + housingFund: slip.housingFund, + tax: slip.tax, + grossPay: slip.grossPay, + netPay: slip.netPay, + overtimeHours: slip.overtimeHours, + compLeaveUsedDays: slip.compLeaveUsedDays, + compLeaveHours: slip.compLeaveHours, + }, + }); + } + const full = await tx.payrollRun.findUniqueOrThrow({ + where: { id: run.id }, + include: { items: { include: { employee: { include: { department: true } } }, orderBy: { employee: { employeeNo: 'asc' } } } }, + }); + return { + yearMonth: ym, + run: { id: full.id, status: full.status, confirmedAt: full.confirmedAt, createdAt: full.createdAt }, + items: full.items.map((i) => this.serializeItem(i)), + }; + }); + } + + async confirmPayroll(id: string, user: AuthUser) { + if (!canConfirmPayroll(user.roles)) throw new ForbiddenException('无权确认薪酬'); + const run = await this.prisma.payrollRun.findFirst({ where: { id, tenantId: '1' } }); + if (!run) throw new NotFoundException('核算批次不存在'); + if (run.status === PAYROLL_STATUS.CONFIRMED) throw new BadRequestException('本月已确认'); + const updated = await this.prisma.payrollRun.update({ + where: { id }, + data: { status: PAYROLL_STATUS.CONFIRMED, confirmedAt: new Date() }, + }); + return { id: updated.id, yearMonth: updated.yearMonth, status: updated.status, confirmedAt: updated.confirmedAt }; + } + + async unconfirmPayroll(id: string, user: AuthUser) { + if (!canConfirmPayroll(user.roles)) throw new ForbiddenException('无权取消确认'); + const run = await this.prisma.payrollRun.findFirst({ where: { id, tenantId: '1' } }); + if (!run) throw new NotFoundException('核算批次不存在'); + if (run.status !== PAYROLL_STATUS.CONFIRMED) throw new BadRequestException('本月尚未确认'); + const updated = await this.prisma.payrollRun.update({ + where: { id }, + data: { status: PAYROLL_STATUS.DRAFT, confirmedAt: null }, + }); + return { id: updated.id, yearMonth: updated.yearMonth, status: updated.status, confirmedAt: updated.confirmedAt }; + } + + async adjustPerformance(id: string, user: AuthUser, dto: { performanceRatio: number }) { + if (!canAdjustPerformance(user.roles)) throw new ForbiddenException('无权调整绩效比例'); + const ratio = Number(dto.performanceRatio); + if (!Number.isFinite(ratio) || ratio < 0 || ratio > 200) throw new BadRequestException('发放比例须在 0 到 200 之间'); + const item = await this.prisma.payrollItem.findFirst({ + where: { id }, + include: { run: true, employee: { include: { department: true } } }, + }); + if (!item) throw new NotFoundException('工资条不存在'); + this.assertCanAdjustEmployee(user, item.employee); + if (item.run.status === PAYROLL_STATUS.CONFIRMED) { + await this.prisma.payrollRun.update({ + where: { id: item.runId }, + data: { status: PAYROLL_STATUS.DRAFT, confirmedAt: null }, + }); + } + const att = await this.prisma.attendanceMonth.findFirst({ + where: { employeeId: item.employeeId, yearMonth: item.run.yearMonth }, + }); + const emp = item.employee; + const overtime = n(att?.overtimeHours); + const usedComp = n(att?.compLeaveDays); + const prevBalance = n(emp.compLeaveHours) - overtime + usedComp * HOURS_PER_DAY; + const reg = await this.prisma.employmentEvent.findFirst({ + where: { employeeId: emp.id, kind: LIFE_KIND.REGULARIZE }, + orderBy: { effectiveAt: 'desc' }, + }); + const slip = computePayslip({ + baseSalary: n(emp.baseSalary), + postSalary: n(emp.postSalary), + attendanceBonus: n(emp.attendanceBonus), + phoneSubsidy: n(emp.phoneSubsidy), + transportSubsidy: n(emp.transportSubsidy), + mealSubsidy: n(emp.mealSubsidy), + probationSalary: n(emp.probationSalary), + perfBase: n(emp.perfBase), + socialBase: n(emp.socialBase), + restSchedule: emp.restSchedule, + employmentStatus: emp.employmentStatus, + hiredAt: emp.hiredAt, + leftAt: emp.leftAt, + regularizeAt: reg?.effectiveAt || null, + yearMonth: item.run.yearMonth, + leaveDays: n(att?.leaveDays), + sickDays: n(att?.sickDays), + overtimeHours: overtime, + compLeaveDays: usedComp, + performanceRatio: ratio, + prevCompLeaveHours: Math.max(0, prevBalance), + }); + const updated = await this.prisma.payrollItem.update({ + where: { id }, + data: { + performanceRatio: slip.performanceRatio, + performancePay: slip.performancePay, + grossPay: slip.grossPay, + tax: slip.tax, + netPay: slip.netPay, + socialPersonal: slip.socialPersonal, + housingFund: slip.housingFund, + basePay: slip.basePay, + }, + include: { employee: { include: { department: true } } }, + }); + return this.serializeItem(updated); + } + + private assertLabor(user: AuthUser) { + if (canManageHr(user.roles, user.permissions) || canAccessContract(user.roles, user.permissions)) return; + throw new ForbiddenException('无权访问人事合同'); + } + + private laborInclude() { + return { + employee: { include: { department: true } }, + createdBy: { select: { id: true, displayName: true, username: true } }, + } as const; + } + + async listLaborContracts(user: AuthUser, keyword?: string) { + this.assertLabor(user); + const where: Prisma.LaborContractWhereInput = { tenantId: '1' }; + if (keyword?.trim()) { + where.OR = [ + { contractNo: { contains: keyword.trim() } }, + { remark: { contains: keyword.trim() } }, + { employee: { name: { contains: keyword.trim() } } }, + { employee: { employeeNo: { contains: keyword.trim() } } }, + ]; + } + const rows = await this.prisma.laborContract.findMany({ + where, + include: this.laborInclude(), + orderBy: { createdAt: 'desc' }, + }); + const files = await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizType: 'EMPLOYEE_LABOR', bizId: { in: rows.map((r) => r.id) } }, + orderBy: { createdAt: 'asc' }, + }); + const byId = new Map(); + for (const f of files) { + const list = byId.get(f.bizId) || []; + list.push(f); + byId.set(f.bizId, list); + } + const employees = await this.prisma.employee.findMany({ + where: { tenantId: '1' }, + include: { department: true }, + orderBy: { employeeNo: 'asc' }, + }); + return { + items: rows.map((r) => ({ ...r, files: byId.get(r.id) || [] })), + employees: employees.map((e) => ({ + id: e.id, + name: e.name, + employeeNo: e.employeeNo, + department: e.department?.name, + contractStartAt: e.contractStartAt, + contractEndAt: e.contractEndAt, + })), + total: rows.length, + }; + } + + async getLaborContract(id: string, user: AuthUser) { + this.assertLabor(user); + const row = await this.prisma.laborContract.findFirst({ + where: { id, tenantId: '1' }, + include: this.laborInclude(), + }); + if (!row) throw new NotFoundException('人事合同不存在'); + const files = await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizType: 'EMPLOYEE_LABOR', bizId: id }, + orderBy: { createdAt: 'asc' }, + }); + return { ...row, files }; + } + + async createLaborContract( + user: AuthUser, + dto: { employeeId: string; contractNo?: string; startAt?: string; endAt?: string; remark?: string }, + ) { + this.assertLabor(user); + if (!dto.employeeId) throw new BadRequestException('请选择员工'); + const emp = await this.assertEmployee(dto.employeeId); + const startAt = dto.startAt ? new Date(dto.startAt) : emp.contractStartAt; + const endAt = dto.endAt ? new Date(dto.endAt) : emp.contractEndAt; + const created = await this.prisma.laborContract.create({ + data: { + tenantId: '1', + employeeId: emp.id, + contractNo: dto.contractNo?.trim() || null, + startAt, + endAt, + remark: dto.remark?.trim() || null, + createdById: user.id, + }, + }); + if (startAt || endAt) { + await this.prisma.employee.update({ + where: { id: emp.id }, + data: { + contractStartAt: startAt ?? emp.contractStartAt, + contractEndAt: endAt ?? emp.contractEndAt, + }, + }); + } + return this.getLaborContract(created.id, user); + } + + async updateLaborContract( + id: string, + user: AuthUser, + dto: { contractNo?: string; startAt?: string | null; endAt?: string | null; remark?: string }, + ) { + this.assertLabor(user); + const row = await this.prisma.laborContract.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('人事合同不存在'); + const startAt = dto.startAt === undefined ? row.startAt : dto.startAt ? new Date(dto.startAt) : null; + const endAt = dto.endAt === undefined ? row.endAt : dto.endAt ? new Date(dto.endAt) : null; + await this.prisma.laborContract.update({ + where: { id }, + data: { + contractNo: dto.contractNo === undefined ? row.contractNo : dto.contractNo.trim() || null, + startAt, + endAt, + remark: dto.remark === undefined ? row.remark : dto.remark.trim() || null, + }, + }); + await this.prisma.employee.update({ + where: { id: row.employeeId }, + data: { contractStartAt: startAt, contractEndAt: endAt }, + }); + return this.getLaborContract(id, user); + } + + async deleteLaborContract(id: string, user: AuthUser) { + this.assertLabor(user); + const row = await this.prisma.laborContract.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('人事合同不存在'); + await this.prisma.fileAsset.deleteMany({ where: { tenantId: '1', bizType: 'EMPLOYEE_LABOR', bizId: id } }); + await this.prisma.laborContract.delete({ where: { id } }); + return { ok: true }; + } + + private async writeAttendance( + tx: Tx, + userId: string, + emp: { id: string; compLeaveHours: Prisma.Decimal }, + ym: string, + dto: { + workDays?: number; + leaveDays?: number; + sickDays?: number; + overtimeHours?: number; + compLeaveDays?: number; + remark?: string; + }, + ) { + const existing = await tx.attendanceMonth.findUnique({ + where: { employeeId_yearMonth: { employeeId: emp.id, yearMonth: ym } }, + }); + const next = { + workDays: numOr(dto.workDays, existing ? n(existing.workDays) : 0), + leaveDays: numOr(dto.leaveDays, existing ? n(existing.leaveDays) : 0), + sickDays: numOr(dto.sickDays, existing ? n(existing.sickDays) : 0), + overtimeHours: numOr(dto.overtimeHours, existing ? n(existing.overtimeHours) : 0), + compLeaveDays: numOr(dto.compLeaveDays, existing ? n(existing.compLeaveDays) : 0), + }; + const oldOt = existing ? n(existing.overtimeHours) : 0; + const oldComp = existing ? n(existing.compLeaveDays) : 0; + const current = n(emp.compLeaveHours); + const reversed = current - oldOt + oldComp * HOURS_PER_DAY; + const nextBalance = reversed + next.overtimeHours - next.compLeaveDays * HOURS_PER_DAY; + if (nextBalance < -0.001) throw new BadRequestException('调休余额不足,请少用调休或先导入加班小时'); + const row = existing + ? await tx.attendanceMonth.update({ + where: { id: existing.id }, + data: { ...next, remark: dto.remark?.trim() || existing.remark, importedById: userId, importedAt: new Date() }, + }) + : await tx.attendanceMonth.create({ + data: { + tenantId: '1', + employeeId: emp.id, + yearMonth: ym, + ...next, + remark: dto.remark?.trim() || null, + importedById: userId, + }, + }); + const updatedEmp = await tx.employee.update({ + where: { id: emp.id }, + data: { compLeaveHours: Math.max(0, nextBalance) }, + }); + return { + ...row, + workDays: n(row.workDays), + leaveDays: n(row.leaveDays), + sickDays: n(row.sickDays), + overtimeHours: n(row.overtimeHours), + compLeaveDays: n(row.compLeaveDays), + compLeaveHours: n(updatedEmp.compLeaveHours), + }; + } + + private filterPayrollItems( + user: AuthUser, + items: T[], + ) { + if (canSeeCompanyPayroll(user.roles)) return items; + if (!user.departmentId && !user.employeeId) return items; + return items.filter( + (i) => i.employee.departmentId === user.departmentId || reportsTo(i.employee, user.employeeId), + ); + } + + private assertCanAdjustEmployee(user: AuthUser, emp: { departmentId: string; managerId: string | null; managerIds?: string | null }) { + if (canManageHr(user.roles, user.permissions) || user.roles.includes('biz_director')) return; + const sameDept = user.departmentId && emp.departmentId === user.departmentId; + const isReport = reportsTo(emp, user.employeeId); + if (!sameDept && !isReport) throw new ForbiddenException('只能调整本部门或下属的绩效比例'); + } + + private async assertDept(id: string) { + const row = await this.prisma.department.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new BadRequestException('部门不存在'); + return row; + } + + private async assertEmployee(id: string) { + const row = await this.prisma.employee.findFirst({ where: { id, tenantId: '1' }, include: EMP_INCLUDE }); + if (!row) throw new NotFoundException('员工不存在'); + return row; + } + + private async assertDeptLead(employeeId: string, departmentId: string, selfId?: string) { + if (selfId && employeeId === selfId) throw new BadRequestException('不能把汇报对象设为自己'); + const emp = await this.prisma.employee.findFirst({ + where: { id: employeeId, tenantId: '1' }, + include: EMP_INCLUDE, + }); + if (!emp) throw new BadRequestException('所选人员不存在'); + if (emp.departmentId !== departmentId) { + throw new BadRequestException('只能选择本部门的项目经理或总监'); + } + if (!isDeptLead(emp)) { + throw new BadRequestException('请选择本部门的项目经理或总监'); + } + return emp; + } + + private async assertCcLeads( + dto: { ccDaily?: unknown; ccWeekly?: unknown; ccMonthly?: unknown }, + departmentId: string, + selfId?: string, + managerIds?: string | string[] | null, + ) { + const ids = [ + ...new Set( + (['ccDaily', 'ccWeekly', 'ccMonthly'] as const) + .flatMap((k) => { + const joined = joinPeopleIds(dto[k]); + return joined ? joined.split(',') : []; + }), + ), + ]; + if (!ids.length) return; + const mgrSet = new Set(peopleIdList(managerIds)); + const chain = await ancestorDeptIds(this.prisma, departmentId); + const inChain = await this.prisma.employee.findMany({ + where: { tenantId: '1', departmentId: { in: chain } }, + include: EMP_INCLUDE, + }); + const allowedDeptIds: string[] = []; + for (const deptId of chain) { + allowedDeptIds.push(deptId); + if (inChain.some((e) => e.departmentId === deptId && e.id !== selfId && isDirectorLead(e))) break; + } + const allowed = new Set(allowedDeptIds); + for (const id of ids) { + if (selfId && id === selfId) throw new BadRequestException('抄送人不能选自己'); + if (mgrSet.has(id)) { + throw new BadRequestException('抄送人不能和汇报对象相同,普通员工汇报给项目经理、抄送给总监'); + } + const emp = inChain.find((e) => e.id === id) || (await this.prisma.employee.findFirst({ + where: { id, tenantId: '1' }, + include: EMP_INCLUDE, + })); + if (!emp) throw new BadRequestException('所选抄送人不存在'); + if (!allowed.has(emp.departmentId) || !isDirectorLead(emp)) { + throw new BadRequestException('抄送人只能选本部门或上级部门的总监,不能选项目经理'); + } + } + } + + private async attachManagerNames( + items: T[], + ): Promise<(T & { managerIds: string[]; managerName?: string })[]> { + const allIds = new Set(); + for (const i of items) { + for (const id of managerIdList(i)) allIds.add(id); + } + if (!allIds.size) { + return items.map((i) => ({ ...i, managerIds: managerIdList(i), managerName: i.managerName || undefined })); + } + const people = await this.prisma.employee.findMany({ + where: { id: { in: [...allIds] } }, + select: { id: true, name: true }, + }); + const names = new Map(people.map((p) => [p.id, p.name])); + return items.map((i) => { + const ordered = managerIdList(i); + return { + ...i, + managerIds: ordered, + managerName: ordered.map((id) => names.get(id)).filter(Boolean).join('、') || i.managerName || undefined, + }; + }); + } + + private async attachPhotos( + items: Array & { id: string }>, + ) { + const userIds = items.flatMap((i) => (i.accounts || []).map((a) => a.id)); + const empIds = items.map((i) => String(i.id)); + if (!userIds.length && !empIds.length) return items; + const files = await this.prisma.fileAsset.findMany({ + where: { + tenantId: '1', + OR: [ + userIds.length ? { bizType: 'USER_ID_PHOTO', bizId: { in: userIds } } : undefined, + empIds.length + ? { bizType: { in: ['EMPLOYEE_ID_PHOTO', 'EMPLOYEE_ID_FRONT', 'EMPLOYEE_ID_BACK', 'EMPLOYEE_CERT'] }, bizId: { in: empIds } } + : undefined, + ].filter(Boolean) as Prisma.FileAssetWhereInput[], + }, + orderBy: { createdAt: 'desc' }, + }); + const latest = new Map(); + const certs = new Map(); + for (const f of files) { + const key = `${f.bizType}:${f.bizId}`; + if (!latest.has(key)) latest.set(key, f.id); + if (f.bizType === 'EMPLOYEE_CERT') { + const list = certs.get(f.bizId) || []; + list.push({ id: f.id, fileName: f.fileName, size: f.size, mimeType: f.mimeType, bizType: f.bizType }); + certs.set(f.bizId, list); + } + } + return items.map((i) => { + const fromUser = (i.accounts || []).map((a) => latest.get(`USER_ID_PHOTO:${a.id}`)).find(Boolean); + return { + ...i, + userId: i.accounts?.[0]?.id || null, + idPhotoFileId: fromUser || latest.get(`EMPLOYEE_ID_PHOTO:${String(i.id)}`) || null, + idFrontFileId: latest.get(`EMPLOYEE_ID_FRONT:${String(i.id)}`) || null, + idBackFileId: latest.get(`EMPLOYEE_ID_BACK:${String(i.id)}`) || null, + certFiles: certs.get(String(i.id)) || [], + }; + }); + } + + private serializeEmployee( + row: { + baseSalary: Prisma.Decimal; + postSalary?: Prisma.Decimal; + attendanceBonus?: Prisma.Decimal; + phoneSubsidy?: Prisma.Decimal; + transportSubsidy?: Prisma.Decimal; + mealSubsidy?: Prisma.Decimal; + probationSalary?: Prisma.Decimal; + perfBase: Prisma.Decimal; + socialBase: Prisma.Decimal; + compLeaveHours: Prisma.Decimal; + department?: { name: string } | null; + manager?: { name: string } | null; + users?: { id: string; username: string; status: string; roles?: { role: { code: string } }[] }[]; + mobile?: string | null; + idNo?: string | null; + bankAccount?: string | null; + } & Record, + maskSensitive = false, + ) { + const users = row.users || []; + return { + ...row, + id: String((row as unknown as { id: string }).id), + baseSalary: n(row.baseSalary), + postSalary: n(row.postSalary), + attendanceBonus: n(row.attendanceBonus), + phoneSubsidy: n(row.phoneSubsidy), + transportSubsidy: n(row.transportSubsidy), + mealSubsidy: n(row.mealSubsidy), + probationSalary: n(row.probationSalary), + perfBase: n(row.perfBase), + socialBase: n(row.socialBase), + compLeaveHours: n(row.compLeaveHours), + departmentName: row.department?.name, + managerIds: managerIdList(row as { managerId?: string | null; managerIds?: unknown }), + managerName: row.manager?.name, + accountUsername: users[0]?.username || null, + accountRoles: accountRolesOf(users), + accounts: users, + mobile: row.mobile, + idNo: maskSensitive ? maskValue(row.idNo) : row.idNo, + bankAccount: maskSensitive ? maskValue(row.bankAccount) : row.bankAccount, + }; + } + + private serializeItem(row: { + employee: { name: string; employeeNo: string; restSchedule: string; employmentStatus: string; department?: { name: string } | null }; + prorationFactor: Prisma.Decimal; + scheduledDays: Prisma.Decimal; + dailyRate: Prisma.Decimal; + basePay: Prisma.Decimal; + performancePay: Prisma.Decimal; + performanceRatio: Prisma.Decimal; + leaveDeduct: Prisma.Decimal; + sickDeduct: Prisma.Decimal; + socialPersonal: Prisma.Decimal; + housingFund: Prisma.Decimal; + tax: Prisma.Decimal; + grossPay: Prisma.Decimal; + netPay: Prisma.Decimal; + overtimeHours: Prisma.Decimal; + compLeaveUsedDays: Prisma.Decimal; + compLeaveHours: Prisma.Decimal; + id: string; + employeeId: string; + }) { + return { + id: row.id, + employeeId: row.employeeId, + name: row.employee.name, + employeeNo: row.employee.employeeNo, + departmentName: row.employee.department?.name, + restSchedule: row.employee.restSchedule, + employmentStatus: row.employee.employmentStatus, + prorationFactor: n(row.prorationFactor), + scheduledDays: n(row.scheduledDays), + dailyRate: n(row.dailyRate), + basePay: n(row.basePay), + performancePay: n(row.performancePay), + performanceRatio: n(row.performanceRatio), + leaveDeduct: n(row.leaveDeduct), + sickDeduct: n(row.sickDeduct), + socialPersonal: n(row.socialPersonal), + housingFund: n(row.housingFund), + tax: n(row.tax), + grossPay: n(row.grossPay), + netPay: n(row.netPay), + overtimeHours: n(row.overtimeHours), + compLeaveUsedDays: n(row.compLeaveUsedDays), + compLeaveHours: n(row.compLeaveHours), + }; + } +} + +function assertYearMonthSafe(ym: string) { + try { + return assertYearMonth(ym); + } catch (e) { + throw new BadRequestException((e as Error).message); + } +} + +function kindLabel(kind: string) { + return HR_APPLY_LABEL[kind] || kind; +} + +function numOr(value: number | undefined, fallback: number) { + if (value === undefined || value === null || Number.isNaN(Number(value))) return fallback; + return Math.max(0, Number(value)); +} diff --git a/apps/api/src/im/im-bus.ts b/apps/api/src/im/im-bus.ts new file mode 100644 index 0000000..e62d39a --- /dev/null +++ b/apps/api/src/im/im-bus.ts @@ -0,0 +1,22 @@ +export type ImPushPayload = { + userIds: string[]; + kind: 'chat' | 'todo' | 'approval'; + title: string; + body?: string; + bizType?: string; + bizId?: string; + fromUserId?: string; +}; + +type ImHandler = (payload: ImPushPayload) => Promise; + +let handler: ImHandler | null = null; + +export function registerImHandler(fn: ImHandler) { + handler = fn; +} + +export function emitIm(payload: ImPushPayload) { + if (!handler || !payload.userIds?.length) return; + void handler(payload).catch(() => undefined); +} diff --git a/apps/api/src/im/im.controller.ts b/apps/api/src/im/im.controller.ts new file mode 100644 index 0000000..9d30ff9 --- /dev/null +++ b/apps/api/src/im/im.controller.ts @@ -0,0 +1,202 @@ +import { Body, Controller, Get, Headers, Param, Patch, Post, Query, UnauthorizedException } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Public } from '../common/decorators/auth.decorators'; +import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator'; +import { SkipThrottle } from '@nestjs/throttler'; +import { ImService } from './im.service'; + +@ApiTags('im') +@ApiBearerAuth() +@SkipThrottle() +@Controller('im') +export class ImController { + constructor(private readonly im: ImService) {} + + @Get('status') + status() { + return this.im.status(); + } + + @Get('conversations') + conversations(@CurrentUser() user: AuthUser) { + return this.im.conversations(user); + } + + @Get('messages') + messages( + @CurrentUser() user: AuthUser, + @Query('peerId') peerId?: string, + @Query('conversationId') conversationId?: string, + @Query('sinceSeq') sinceSeq?: string, + ) { + const seq = sinceSeq ? Number(sinceSeq) : undefined; + return this.im.messages(user, peerId, conversationId, seq && seq > 0 ? seq : undefined); + } + + @Post('messages') + send( + @CurrentUser() user: AuthUser, + @Body() + body: { + peerId?: string; + conversationId?: string; + body?: string; + fingerprint?: string; + contentType?: string; + meta?: unknown; + kind?: string; + }, + ) { + return this.im.sendChat(user, body); + } + + @Patch('messages/:id') + edit( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { body?: string }, + ) { + return this.im.editMessage(user, id, body.body || ''); + } + + @Post('messages/:id/recall') + recall(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.im.recallMessage(user, id); + } + + @Post('groups') + createGroup( + @CurrentUser() user: AuthUser, + @Body() body: { name?: string; memberIds?: string[] }, + ) { + return this.im.createGroup(user, body.name || '', body.memberIds || []); + } + + @Get('groups/:id/members') + groupMembers(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.im.groupMembers(user, id); + } + + @Post('groups/:id/members') + addMembers( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { memberIds?: string[] }, + ) { + return this.im.addMembers(user, id, body.memberIds || []); + } + + @Post('events/screenshot') + screenshot( + @CurrentUser() user: AuthUser, + @Body() body: { conversationId?: string; peerId?: string }, + ) { + return this.im.screenshot(user, body.conversationId, body.peerId); + } + + @Post('read') + markRead( + @CurrentUser() user: AuthUser, + @Body() body: { conversationId?: string; peerId?: string }, + ) { + return this.im.markRead(user, body.conversationId, body.peerId); + } + + @Post('voice/transcribe') + transcribeVoice(@CurrentUser() user: AuthUser, @Body() body: { fileId?: string }) { + return this.im.transcribeVoice(user, body.fileId || ''); + } + + @Get('messages/:id/receipts') + receipts(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.im.receipts(user, id); + } + + @Get('calls/incoming') + incoming(@CurrentUser() user: AuthUser) { + return this.im.incomingCalls(user); + } + + @Post('calls') + createCall( + @CurrentUser() user: AuthUser, + @Body() body: { conversationId?: string; peerId?: string; kind?: string; title?: string }, + ) { + return this.im.createCall(user, body); + } + + @Get('calls/:id') + callStatus(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.im.callStatus(user, id); + } + + @Post('calls/:id/livekit-token') + livekitToken(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.im.livekitToken(user, id); + } + + @Post('calls/:id/signal') + signal( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { toUserId?: string; kind?: string; payload?: unknown }, + ) { + return this.im.postSignal(user, id, body); + } + + @Post('calls/:id/:action') + callAction( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Param('action') action: 'join' | 'reject' | 'end', + ) { + return this.im.callAction(user, id, action); + } + + @Get('calls/:id/signals') + signals( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Query('after') after?: string, + ) { + return this.im.pullSignals(user, id, after); + } + + @Public() + @Get('internal/members/:id') + internalMembers(@Headers('x-im-token') token: string, @Param('id') id: string) { + const expected = process.env.IM_INTERNAL_TOKEN || ''; + if (!expected || token !== expected) throw new UnauthorizedException('IM 内部令牌无效'); + return this.im.internalMembers(id); + } + + @Public() + @Post('internal/ingest') + ingest( + @Headers('x-im-token') token: string, + @Body() + body: { + fromUserId?: string; + toUserId?: string; + conversationId?: string; + kind?: string; + contentType?: string; + body?: string; + meta?: unknown; + fingerprint?: string; + }, + ) { + const expected = process.env.IM_INTERNAL_TOKEN || ''; + if (!expected || token !== expected) throw new UnauthorizedException('IM 内部令牌无效'); + return this.im.ingest({ + fromUserId: body.fromUserId || '', + toUserId: body.toUserId || '', + conversationId: body.conversationId, + kind: body.kind, + contentType: body.contentType, + body: body.body || '', + meta: body.meta, + fingerprint: body.fingerprint || '', + }); + } +} diff --git a/apps/api/src/im/im.module.ts b/apps/api/src/im/im.module.ts new file mode 100644 index 0000000..7b0c4c0 --- /dev/null +++ b/apps/api/src/im/im.module.ts @@ -0,0 +1,15 @@ +import { Global, Module } from '@nestjs/common'; +import { ImController } from './im.controller'; +import { ImService } from './im.service'; +import { PushModule } from '../push/push.module'; +import { VoiceAsrService } from './voice-asr.service'; +import { FilesModule } from '../files/files.module'; + +@Global() +@Module({ + imports: [PushModule, FilesModule], + controllers: [ImController], + providers: [ImService, VoiceAsrService], + exports: [ImService], +}) +export class ImModule {} diff --git a/apps/api/src/im/im.service.ts b/apps/api/src/im/im.service.ts new file mode 100644 index 0000000..3a0768c --- /dev/null +++ b/apps/api/src/im/im.service.ts @@ -0,0 +1,998 @@ +import { BadRequestException, ConflictException, ForbiddenException, Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { randomUUID } from 'crypto'; +import { AccessToken } from 'livekit-server-sdk'; +import { PrismaService } from '../prisma/prisma.service'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { emitIm, registerImHandler, type ImPushPayload } from './im-bus'; +import { PushService } from '../push/push.service'; +import { FilesService } from '../files/files.service'; +import { VoiceAsrService } from './voice-asr.service'; + +function pair(a: string, b: string) { + return a < b ? [a, b] : [b, a]; +} + +function preview(kind: string, contentType: string, body: string) { + if (contentType === 'location') return '[位置]'; + if (contentType === 'call') return '[通话]'; + if (contentType === 'image') return '[图片]'; + if (contentType === 'sticker') return '[表情]'; + if (contentType === 'voice') return '[语音]'; + if (contentType === 'file') return '[文件]'; + if (kind === 'screenshot') return '[截屏通知]'; + return body.slice(0, 80); +} + +async function avatarByUserIds(prisma: PrismaService, userIds: string[]) { + const ids = [...new Set(userIds.filter(Boolean))]; + if (!ids.length) return new Map(); + const rows = await prisma.fileAsset.findMany({ + where: { tenantId: '1', bizType: 'USER_AVATAR', bizId: { in: ids } }, + select: { id: true, bizId: true }, + orderBy: { createdAt: 'desc' }, + }); + const map = new Map(); + for (const r of rows) { + if (!map.has(r.bizId)) map.set(r.bizId, r.id); + } + return map; +} + +@Injectable() +export class ImService implements OnModuleInit { + private readonly log = new Logger(ImService.name); + + constructor( + private readonly prisma: PrismaService, + private readonly push: PushService, + private readonly files: FilesService, + private readonly asr: VoiceAsrService, + ) {} + + onModuleInit() { + registerImHandler((payload) => this.pushEvent(payload)); + } + + async status() { + let online = 0; + try { + const port = Number(process.env.IM_HTTP_PORT || 8902); + const res = await fetch(`http://127.0.0.1:${port}/health`); + if (res.ok) { + const json = (await res.json()) as { online?: number }; + online = Number(json.online || 0); + } + } catch { + /* IM 未启动 */ + } + return { + id: 'channel', + inboxEnabled: '已开启', + mobileImSdk: '已启用', + tcpPort: Number(process.env.IM_TCP_PORT || 8901), + httpPort: Number(process.env.IM_HTTP_PORT || 8902), + online, + stun: 'stun:stun.qq.com:3478', + iceServers: [ + { urls: 'stun:stun.qq.com:3478' }, + { urls: 'stun:stun.miwifi.com:3478' }, + { urls: 'stun:stun.l.google.com:19302' }, + ], + note: '私聊/群聊、已读、定位、音视频信令。客户端先登录 OA,再用 accessToken 连 IM。', + }; + } + + async conversations(user: AuthUser) { + await this.backfillDirectMembers(); + const memberOf = await this.prisma.imConversationMember.findMany({ + where: { userId: user.id, leftAt: null }, + select: { conversationId: true }, + }); + const ids = [...new Set(memberOf.map((m) => m.conversationId))]; + const legacy = await this.prisma.imConversation.findMany({ + where: { + tenantId: '1', + type: 'direct', + OR: [{ userAId: user.id }, { userBId: user.id }], + }, + select: { id: true }, + }); + const allIds = [...new Set([...ids, ...legacy.map((r) => r.id)])]; + if (!allIds.length) return []; + const rows = await this.prisma.imConversation.findMany({ + where: { id: { in: allIds } }, + orderBy: { lastAt: 'desc' }, + take: 200, + }); + const members = await this.prisma.imConversationMember.findMany({ + where: { conversationId: { in: rows.map((r) => r.id) }, leftAt: null }, + }); + const byConv = new Map(); + for (const m of members) { + const list = byConv.get(m.conversationId) || []; + list.push(m.userId); + byConv.set(m.conversationId, list); + } + const peerIds = [ + ...new Set( + rows.flatMap((r) => { + if (r.type === 'group') return []; + const idsIn = byConv.get(r.id) || [r.userAId, r.userBId].filter(Boolean) as string[]; + return idsIn.filter((id) => id && id !== user.id); + }), + ), + ]; + const people = await this.prisma.user.findMany({ + where: { id: { in: [...peerIds, ...rows.map((r) => r.ownerId).filter(Boolean) as string[]] } }, + select: { id: true, displayName: true, username: true }, + }); + const names = new Map(people.map((p) => [p.id, p.displayName || p.username])); + const avatars = await avatarByUserIds(this.prisma, [...peerIds, user.id]); + const cursors = await this.prisma.imReadCursor.findMany({ + where: { userId: user.id, conversationId: { in: rows.map((r) => r.id) } }, + }); + const lastRead = new Map(cursors.map((c) => [c.conversationId, c.lastReadAt])); + const lastReadSeq = new Map(cursors.map((c) => [c.conversationId, c.lastReadSeq])); + const unreadCounts = await Promise.all( + rows.map((r) => + this.prisma.imMessage.count({ + where: { + conversationId: r.id, + fromUserId: { not: user.id }, + seq: { gt: lastReadSeq.get(r.id) || 0 }, + }, + }), + ), + ); + return rows.map((r, i) => { + const memberIds = byConv.get(r.id) || []; + const peerId = + r.type === 'group' + ? '' + : memberIds.find((id) => id !== user.id) || + (r.userAId === user.id ? r.userBId : r.userAId) || + ''; + return { + id: r.id, + type: r.type, + name: r.type === 'group' ? r.name || '群聊' : peerId === '0' ? '系统通知' : names.get(peerId) || '同事', + peerId, + peerName: r.type === 'group' ? r.name || '群聊' : peerId === '0' ? '系统通知' : names.get(peerId) || '同事', + ownerId: r.ownerId, + memberCount: memberIds.length, + lastText: r.lastText, + lastAt: r.lastAt, + unread: unreadCounts[i], + avatarFileId: r.type === 'group' ? null : avatars.get(peerId) || null, + }; + }); + } + + async messages(user: AuthUser, peerId?: string, conversationId?: string, sinceSeq?: number) { + const conv = conversationId + ? await this.requireMember(conversationId, user.id) + : await this.ensureDirect(user.id, peerId || ''); + const gapFill = sinceSeq != null && sinceSeq > 0; + const items = await this.prisma.imMessage.findMany({ + where: { + conversationId: conv.id, + ...(gapFill ? { seq: { gt: sinceSeq } } : {}), + }, + orderBy: { seq: gapFill ? 'asc' : 'desc' }, + take: gapFill ? 500 : 300, + }); + if (!gapFill) items.reverse(); + if (!gapFill) { + await this.prisma.imReadCursor.upsert({ + where: { userId_conversationId: { userId: user.id, conversationId: conv.id } }, + create: { userId: user.id, conversationId: conv.id, lastReadAt: new Date() }, + update: { lastReadAt: new Date() }, + }); + const latestSeq = items.length ? items[items.length - 1].seq : 0; + if (latestSeq > 0) { + await this.prisma.imReadCursor.update({ + where: { userId_conversationId: { userId: user.id, conversationId: conv.id } }, + data: { lastReadSeq: latestSeq }, + }); + } + } + const ids = items.map((m) => m.id); + const created = !gapFill && ids.length + ? await this.prisma.imMessageReceipt.createMany({ + data: ids + .filter((id) => items.find((m) => m.id === id)?.fromUserId !== user.id) + .map((messageId) => ({ messageId, userId: user.id })), + skipDuplicates: true, + }) + : { count: 0 }; + if (created.count > 0) { + const senderIds = [ + ...new Set(items.filter((m) => m.fromUserId && m.fromUserId !== user.id).map((m) => m.fromUserId)), + ]; + await this.fanout(senderIds, { + kind: 'receipt', + conversationId: conv.id, + fromUserId: user.id, + }); + } + const receipts = ids.length + ? await this.prisma.imMessageReceipt.findMany({ where: { messageId: { in: ids } } }) + : []; + const byMsg = new Map(); + for (const r of receipts) { + const list = byMsg.get(r.messageId) || []; + list.push(r.userId); + byMsg.set(r.messageId, list); + } + const memberCount = await this.prisma.imConversationMember.count({ + where: { conversationId: conv.id, leftAt: null }, + }); + const fromIds = [...new Set(items.map((m) => m.fromUserId).filter(Boolean))]; + const senders = fromIds.length + ? await this.prisma.user.findMany({ + where: { id: { in: fromIds } }, + select: { id: true, displayName: true, username: true }, + }) + : []; + const senderNames = new Map(senders.map((p) => [p.id, p.displayName || p.username])); + const avatars = await avatarByUserIds(this.prisma, fromIds); + return { + conversationId: conv.id, + type: conv.type, + name: conv.name, + memberCount, + items: items.map((m) => { + let meta: Record = {}; + try { meta = JSON.parse(m.meta || '{}') as Record; } catch { /* empty */ } + return { + ...m, + recalledAt: meta.recalledAt || null, + fromName: senderNames.get(m.fromUserId) || '', + fromAvatarFileId: avatars.get(m.fromUserId) || null, + readBy: byMsg.get(m.id) || [], + readCount: (byMsg.get(m.id) || []).length, + }; + }), + }; + } + + async markRead(user: AuthUser, conversationId?: string, peerId?: string) { + const conv = conversationId + ? await this.requireMember(conversationId, user.id) + : await this.ensureDirect(user.id, peerId || ''); + const latest = await this.prisma.imMessage.findFirst({ + where: { conversationId: conv.id }, + orderBy: { seq: 'desc' }, + select: { seq: true }, + }); + const lastSeq = latest?.seq || 0; + const others = await this.prisma.imMessage.findMany({ + where: { conversationId: conv.id, NOT: { fromUserId: user.id } }, + select: { id: true, fromUserId: true }, + orderBy: { seq: 'desc' }, + take: 300, + }); + await this.prisma.imReadCursor.upsert({ + where: { userId_conversationId: { userId: user.id, conversationId: conv.id } }, + create: { userId: user.id, conversationId: conv.id, lastReadAt: new Date(), lastReadSeq: lastSeq }, + update: { lastReadAt: new Date(), lastReadSeq: lastSeq }, + }); + await this.forwardTcp(user.id, { + kind: 'readSync', + conversationId: conv.id, + lastReadSeq: lastSeq, + userId: user.id, + }); + if (!others.length) return { count: 0 }; + const created = await this.prisma.imMessageReceipt.createMany({ + data: others.map((m) => ({ messageId: m.id, userId: user.id })), + skipDuplicates: true, + }); + if (created.count > 0) { + await this.fanout( + [...new Set(others.map((m) => m.fromUserId).filter(Boolean))], + { kind: 'receipt', conversationId: conv.id, fromUserId: user.id }, + ); + } + return { count: created.count }; + } + + async transcribeVoice(user: AuthUser, fileId: string) { + if (!fileId) throw new BadRequestException('fileId 无效'); + const { row, stream } = await this.files.download(fileId, user); + if (row.bizType !== 'IM_VOICE') throw new BadRequestException('不是语音附件'); + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + const text = await this.asr.transcribe(Buffer.concat(chunks)); + return { text }; + } + + async sendChat( + user: AuthUser, + dto: { + peerId?: string; + conversationId?: string; + body?: string; + fingerprint?: string; + contentType?: string; + meta?: unknown; + kind?: string; + }, + ) { + const contentType = dto.contentType || 'text'; + const text = (dto.body || '').trim(); + const media = new Set(['image', 'sticker', 'voice', 'location', 'call', 'file', 'business']); + if (contentType === 'text' && !text) throw new BadRequestException('请填写消息'); + if (!media.has(contentType) && contentType !== 'text' && contentType !== 'system' && !text) { + throw new BadRequestException('请填写消息'); + } + const conv = dto.conversationId + ? await this.requireMember(dto.conversationId, user.id) + : await this.ensureDirect(user.id, dto.peerId || ''); + if (contentType === 'business') { + const meta = dto.meta && typeof dto.meta === 'object' ? dto.meta as Record : {}; + const path = String(meta.fetchPath || ''); + if (!path.startsWith('/') || path.includes('..') || path.includes('?')) { + throw new BadRequestException('业务卡片地址无效'); + } + } + const members = await this.memberIds(conv.id); + const peerId = members.find((id) => id !== user.id) || dto.peerId || ''; + const saved = await this.ingest({ + fromUserId: user.id, + toUserId: conv.type === 'group' ? '' : peerId, + conversationId: conv.id, + kind: dto.kind || 'chat', + contentType, + body: text || preview(dto.kind || 'chat', contentType, ''), + meta: dto.meta, + fingerprint: dto.fingerprint || randomUUID(), + }); + const payload = { + kind: dto.kind || 'chat', + contentType, + text: saved.body, + meta: dto.meta || null, + fromUserId: user.id, + fromName: user.displayName, + conversationId: conv.id, + fingerprint: saved.fingerprint, + seq: saved.seq, + }; + await this.fanout(members.filter((id) => id !== user.id), payload); + return saved; + } + + async editMessage(user: AuthUser, id: string, body: string) { + const text = body.trim(); + if (!text) throw new BadRequestException('请填写消息'); + const row = await this.prisma.imMessage.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new BadRequestException('消息不存在'); + if (row.fromUserId !== user.id) throw new ForbiddenException('只能改自己发的消息'); + let recalled = false; + try { + recalled = Boolean(JSON.parse(row.meta || '{}').recalledAt); + } catch { + recalled = false; + } + if (row.contentType !== 'text' && !recalled) throw new BadRequestException('这类消息不能改文字'); + if (!recalled && Date.now() - row.createdAt.getTime() > 2 * 60 * 1000) { + throw new BadRequestException('超过两分钟不能再改'); + } + const next = await this.prisma.imMessage.update({ + where: { id }, + data: { body: text, contentType: 'text', meta: recalled ? '' : row.meta }, + }); + await this.prisma.imConversation.update({ + where: { id: row.conversationId }, + data: { lastText: preview(next.kind, next.contentType, text), lastAt: new Date() }, + }); + return next; + } + + async recallMessage(user: AuthUser, id: string) { + const row = await this.prisma.imMessage.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new BadRequestException('消息不存在'); + await this.requireMember(row.conversationId, user.id); + if (row.fromUserId !== user.id) throw new ForbiddenException('只能撤回自己发的消息'); + if (row.contentType !== 'text') throw new BadRequestException('仅文字消息支持撤回'); + if (Date.now() - row.createdAt.getTime() > 2 * 60 * 1000) { + throw new BadRequestException('超过两分钟不能撤回'); + } + const meta = { recalledAt: new Date().toISOString(), originalBody: row.body }; + const next = await this.prisma.imMessage.update({ + where: { id }, + data: { body: '你撤回了一条消息', contentType: 'system', meta: JSON.stringify(meta) }, + }); + await this.prisma.imConversation.update({ + where: { id: row.conversationId }, + data: { lastText: '你撤回了一条消息', lastAt: new Date() }, + }); + const members = await this.memberIds(row.conversationId); + await this.fanout(members.filter((memberId) => memberId !== user.id), { + kind: 'chat', + contentType: 'system', + text: `${user.displayName} 撤回了一条消息`, + conversationId: row.conversationId, + fromUserId: user.id, + messageId: row.id, + action: 'recall', + }); + return next; + } + + async createGroup(user: AuthUser, name: string, memberIds: string[]) { + const title = name.trim(); + if (!title) throw new BadRequestException('请填写群名称'); + const uniq = [...new Set([user.id, ...memberIds.filter(Boolean)])]; + if (uniq.length < 3) throw new BadRequestException('群聊至少需要 3 人'); + const conv = await this.prisma.imConversation.create({ + data: { + type: 'group', + name: title, + ownerId: user.id, + lastText: `${user.displayName} 创建了群聊`, + lastAt: new Date(), + }, + }); + await this.prisma.imConversationMember.createMany({ + data: uniq.map((userId) => ({ + conversationId: conv.id, + userId, + role: userId === user.id ? 'owner' : 'member', + })), + }); + await this.ingest({ + fromUserId: user.id, + toUserId: '', + conversationId: conv.id, + kind: 'system', + contentType: 'system', + body: `${user.displayName} 创建了群聊「${title}」`, + fingerprint: randomUUID(), + }); + await this.fanout( + uniq.filter((id) => id !== user.id), + { + kind: 'system', + text: `${user.displayName} 邀请你加入群聊「${title}」`, + conversationId: conv.id, + fromUserId: user.id, + }, + ); + return conv; + } + + async groupMembers(user: AuthUser, conversationId: string) { + await this.requireMember(conversationId, user.id); + const rows = await this.prisma.imConversationMember.findMany({ + where: { conversationId, leftAt: null }, + }); + const people = await this.prisma.user.findMany({ + where: { id: { in: rows.map((r) => r.userId) } }, + select: { id: true, displayName: true, username: true }, + }); + const names = new Map(people.map((p) => [p.id, p.displayName || p.username])); + const avatars = await avatarByUserIds(this.prisma, rows.map((r) => r.userId)); + return rows.map((r) => ({ + ...r, + id: r.userId, + name: names.get(r.userId) || '同事', + avatarFileId: avatars.get(r.userId) || null, + })); + } + + async addMembers(user: AuthUser, conversationId: string, memberIds: string[]) { + const conv = await this.requireMember(conversationId, user.id); + if (conv.type !== 'group') throw new BadRequestException('只有群聊可以拉人'); + const uniq = [...new Set(memberIds.filter(Boolean))]; + await this.prisma.imConversationMember.createMany({ + data: uniq.map((userId) => ({ conversationId, userId, role: 'member' })), + skipDuplicates: true, + }); + return this.groupMembers(user, conversationId); + } + + async screenshot(user: AuthUser, conversationId?: string, peerId?: string) { + const conv = conversationId + ? await this.requireMember(conversationId, user.id) + : await this.ensureDirect(user.id, peerId || ''); + const members = await this.memberIds(conv.id); + const body = `${user.displayName} 截屏了`; + await this.ingest({ + fromUserId: user.id, + toUserId: '', + conversationId: conv.id, + kind: 'screenshot', + contentType: 'system', + body, + fingerprint: randomUUID(), + }); + await this.fanout( + members.filter((id) => id !== user.id), + { kind: 'screenshot', text: body, conversationId: conv.id, fromUserId: user.id }, + ); + return { ok: true }; + } + + async receipts(user: AuthUser, messageId: string) { + const msg = await this.prisma.imMessage.findFirst({ where: { id: messageId } }); + if (!msg) throw new NotFoundLike(); + await this.requireMember(msg.conversationId, user.id); + const rows = await this.prisma.imMessageReceipt.findMany({ where: { messageId } }); + const people = await this.prisma.user.findMany({ + where: { id: { in: rows.map((r) => r.userId) } }, + select: { id: true, displayName: true }, + }); + return people.map((p) => ({ userId: p.id, name: p.displayName })); + } + + async createCall(user: AuthUser, dto: { conversationId?: string; peerId?: string; kind?: string; title?: string }) { + await this.expireRingingCalls(); + const kind = dto.kind === 'audio' ? 'audio' : dto.kind === 'meeting' ? 'meeting' : 'video'; + const conv = dto.conversationId + ? await this.requireMember(dto.conversationId, user.id) + : dto.peerId + ? await this.ensureDirect(user.id, dto.peerId) + : null; + const memberIds = conv ? await this.memberIds(conv.id) : [user.id, dto.peerId || ''].filter(Boolean); + const others = memberIds.filter((id) => id !== user.id); + if (!others.length) throw new BadRequestException('请选择通话对象'); + const call = await this.prisma.$transaction(async (tx) => { + const lock = (tx as unknown as { $queryRawUnsafe: (sql: string, ...args: unknown[]) => Promise }).$queryRawUnsafe; + for (const id of [...new Set(memberIds)].sort()) { + await lock.call(tx, 'SELECT pg_advisory_xact_lock(hashtext($1))', `fy-im-call:${id}`); + } + const activeCalls = await tx.imCall.findMany({ + where: { status: { in: ['ringing', 'active'] } }, + select: { id: true }, + }); + const busy = activeCalls.length + ? await tx.imCallMember.findFirst({ + where: { + callId: { in: activeCalls.map((item) => item.id) }, + userId: { in: memberIds }, + status: { in: ['invited', 'joined'] }, + }, + }) + : null; + if (busy) { + if (busy.userId === user.id) throw new ConflictException('您正在通话中'); + throw new ConflictException('对方正在通话中'); + } + const created = await tx.imCall.create({ + data: { + conversationId: conv?.id, + kind, + status: 'ringing', + initiatorId: user.id, + title: dto.title?.trim() || (kind === 'meeting' ? '会议' : kind === 'audio' ? '语音通话' : '视频通话'), + }, + }); + await tx.imCallMember.createMany({ + data: memberIds.map((userId) => ({ + callId: created.id, + userId, + status: userId === user.id ? 'joined' : 'invited', + joinedAt: userId === user.id ? new Date() : null, + })), + }); + return created; + }); + if (conv) { + await this.ingest({ + fromUserId: user.id, + toUserId: conv.type === 'group' ? '' : others[0] || '', + conversationId: conv.id, + kind: 'chat', + contentType: 'call', + body: `${user.displayName} 发起了${call.title}`, + meta: { callId: call.id, callKind: kind }, + fingerprint: randomUUID(), + }); + } + await this.fanout(others, { + kind: 'call', + contentType: 'call', + text: `${user.displayName} 发起了${call.title}`, + conversationId: conv?.id, + callId: call.id, + callKind: kind, + fromUserId: user.id, + fromName: user.displayName, + memberIds, + }); + return { ...call, memberIds }; + } + + async incomingCalls(user: AuthUser) { + const staleAt = new Date(Date.now() - 45 * 1000); + await this.expireRingingCalls(); + const rows = await this.prisma.imCallMember.findMany({ + where: { userId: user.id, status: 'invited' }, + }); + const calls = await this.prisma.imCall.findMany({ + where: { + id: { in: rows.map((r) => r.callId) }, + status: 'ringing', + createdAt: { gte: staleAt }, + }, + orderBy: { createdAt: 'desc' }, + take: 5, + }); + const people = await this.prisma.user.findMany({ + where: { id: { in: [...new Set(calls.map((c) => c.initiatorId))] } }, + select: { id: true, displayName: true }, + }); + const names = new Map(people.map((p) => [p.id, p.displayName])); + return Promise.all( + calls.map(async (c) => ({ + ...c, + title: c.title || names.get(c.initiatorId) || '来电', + fromName: names.get(c.initiatorId) || '', + memberIds: await this.memberIdsOfCall(c.id), + })), + ); + } + + async callAction(user: AuthUser, callId: string, action: 'join' | 'reject' | 'end') { + const call = await this.prisma.imCall.findFirst({ where: { id: callId } }); + if (!call) throw new NotFoundLike(); + const member = await this.prisma.imCallMember.findFirst({ where: { callId, userId: user.id } }); + if (!member) throw new ForbiddenException('不是通话成员'); + if (action === 'join') { + await this.prisma.imCallMember.update({ + where: { id: member.id }, + data: { status: 'joined', joinedAt: new Date() }, + }); + const joined = await this.prisma.imCallMember.count({ where: { callId, status: 'joined' } }); + if (call.status === 'ringing' && joined >= 2) { + await this.prisma.imCall.update({ where: { id: callId }, data: { status: 'active' } }); + } + } else if (action === 'reject') { + await this.prisma.imCallMember.update({ + where: { id: member.id }, + data: { status: 'rejected', leftAt: new Date() }, + }); + const still = await this.prisma.imCallMember.count({ + where: { callId, userId: { not: user.id }, status: { in: ['invited', 'joined'] } }, + }); + if (still <= 1) { + await this.prisma.imCall.update({ + where: { id: callId }, + data: { status: 'ended', endedAt: new Date() }, + }); + } + const others = await this.memberIdsOfCall(callId); + await this.fanout(others.filter((id) => id !== user.id), { + kind: 'call', callId, action: 'reject', text: '对方拒绝接听', fromUserId: user.id, + }); + } else { + await this.prisma.imCall.update({ + where: { id: callId }, + data: { status: 'ended', endedAt: new Date() }, + }); + await this.prisma.imCallMember.updateMany({ + where: { callId, status: { in: ['invited', 'joined'] } }, + data: { status: 'left', leftAt: new Date() }, + }); + const others = await this.memberIdsOfCall(callId); + await this.fanout( + others.filter((id) => id !== user.id), + { kind: 'call', callId, action: 'end', text: '通话已结束', fromUserId: user.id }, + ); + } + return this.prisma.imCall.findFirst({ where: { id: callId } }); + } + + async callStatus(user: AuthUser, callId: string) { + await this.expireRingingCalls(); + const call = await this.prisma.imCall.findFirst({ where: { id: callId } }); + if (!call) throw new NotFoundLike(); + const members = await this.prisma.imCallMember.findMany({ where: { callId } }); + const mine = members.find((m) => m.userId === user.id); + if (!mine) throw new ForbiddenException('不是通话成员'); + const others = members.filter((m) => m.userId !== user.id); + const initiator = await this.prisma.user.findUnique({ where: { id: call.initiatorId }, select: { displayName: true } }); + return { + ...call, + memberIds: members.map((m) => m.userId), + memberStatus: mine.status, + peerJoined: others.some((m) => m.status === 'joined'), + peerRejected: others.some((m) => m.status === 'rejected'), + joinedCount: members.filter((m) => m.status === 'joined').length, + fromName: initiator?.displayName || '', + }; + } + + async livekitToken(user: AuthUser, callId: string) { + const state = await this.callStatus(user, callId); + if (state.status === 'ended' || state.memberStatus === 'rejected' || state.memberStatus === 'left') { + throw new BadRequestException('通话已结束'); + } + const apiKey = process.env.LIVEKIT_API_KEY || ''; + const apiSecret = process.env.LIVEKIT_API_SECRET || ''; + const url = process.env.LIVEKIT_URL || ''; + if (!apiKey || !apiSecret || !url) throw new BadRequestException('音视频服务未配置'); + const token = new AccessToken(apiKey, apiSecret, { + identity: `user-${user.id}`, + name: user.displayName, + ttl: '1h', + }); + token.addGrant({ roomJoin: true, room: `fy-call-${callId}`, canPublish: true, canSubscribe: true }); + return { url, token: await token.toJwt(), room: `fy-call-${callId}` }; + } + + async postSignal( + user: AuthUser, + callId: string, + dto: { toUserId?: string; kind?: string; payload?: unknown }, + ) { + const member = await this.prisma.imCallMember.findFirst({ where: { callId, userId: user.id } }); + if (!member) throw new ForbiddenException('不是通话成员'); + if (!dto.toUserId || !dto.kind) throw new BadRequestException('缺少信令对象'); + const row = await this.prisma.imCallSignal.create({ + data: { + callId, + fromUserId: user.id, + toUserId: dto.toUserId, + kind: dto.kind, + payload: typeof dto.payload === 'string' ? dto.payload : JSON.stringify(dto.payload ?? {}), + }, + }); + await this.forwardTcp(dto.toUserId, { + kind: 'signal', + callId, + signalKind: dto.kind, + fromUserId: user.id, + payload: dto.payload, + }); + return row; + } + + async pullSignals(user: AuthUser, callId: string, after?: string) { + const member = await this.prisma.imCallMember.findFirst({ where: { callId, userId: user.id } }); + if (!member) throw new ForbiddenException('不是通话成员'); + return this.prisma.imCallSignal.findMany({ + where: { + callId, + toUserId: user.id, + ...(after ? { createdAt: { gt: new Date(after) } } : {}), + }, + orderBy: { createdAt: 'asc' }, + take: 100, + }); + } + + async ingest(dto: { + fromUserId: string; + toUserId: string; + conversationId?: string; + kind?: string; + contentType?: string; + body: string; + meta?: unknown; + fingerprint: string; + }) { + if (!dto.fromUserId || !dto.fingerprint) throw new BadRequestException('缺少会话字段'); + const existing = await this.prisma.imMessage.findUnique({ where: { fingerprint: dto.fingerprint } }); + if (existing) return existing; + const conv = dto.conversationId + ? await this.prisma.imConversation.findFirst({ where: { id: dto.conversationId } }) + : await this.ensureDirect(dto.fromUserId, dto.toUserId); + if (!conv) throw new BadRequestException('会话不存在'); + const meta = dto.meta == null ? '' : typeof dto.meta === 'string' ? dto.meta : JSON.stringify(dto.meta); + const seq = await this.allocateSeq(conv.id); + const row = await this.prisma.imMessage.create({ + data: { + conversationId: conv.id, + seq, + fromUserId: dto.fromUserId, + toUserId: dto.toUserId || '', + kind: dto.kind || 'chat', + contentType: dto.contentType || 'text', + body: dto.body, + meta, + fingerprint: dto.fingerprint, + }, + }); + await this.prisma.imConversation.update({ + where: { id: conv.id }, + data: { + lastText: preview(row.kind, row.contentType, dto.body), + lastAt: new Date(), + }, + }); + return row; + } + + async pushEvent(payload: ImPushPayload) { + const text = payload.body || payload.title; + for (const toUserId of payload.userIds) { + if (!toUserId || toUserId === payload.fromUserId) continue; + const fp = payload.bizType && payload.bizId + ? `evt:${payload.bizType}:${payload.bizId}:${payload.kind || 'todo'}` + : randomUUID(); + const saved = await this.ingest({ + fromUserId: payload.fromUserId || '0', + toUserId, + kind: payload.kind, + body: text, + fingerprint: fp, + }); + await this.forwardTcp(toUserId, { + kind: payload.kind, + text, + title: payload.title, + bizType: payload.bizType, + bizId: payload.bizId, + fromUserId: payload.fromUserId || '0', + fingerprint: fp, + seq: saved.seq, + }); + void this.push.notifyUsers([toUserId], { + title: payload.title || '风影办公', + content: text || '你有一条新消息', + kind: payload.kind, + category: payload.kind === 'chat' ? 'IM' : 'TODO', + bizType: payload.bizType, + bizId: payload.bizId, + }); + } + } + + async internalMembers(conversationId: string) { + return { userIds: await this.memberIds(conversationId) }; + } + + private async ensureDirect(a: string, b: string) { + if (!a || !b) throw new BadRequestException('请选择同事'); + const [userAId, userBId] = pair(a, b); + const conv = await this.prisma.imConversation.upsert({ + where: { userAId_userBId: { userAId, userBId } }, + create: { type: 'direct', userAId, userBId }, + update: {}, + }); + await this.prisma.imConversationMember.createMany({ + data: [ + { conversationId: conv.id, userId: a, role: 'member' }, + { conversationId: conv.id, userId: b, role: 'member' }, + ], + skipDuplicates: true, + }); + return conv; + } + + private async requireMember(conversationId: string, userId: string) { + const conv = await this.prisma.imConversation.findFirst({ where: { id: conversationId } }); + if (!conv) throw new BadRequestException('会话不存在'); + const hit = await this.prisma.imConversationMember.findFirst({ + where: { conversationId, userId, leftAt: null }, + }); + const legacy = conv.type === 'direct' && (conv.userAId === userId || conv.userBId === userId); + if (!hit && !legacy) throw new ForbiddenException('不是会话成员'); + if (!hit && legacy) { + await this.prisma.imConversationMember.createMany({ + data: [ + { conversationId, userId: conv.userAId || userId, role: 'member' }, + { conversationId, userId: conv.userBId || userId, role: 'member' }, + ].filter((r) => r.userId), + skipDuplicates: true, + }); + } + return conv; + } + + private async memberIds(conversationId: string) { + const rows = await this.prisma.imConversationMember.findMany({ + where: { conversationId, leftAt: null }, + select: { userId: true }, + }); + if (rows.length) return rows.map((r) => r.userId); + const conv = await this.prisma.imConversation.findFirst({ where: { id: conversationId } }); + return [conv?.userAId, conv?.userBId].filter(Boolean) as string[]; + } + + private async memberIdsOfCall(callId: string) { + const rows = await this.prisma.imCallMember.findMany({ where: { callId }, select: { userId: true } }); + return rows.map((r) => r.userId); + } + + private async expireRingingCalls() { + const staleAt = new Date(Date.now() - 45 * 1000); + const stale = await this.prisma.imCall.findMany({ + where: { status: 'ringing', createdAt: { lt: staleAt } }, + select: { id: true }, + }); + if (!stale.length) return; + await this.prisma.imCall.updateMany({ + where: { id: { in: stale.map((row) => row.id) }, status: 'ringing' }, + data: { status: 'ended', endedAt: new Date() }, + }); + await this.prisma.imCallMember.updateMany({ + where: { callId: { in: stale.map((row) => row.id) }, status: { in: ['invited', 'joined'] } }, + data: { status: 'left', leftAt: new Date() }, + }); + for (const row of stale) { + const members = await this.memberIdsOfCall(row.id); + await this.fanout(members, { kind: 'call', callId: row.id, action: 'timeout', text: '无人接听' }); + } + } + + private async fanout(userIds: string[], payload: Record) { + await Promise.all(userIds.map((id) => this.forwardTcp(id, payload))); + const kind = String(payload.kind || ''); + const title = String(payload.fromName || payload.title || '风影办公'); + const content = String(payload.text || payload.body || '你有一条新消息'); + void this.push.notifyUsers(userIds, { + title, + content, + conversationId: String(payload.conversationId || ''), + kind, + category: kind === 'todo' || kind === 'approval' ? 'TODO' : 'IM', + bizType: String(payload.bizType || ''), + bizId: String(payload.bizId || ''), + callId: String(payload.callId || ''), + callKind: String(payload.callKind || ''), + fromName: String(payload.fromName || ''), + action: String(payload.action || ''), + }); + } + + private async forwardTcp(toUserId: string, payload: Record) { + const port = Number(process.env.IM_HTTP_PORT || 8902); + const token = process.env.IM_INTERNAL_TOKEN || ''; + try { + const res = await fetch(`http://127.0.0.1:${port}/push`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-im-token': token }, + body: JSON.stringify({ + toUserId, + dataContent: JSON.stringify(payload), + seq: payload.seq, + }), + }); + if (!res.ok) this.log.warn(`im push ${res.status}`); + } catch (e) { + this.log.warn(`im push: ${(e as Error).message}`); + } + } + + /** 会话内单调递增 seq,服务端唯一分配。 */ + private async allocateSeq(conversationId: string) { + const conv = await this.prisma.imConversation.update({ + where: { id: conversationId }, + data: { nextSeq: { increment: 1 } }, + select: { nextSeq: true }, + }); + return conv.nextSeq - 1; + } + + private async backfillDirectMembers() { + const orphans = await this.prisma.imConversation.findMany({ + where: { type: 'direct', userAId: { not: null }, userBId: { not: null } }, + take: 50, + orderBy: { lastAt: 'desc' }, + }); + for (const c of orphans) { + if (!c.userAId || !c.userBId) continue; + await this.prisma.imConversationMember.createMany({ + data: [ + { conversationId: c.id, userId: c.userAId, role: 'member' }, + { conversationId: c.id, userId: c.userBId, role: 'member' }, + ], + skipDuplicates: true, + }); + } + } +} + +class NotFoundLike extends BadRequestException { + constructor() { + super('记录不存在'); + } +} + +export { emitIm }; diff --git a/apps/api/src/im/voice-asr.service.ts b/apps/api/src/im/voice-asr.service.ts new file mode 100644 index 0000000..d3b4663 --- /dev/null +++ b/apps/api/src/im/voice-asr.service.ts @@ -0,0 +1,178 @@ +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; +import { createHmac, randomUUID } from 'crypto'; +import { execFile } from 'child_process'; +import { mkdtempSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { promisify } from 'util'; + +const execFileAsync = promisify(execFile); + +function percentEncode(value: string): string { + return encodeURIComponent(value).replace(/\+/g, '%20').replace(/\*/g, '%2A').replace(/%7E/g, '~'); +} + +function signRpc(params: Record, secret: string): string { + const keys = Object.keys(params).sort(); + const canonical = keys.map((k) => `${percentEncode(k)}=${percentEncode(params[k]!)}`).join('&'); + const stringToSign = `GET&${percentEncode('/')}&${percentEncode(canonical)}`; + return createHmac('sha1', `${secret}&`).update(stringToSign).digest('base64'); +} + +type TokenCache = { token: string; expireAt: number }; +let nlsToken: TokenCache | null = null; + +@Injectable() +export class VoiceAsrService { + private readonly log = new Logger(VoiceAsrService.name); + + async transcribe(buffer: Buffer): Promise { + if (!buffer.length) throw new BadRequestException('语音为空'); + const wav = await this.toWav16k(buffer); + try { + const dash = await this.dashScope(wav); + if (dash) return dash; + } catch (e) { + this.log.warn(`DashScope ASR failed: ${e}`); + } + try { + const ali = await this.aliyunFlash(wav); + if (ali) return ali; + } catch (e) { + this.log.warn(`Aliyun NLS failed: ${e}`); + } + try { + const whisper = await this.whisperCli(wav); + if (whisper) return whisper; + } catch (e) { + this.log.warn(`whisper cli failed: ${e}`); + } + throw new BadRequestException('语音识别服务未配置或识别失败,请联系管理员配置 DASHSCOPE_API_KEY 或 ALIYUN_NLS_APPKEY'); + } + + private async toWav16k(input: Buffer): Promise { + const dir = mkdtempSync(join(tmpdir(), 'fy-asr-')); + const inPath = join(dir, 'in.bin'); + const outPath = join(dir, 'out.wav'); + writeFileSync(inPath, input); + try { + await execFileAsync('ffmpeg', ['-y', '-i', inPath, '-ac', '1', '-ar', '16000', '-f', 'wav', outPath], { + timeout: 60000, + }); + return readFileSync(outPath); + } finally { + for (const p of [inPath, outPath]) { + try { + unlinkSync(p); + } catch { + /* ignore */ + } + } + } + } + + private async dashScope(wav: Buffer): Promise { + const key = process.env.DASHSCOPE_API_KEY || ''; + if (!key) return null; + const res = await fetch('https://dashscope.aliyuncs.com/api/v1/services/audio/asr/transcription', { + method: 'POST', + headers: { + Authorization: `Bearer ${key}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model: 'paraformer-v2', + input: { audio: wav.toString('base64'), format: 'wav', sample_rate: 16000 }, + }), + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`DashScope ${res.status}: ${err.slice(0, 200)}`); + } + const json = (await res.json()) as { output?: { text?: string; sentence?: { text?: string }[] } }; + const text = json.output?.text?.trim() || json.output?.sentence?.map((s) => s.text || '').join('').trim() || ''; + return text || null; + } + + private async aliyunFlash(wav: Buffer): Promise { + const appkey = process.env.ALIYUN_NLS_APPKEY || ''; + if (!appkey) return null; + const token = await this.nlsToken(); + const url = + `https://nls-gateway-cn-shanghai.aliyuncs.com/stream/v1/FlashRecognizer` + + `?appkey=${encodeURIComponent(appkey)}&token=${encodeURIComponent(token)}&format=wav&sample_rate=16000`; + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/octet-stream' }, + body: new Uint8Array(wav), + }); + if (!res.ok) { + const err = await res.text(); + throw new Error(`NLS ${res.status}: ${err.slice(0, 200)}`); + } + const json = (await res.json()) as { + flash_result?: { sentences?: { text?: string }[] }; + result?: string; + message?: string; + }; + if (json.message && !json.flash_result && !json.result) { + throw new Error(json.message); + } + const text = + json.flash_result?.sentences?.map((s) => s.text || '').join('').trim() || `${json.result || ''}`.trim(); + return text || null; + } + + private async nlsToken(): Promise { + const now = Date.now(); + if (nlsToken && nlsToken.expireAt > now + 60_000) return nlsToken.token; + const id = process.env.ALIYUN_SMS_ACCESS_KEY_ID || process.env.ALIYUN_ACCESS_KEY_ID || ''; + const secret = process.env.ALIYUN_SMS_ACCESS_KEY_SECRET || process.env.ALIYUN_ACCESS_KEY_SECRET || ''; + if (!id || !secret) throw new Error('缺少阿里云 AccessKey'); + const params: Record = { + Action: 'CreateToken', + Version: '2019-02-28', + Format: 'JSON', + RegionId: 'cn-shanghai', + AccessKeyId: id, + SignatureMethod: 'HMAC-SHA1', + SignatureVersion: '1.0', + SignatureNonce: randomUUID(), + Timestamp: new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'), + }; + params.Signature = signRpc(params, secret); + const qs = Object.keys(params) + .sort() + .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k]!)}`) + .join('&'); + const res = await fetch(`https://nls-meta.cn-shanghai.aliyuncs.com/?${qs}`); + if (!res.ok) throw new Error(`CreateToken HTTP ${res.status}`); + const json = (await res.json()) as { Token?: { Id?: string; ExpireTime?: number }; Message?: string }; + const token = json.Token?.Id || ''; + if (!token) throw new Error(json.Message || 'CreateToken 失败'); + const expireAt = (json.Token?.ExpireTime || 0) * 1000; + nlsToken = { token, expireAt: expireAt || now + 3600_000 }; + return token; + } + + private async whisperCli(wav: Buffer): Promise { + const dir = mkdtempSync(join(tmpdir(), 'fy-whisper-')); + const wavPath = join(dir, 'in.wav'); + writeFileSync(wavPath, wav); + try { + const { stdout } = await execFileAsync('whisper', [wavPath, '--language', 'Chinese', '--output_format', 'txt'], { + timeout: 120000, + }); + const text = `${stdout || ''}`.trim(); + return text || null; + } catch { + return null; + } finally { + try { + unlinkSync(wavPath); + } catch { + /* ignore */ + } + } + } +} diff --git a/apps/api/src/legal/legal.controller.ts b/apps/api/src/legal/legal.controller.ts new file mode 100644 index 0000000..d790e37 --- /dev/null +++ b/apps/api/src/legal/legal.controller.ts @@ -0,0 +1,27 @@ +import { Controller, Get, NotFoundException, Param } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { SkipThrottle } from '@nestjs/throttler'; +import { Public } from '../common/decorators/auth.decorators'; +import { LEGAL_DOCS, isLegalKind } from './legal.docs'; + +@ApiTags('legal') +@SkipThrottle() +@Controller('legal') +export class LegalController { + @Public() + @Get() + index() { + return Object.values(LEGAL_DOCS).map((d) => ({ + kind: d.kind, + title: d.title, + updatedAt: d.updatedAt, + })); + } + + @Public() + @Get(':kind') + doc(@Param('kind') kind: string) { + if (!isLegalKind(kind)) throw new NotFoundException('文档不存在'); + return LEGAL_DOCS[kind]; + } +} diff --git a/apps/api/src/legal/legal.docs.ts b/apps/api/src/legal/legal.docs.ts new file mode 100644 index 0000000..44f7db6 --- /dev/null +++ b/apps/api/src/legal/legal.docs.ts @@ -0,0 +1,127 @@ +export type LegalKind = 'user-agreement' | 'privacy'; + +export type LegalDoc = { + kind: LegalKind; + title: string; + updatedAt: string; + operator: string; + product: string; + paragraphs: { heading?: string; body: string }[]; +}; + +const OPERATOR = '江苏风影随行科技有限公司'; +const PRODUCT = '风影智慧办公'; +const UPDATED = '2026-08-31'; + +const USER_AGREEMENT: LegalDoc = { + kind: 'user-agreement', + title: '用户协议', + updatedAt: UPDATED, + operator: OPERATOR, + product: PRODUCT, + paragraphs: [ + { + heading: '一、协议的订立', + body: `欢迎使用${PRODUCT}(含网页端、Android 客户端及其他经授权的接入端,以下统称「本平台」)。本协议由您与${OPERATOR}(以下简称「我们」)订立。您点击同意、勾选同意或实际登录使用,即表示您已阅读、理解并同意受本协议约束。未勾选同意前,任何一端均不得登录。`, + }, + { + heading: '二、服务内容', + body: '本平台为企业内部办公系统,向经授权的员工提供即时通讯、审批、人事、用章、资产、采购、差旅报销及相关台账等服务。账号由单位开通或确认,仅限本人履行职务使用,不得出借、转让或交由他人代为登录。', + }, + { + heading: '三、账号与安全', + body: '您应妥善保管手机号、密码、短信验证码及设备。因您保管不善导致的账号盗用、信息泄露,由您和所属单位按内部规定承担责任。发现异常登录应立即修改密码并通知管理员。我们有权在发现安全风险时限制、冻结或注销账号。', + }, + { + heading: '四、使用规范', + body: '您不得利用本平台从事违法违规、侵害他人权益或危害信息系统安全的行为,包括但不限于传播违法信息、绕过权限、扫描攻击、爬取他人数据、伪造审批或篡改台账。工作信息、审批意见、聊天记录等应按单位制度使用,不得擅自对外披露。', + }, + { + heading: '五、通知与推送', + body: '为及时送达聊天消息、审批待办等,客户端可能申请通知权限、忽略电池优化,并引导您按机型打开自启动/后台运行。您可在系统设置中关闭,但可能无法及时收到消息。相机、麦克风、位置等权限仅在您使用拍照、语音、定位相关功能时申请。', + }, + { + heading: '六、知识产权', + body: `本平台的软件、界面、文档及相关知识产权归${OPERATOR}或合法权利人所有。您仅获得按职务需要使用的有限、不可转让许可,不得反向工程、破解或把本平台提供给无关第三方。您在平台产生的工作内容,权属按劳动合同及单位制度执行。`, + }, + { + heading: '七、服务变更与中止', + body: '我们可能因系统维护、安全事件、政策要求或与贵单位的服务安排调整功能或中止服务。重大变更将通过公告、站内信或管理员通知。因不可抗力、基础运营商故障、第三方推送通道或您未按机型完成保活设置导致的延迟或中断,我们将在合理范围内协助排查。', + }, + { + heading: '八、责任限制', + body: '本平台按「现状」向内部用户提供。在法律允许范围内,我们对间接损失、预期利益、数据未能及时推送造成的延误不承担责任,但因我们故意或重大过失造成的除外。与职务相关的审批、合同、财务责任仍由相应业务制度约束。', + }, + { + heading: '九、协议变更与法律适用', + body: '我们可能更新本协议,更新后将在登录页公示并重新取得勾选同意。本协议适用中华人民共和国法律。争议由我们住所地有管辖权的人民法院管辖。', + }, + { + heading: '十、联系我们', + body: `如对本协议有疑问,请通过本平台管理员或单位信息化接口人联系${OPERATOR}。`, + }, + ], +}; + +const PRIVACY: LegalDoc = { + kind: 'privacy', + title: '隐私政策', + updatedAt: UPDATED, + operator: OPERATOR, + product: PRODUCT, + paragraphs: [ + { + heading: '一、引言', + body: `${OPERATOR}(以下简称「我们」)作为${PRODUCT}的运营者,重视员工及用户的个人信息保护。本政策说明我们处理个人信息的目的、方式、范围和您的权利。本平台为企业内部办公系统,处理活动同时受贵单位用工管理与数据安全制度约束。未阅读并勾选同意本政策前,任何一端均不得登录。`, + }, + { + heading: '二、我们收集的信息', + body: '1. 账号信息:手机号、姓名、工号、部门、岗位、角色权限,用于登录鉴权和权限控制。2. 身份与人事信息:按入职/档案要求可能包括证件影像、紧急联系人、银行卡等,仅用于人事与薪酬流程。3. 办公业务信息:审批、用章、资产占用、差旅报销、即时通讯内容等,用于完成您发起或处理的业务。4. 设备与推送信息:设备品牌型号、推送登记标识(如各厂商 pushId/regId)、应用版本,用于消息送达。5. 日志信息:登录时间、IP、操作记录,用于安全审计。6. 您主动上传的照片、语音、文件。', + }, + { + heading: '三、权限调用说明', + body: '通知:用于聊天、审批待办等提醒。电池优化/自启动/后台运行:用于厂商系统下保持消息通道,不用于采集额外个人信息。相机:仅在拍照、扫码或上传影像时使用。麦克风:仅在按住说话等语音功能时使用。位置:仅在考勤打卡、外勤等明确需要定位的功能中使用。存储/相册:仅在您选择图片或文件时读取您指定的内容。上述权限均可在系统设置中关闭,关闭后对应功能可能不可用。', + }, + { + heading: '四、使用目的', + body: '我们在实现办公协同、身份认证、审批流转、消息通知、安全审计、故障排查和版本升级所必需的范围内处理个人信息,不会将您的个人信息用于与职务无关的营销推送。', + }, + { + heading: '五、委托处理、共享与公开', + body: '为实现推送,我们可能将推送标识及通知标题/摘要发送至 vivo、OPPO/一加、魅族/Flyme 等系统推送通道,传输内容以完成消息展示为限。对象存储、短信等基础设施服务商仅在提供服务所必需的范围内处理数据。我们不会出售个人信息。因法律法规、监管或安全需要,可能依法提供必要信息。', + }, + { + heading: '六、存储与跨境', + body: '个人信息存储在中华人民共和国境内。保留期限为实现目的所必需的最短时间,或按会计、劳动、档案等法定期限及单位制度执行。逾期将删除或匿名化,法律另有规定的除外。', + }, + { + heading: '七、安全保护', + body: '我们采取访问控制、传输加密、操作日志、最小权限等措施。因您将账号交由他人使用,或在越狱/Root、非正式渠道安装的设备上使用而导致的风险,请您谨慎评估。', + }, + { + heading: '八、您的权利', + body: '您有权查阅、更正与职务相关的个人资料,并可通过「我」或管理员申请注销账号。账号注销后,除依法需保留的人事、财务、审计记录外,我们将停止提供服务并删除或匿名化相关信息。部分职务数据可能因单位档案要求继续由单位保管。', + }, + { + heading: '九、未成年人', + body: '本平台面向单位在职人员。如发现未成年人在未获监护人同意的情况下使用,请联系管理员停止服务。', + }, + { + heading: '十、政策更新', + body: '本政策更新后将在登录页公示。若处理目的、方式或种类发生重大变化,我们将再次取得您的同意。继续勾选并登录视为知悉更新后的政策。', + }, + { + heading: '十一、联系我们', + body: `个人信息保护相关事项,请通过本平台管理员联系${OPERATOR}。`, + }, + ], +}; + +export const LEGAL_DOCS: Record = { + 'user-agreement': USER_AGREEMENT, + privacy: PRIVACY, +}; + +export function isLegalKind(v: string): v is LegalKind { + return v === 'user-agreement' || v === 'privacy'; +} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts new file mode 100644 index 0000000..957e456 --- /dev/null +++ b/apps/api/src/main.ts @@ -0,0 +1,51 @@ +import { NestFactory } from '@nestjs/core'; +import { ValidationPipe } from '@nestjs/common'; +import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import helmet from 'helmet'; +import { AppModule } from './app.module'; +import { loadEnv } from './config/env'; + +function applyEnvFile(file: string) { + try { + for (const line of readFileSync(file, 'utf8').split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const i = trimmed.indexOf('='); + if (i <= 0) continue; + process.env[trimmed.slice(0, i)] = trimmed.slice(i + 1); + } + } catch { + /* 文件不存在就跳过 */ + } +} +applyEnvFile(resolve(__dirname, '../.env')); + +async function bootstrap() { + const env = loadEnv(); + const app = await NestFactory.create(AppModule, { bufferLogs: true }); + app.setGlobalPrefix(env.API_PREFIX); + app.use(helmet()); + app.enableCors({ + origin: env.CORS_ORIGINS.split(',').map((s) => s.trim()), + credentials: true, + }); + app.useGlobalPipes( + new ValidationPipe({ + whitelist: true, + transform: true, + forbidNonWhitelisted: false, + }), + ); + const swagger = new DocumentBuilder() + .setTitle('企业综合运营管理平台') + .setDescription('Web 先行 API') + .setVersion('0.1.0') + .addBearerAuth() + .build(); + SwaggerModule.setup(`${env.API_PREFIX}/docs`, app, SwaggerModule.createDocument(app, swagger)); + await app.listen(env.APP_PORT); + console.log(`API ready at http://localhost:${env.APP_PORT}/${env.API_PREFIX}`); +} +bootstrap(); diff --git a/apps/api/src/master/master.controller.ts b/apps/api/src/master/master.controller.ts new file mode 100644 index 0000000..bbfc9d8 --- /dev/null +++ b/apps/api/src/master/master.controller.ts @@ -0,0 +1,192 @@ +import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsArray, IsBoolean, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { PageQueryDto } from '../common/dto/page-query.dto'; +import { MasterService } from './master.service'; +import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator'; + +class ContactBody { + @IsOptional() + @IsString() + id?: string; + @IsString() + name!: string; + @IsOptional() + @IsString() + phone?: string; + @IsOptional() + @IsString() + title?: string; +} + +class PartyDto { + @IsString() + name!: string; + @IsOptional() + @IsString() + creditNo?: string; + @IsOptional() + @IsString() + level?: string; + @IsOptional() + @IsBoolean() + riskFlag?: boolean; + @IsOptional() + @IsString() + remark?: string; + @IsOptional() + @IsArray() + @ValidateNested({ each: true }) + @Type(() => ContactBody) + contacts?: ContactBody[]; +} + +class EntityDto { + @IsString() + name!: string; + @IsString() + shortName!: string; + @IsOptional() + @IsString() + creditNo?: string; + @IsOptional() + @IsBoolean() + isDefault?: boolean; +} + +class ContactDto extends ContactBody { + @IsString() + partyId!: string; +} + +@ApiTags('master') +@ApiBearerAuth() +@Controller() +export class MasterController { + constructor(private readonly master: MasterService) {} + + @Get('parties') + parties(@Query() q: PageQueryDto) { + return this.master.listParties(q.page, q.pageSize, q.keyword); + } + + @Post('parties') + createParty(@Body() dto: PartyDto) { + return this.master.createParty(dto); + } + + @Get('parties/:id') + party(@Param('id', ParseUUIDPipe) id: string) { + return this.master.getParty(id); + } + + @Patch('parties/:id') + updateParty(@Param('id', ParseUUIDPipe) id: string, @Body() dto: PartyDto) { + return this.master.updateParty(id, dto); + } + + @Delete('parties/:id') + deleteParty(@Param('id', ParseUUIDPipe) id: string) { + return this.master.deleteParty(id); + } + + @Get('legal-entities') + legalEntities() { + return this.master.listEntities(); + } + + @Post('legal-entities') + createEntity(@Body() dto: EntityDto) { + return this.master.createEntity(dto); + } + + @Get('legal-entities/:id') + entity(@Param('id', ParseUUIDPipe) id: string) { + return this.master.getEntity(id); + } + + @Patch('legal-entities/:id') + updateEntity(@Param('id', ParseUUIDPipe) id: string, @Body() dto: EntityDto) { + return this.master.updateEntity(id, dto); + } + + @Delete('legal-entities/:id') + deleteEntity(@Param('id', ParseUUIDPipe) id: string) { + return this.master.deleteEntity(id); + } + + @Get('contacts') + contacts(@Query('keyword') keyword?: string) { + return this.master.listContacts(keyword); + } + + @Post('contacts') + createContact(@Body() dto: ContactDto) { + return this.master.createContact(dto); + } + + @Patch('contacts/:id') + updateContact(@Param('id', ParseUUIDPipe) id: string, @Body() dto: ContactDto) { + return this.master.updateContact(id, dto); + } + + @Delete('contacts/:id') + deleteContact(@Param('id', ParseUUIDPipe) id: string) { + return this.master.deleteContact(id); + } + + @Get('registered-assets') + assets(@Query('kind') kind?: string, @Query('usage') usage?: string) { + return this.master.listAssets(kind, usage); + } + + @Post('registered-assets') + createAsset( + @CurrentUser() user: AuthUser, + @Body() + body: { + kind: string; + name: string; + code?: string; + spec?: string; + keeper?: string; + location?: string; + amount?: number; + remark?: string; + legalEntityId?: string; + usage?: string; + }, + ) { + return this.master.createAsset(user.id, body); + } + + @Patch('registered-assets/:id') + updateAsset( + @Param('id', ParseUUIDPipe) id: string, + @Body() + body: { + name?: string; + code?: string; + spec?: string; + keeper?: string; + location?: string; + amount?: number | null; + remark?: string; + legalEntityId?: string | null; + usage?: string; + }, + ) { + return this.master.updateAsset(id, body); + } + + @Delete('registered-assets/:id') + deleteAsset(@Param('id', ParseUUIDPipe) id: string) { + return this.master.deleteAsset(id); + } + + @Post('registered-assets/:id/inbound') + inboundAsset(@Param('id', ParseUUIDPipe) id: string) { + return this.master.confirmInbound(id); + } +} diff --git a/apps/api/src/master/master.service.ts b/apps/api/src/master/master.service.ts new file mode 100644 index 0000000..3ea48f0 --- /dev/null +++ b/apps/api/src/master/master.service.ts @@ -0,0 +1,380 @@ +import { + BadRequestException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +export type ContactInput = { id?: string; name: string; phone?: string; title?: string }; +export type PartyInput = { + name: string; + creditNo?: string; + level?: string; + riskFlag?: boolean; + remark?: string; + contacts?: ContactInput[]; +}; +export type EntityInput = { + name: string; + shortName: string; + creditNo?: string; + isDefault?: boolean; +}; + +const PARTY_INCLUDE = { contacts: { orderBy: { name: 'asc' as const } } }; + +@Injectable() +export class MasterService { + constructor(private readonly prisma: PrismaService) {} + + listParties(page: number, pageSize: number, keyword?: string) { + const where = { + tenantId: '1', + ...(keyword ? { name: { contains: keyword } } : {}), + }; + return this.prisma.$transaction(async (tx) => { + const [items, total] = await Promise.all([ + tx.businessParty.findMany({ + where, + include: PARTY_INCLUDE, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + tx.businessParty.count({ where }), + ]); + return { items, page, pageSize, total }; + }); + } + + async getParty(id: string) { + const row = await this.prisma.businessParty.findFirst({ + where: { id, tenantId: '1' }, + include: PARTY_INCLUDE, + }); + if (!row) throw new NotFoundException('单位不存在'); + return row; + } + + async createParty(dto: PartyInput) { + const name = dto.name?.trim(); + if (!name) throw new BadRequestException('请填写单位名称'); + return this.prisma.businessParty.create({ + data: { + tenantId: '1', + name, + creditNo: dto.creditNo?.trim() || null, + level: dto.level?.trim() || 'B', + riskFlag: Boolean(dto.riskFlag), + remark: dto.remark?.trim() || null, + contacts: { + create: (dto.contacts || []) + .filter((c) => c.name?.trim()) + .map((c) => ({ + tenantId: '1', + name: c.name.trim(), + phone: c.phone?.trim() || null, + title: c.title?.trim() || null, + })), + }, + }, + include: PARTY_INCLUDE, + }); + } + + async updateParty(id: string, dto: PartyInput) { + await this.getParty(id); + const name = dto.name?.trim(); + if (!name) throw new BadRequestException('请填写单位名称'); + const incoming = dto.contacts?.filter((c) => c.name?.trim()); + return this.prisma.$transaction(async (tx) => { + if (incoming) { + const keepIds = incoming.map((c) => c.id).filter(Boolean) as string[]; + await tx.partyContact.deleteMany({ + where: { partyId: id, ...(keepIds.length ? { id: { notIn: keepIds } } : {}) }, + }); + for (const c of incoming) { + const data = { + name: c.name.trim(), + phone: c.phone?.trim() || null, + title: c.title?.trim() || null, + }; + if (c.id) { + await tx.partyContact.updateMany({ + where: { id: c.id, partyId: id }, + data, + }); + } else { + await tx.partyContact.create({ data: { tenantId: '1', partyId: id, ...data } }); + } + } + } + return tx.businessParty.update({ + where: { id }, + data: { + name, + creditNo: dto.creditNo?.trim() || null, + level: dto.level?.trim() || 'B', + riskFlag: dto.riskFlag === undefined ? undefined : Boolean(dto.riskFlag), + remark: dto.remark?.trim() || null, + }, + include: PARTY_INCLUDE, + }); + }); + } + + async deleteParty(id: string) { + const row = await this.getParty(id); + const used = await this.prisma.bidCase.count({ where: { partyId: id } }); + if (used) throw new BadRequestException('已有投标事项使用该单位,不能删除'); + await this.prisma.businessParty.delete({ where: { id: row.id } }); + return { ok: true }; + } + + listEntities() { + return this.prisma.legalEntity.findMany({ + where: { tenantId: '1' }, + orderBy: { createdAt: 'asc' }, + }); + } + + async getEntity(id: string) { + const row = await this.prisma.legalEntity.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('主体不存在'); + return row; + } + + async createEntity(dto: EntityInput) { + const name = dto.name?.trim(); + const shortName = dto.shortName?.trim(); + if (!name || !shortName) throw new BadRequestException('请填写主体全称和简称'); + if (dto.isDefault) { + await this.prisma.legalEntity.updateMany({ where: { tenantId: '1' }, data: { isDefault: false } }); + } + return this.prisma.legalEntity.create({ + data: { + tenantId: '1', + name, + shortName, + creditNo: dto.creditNo?.trim() || null, + isDefault: Boolean(dto.isDefault), + }, + }); + } + + async updateEntity(id: string, dto: EntityInput) { + await this.getEntity(id); + const name = dto.name?.trim(); + const shortName = dto.shortName?.trim(); + if (!name || !shortName) throw new BadRequestException('请填写主体全称和简称'); + if (dto.isDefault) { + await this.prisma.legalEntity.updateMany({ + where: { tenantId: '1', NOT: { id } }, + data: { isDefault: false }, + }); + } + return this.prisma.legalEntity.update({ + where: { id }, + data: { + name, + shortName, + creditNo: dto.creditNo?.trim() || null, + isDefault: dto.isDefault === undefined ? undefined : Boolean(dto.isDefault), + }, + }); + } + + async deleteEntity(id: string) { + const row = await this.getEntity(id); + const [bids, contracts] = await Promise.all([ + this.prisma.bidCase.count({ where: { legalEntityId: id } }), + this.prisma.contract.count({ where: { legalEntityId: id } }), + ]); + if (bids || contracts) throw new BadRequestException('已有投标或合同使用该主体,不能删除'); + await this.prisma.legalEntity.delete({ where: { id: row.id } }); + return { ok: true }; + } + + listContacts(keyword?: string) { + return this.prisma.partyContact.findMany({ + where: { + tenantId: '1', + ...(keyword + ? { + OR: [ + { name: { contains: keyword } }, + { phone: { contains: keyword } }, + { party: { name: { contains: keyword } } }, + ], + } + : {}), + }, + include: { party: { select: { id: true, name: true, creditNo: true } } }, + orderBy: { name: 'asc' }, + }); + } + + async createContact(dto: ContactInput & { partyId: string }) { + if (!dto.partyId) throw new BadRequestException('请选择所属单位'); + if (!dto.name?.trim()) throw new BadRequestException('请填写联系人姓名'); + await this.getParty(dto.partyId); + return this.prisma.partyContact.create({ + data: { + tenantId: '1', + partyId: dto.partyId, + name: dto.name.trim(), + phone: dto.phone?.trim() || null, + title: dto.title?.trim() || null, + }, + include: { party: { select: { id: true, name: true, creditNo: true } } }, + }); + } + + async updateContact(id: string, dto: ContactInput & { partyId: string }) { + const row = await this.prisma.partyContact.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('联系人不存在'); + if (!dto.partyId) throw new BadRequestException('请选择所属单位'); + if (!dto.name?.trim()) throw new BadRequestException('请填写联系人姓名'); + await this.getParty(dto.partyId); + return this.prisma.partyContact.update({ + where: { id }, + data: { + partyId: dto.partyId, + name: dto.name.trim(), + phone: dto.phone?.trim() || null, + title: dto.title?.trim() || null, + }, + include: { party: { select: { id: true, name: true, creditNo: true } } }, + }); + } + + async deleteContact(id: string) { + const row = await this.prisma.partyContact.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('联系人不存在'); + await this.prisma.partyContact.delete({ where: { id } }); + return { ok: true }; + } + + async listAssets(kind?: string, usage?: string) { + const where = { tenantId: '1', ...(kind ? { kind } : {}), ...(usage ? { usage } : {}) }; + const items = await this.prisma.registeredAsset.findMany({ + where, + include: { + legalEntity: { select: { id: true, name: true, shortName: true } }, + createdBy: { select: { id: true, displayName: true } }, + occupancies: { + where: { status: 'IN_USE' }, + select: { id: true, holder: true, dueAt: true, status: true }, + take: 5, + }, + }, + orderBy: { createdAt: 'desc' }, + }); + return { items, total: items.length }; + } + + async createAsset( + userId: string, + dto: { + kind: string; + name: string; + code?: string; + spec?: string; + keeper?: string; + location?: string; + amount?: number; + remark?: string; + legalEntityId?: string; + usage?: string; + }, + ) { + if (dto.kind !== 'DIGITAL' && dto.kind !== 'FIXED') throw new BadRequestException('请选择资产类型'); + if (!dto.name?.trim()) throw new BadRequestException('请填写资产名称'); + if (dto.legalEntityId) { + const entity = await this.prisma.legalEntity.findFirst({ where: { id: dto.legalEntityId, tenantId: '1' } }); + if (!entity) throw new NotFoundException('参与主体不存在'); + } + return this.prisma.registeredAsset.create({ + data: { + tenantId: '1', + kind: dto.kind, + name: dto.name.trim(), + code: dto.code?.trim() || null, + spec: dto.spec?.trim() || null, + keeper: dto.keeper?.trim() || null, + location: dto.location?.trim() || null, + amount: dto.amount ?? null, + remark: dto.remark?.trim() || null, + legalEntityId: dto.legalEntityId || null, + createdById: userId, + usage: dto.kind === 'FIXED' && dto.usage === 'VEHICLE' ? 'VEHICLE' : 'GENERAL', + status: 'ACTIVE', + }, + include: { + legalEntity: { select: { id: true, name: true, shortName: true } }, + createdBy: { select: { id: true, displayName: true } }, + }, + }); + } + + async updateAsset( + id: string, + dto: { + name?: string; + code?: string; + spec?: string; + keeper?: string; + location?: string; + amount?: number | null; + remark?: string; + legalEntityId?: string | null; + usage?: string; + }, + ) { + const row = await this.prisma.registeredAsset.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('资产不存在'); + if (dto.legalEntityId) { + const entity = await this.prisma.legalEntity.findFirst({ where: { id: dto.legalEntityId, tenantId: '1' } }); + if (!entity) throw new NotFoundException('参与主体不存在'); + } + return this.prisma.registeredAsset.update({ + where: { id }, + data: { + name: dto.name?.trim() || undefined, + code: dto.code === undefined ? undefined : dto.code?.trim() || null, + spec: dto.spec === undefined ? undefined : dto.spec?.trim() || null, + keeper: dto.keeper === undefined ? undefined : dto.keeper?.trim() || null, + location: dto.location === undefined ? undefined : dto.location?.trim() || null, + amount: dto.amount === undefined ? undefined : dto.amount, + remark: dto.remark === undefined ? undefined : dto.remark?.trim() || null, + legalEntityId: dto.legalEntityId === undefined ? undefined : dto.legalEntityId || null, + usage: dto.usage === undefined ? undefined : dto.usage === 'VEHICLE' ? 'VEHICLE' : 'GENERAL', + }, + include: { + legalEntity: { select: { id: true, name: true, shortName: true } }, + createdBy: { select: { id: true, displayName: true } }, + }, + }); + } + + async deleteAsset(id: string) { + const row = await this.prisma.registeredAsset.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('资产不存在'); + await this.prisma.registeredAsset.delete({ where: { id } }); + return { ok: true }; + } + + async confirmInbound(id: string) { + const row = await this.prisma.registeredAsset.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('资产不存在'); + if (row.status !== 'PENDING_IN') throw new BadRequestException('该资产不是待入库状态'); + return this.prisma.registeredAsset.update({ + where: { id }, + data: { status: 'ACTIVE' }, + include: { + legalEntity: { select: { id: true, name: true, shortName: true } }, + createdBy: { select: { id: true, displayName: true } }, + }, + }); + } +} diff --git a/apps/api/src/office/office-apply.spec.ts b/apps/api/src/office/office-apply.spec.ts new file mode 100644 index 0000000..8b79471 --- /dev/null +++ b/apps/api/src/office/office-apply.spec.ts @@ -0,0 +1,16 @@ +import { canApproveOfficeApply, OFFICE_APPLY_KIND } from './office-apply'; + +describe('office apply', () => { + it('lets owner hr and admin approve, not staff', () => { + expect(canApproveOfficeApply(['owner'])).toBe(true); + expect(canApproveOfficeApply(['hr'])).toBe(true); + expect(canApproveOfficeApply(['admin'])).toBe(true); + expect(canApproveOfficeApply(['biz_director'])).toBe(true); + expect(canApproveOfficeApply(['employee'])).toBe(false); + expect(canApproveOfficeApply(['pm'])).toBe(false); + }); + + it('keeps the three office apply kinds', () => { + expect(Object.values(OFFICE_APPLY_KIND)).toEqual(['ASSET_BUY', 'ASSET_USE', 'VEHICLE']); + }); +}); diff --git a/apps/api/src/office/office-apply.ts b/apps/api/src/office/office-apply.ts new file mode 100644 index 0000000..d78af33 --- /dev/null +++ b/apps/api/src/office/office-apply.ts @@ -0,0 +1,765 @@ +import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { existsSync, unlinkSync } from 'fs'; +import { join } from 'path'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { PrismaService } from '../prisma/prisma.service'; +import { ASSET_STATUS, PURCHASE_STATUS } from '../system/ops-workflow'; +import { sealBucket } from '../seal/seal-workflow'; +import { emitSms } from '../sms/sms-bus'; +import { DIRECTOR_ROLES } from '../common/role-groups'; + +export const OFFICE_APPLY_KIND = { + ASSET_BUY: 'ASSET_BUY', + ASSET_USE: 'ASSET_USE', + VEHICLE: 'VEHICLE', +} as const; + +export const OFFICE_APPLY_STATUS = { + PENDING: 'PENDING', + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', +} as const; + +export const OFFICE_BIZ = { + APPLY: 'OFFICE_APPLY', +} as const; + +export const OFFICE_APPLY_APPROVER_ROLES = ['admin', 'owner', 'hr', 'biz_director'] as const; + +const APPLY_INCLUDE = { + applicant: { + select: { + id: true, + displayName: true, + username: true, + employee: { select: { name: true, department: { select: { name: true } } } }, + }, + }, + decidedBy: { select: { id: true, displayName: true, username: true } }, + registeredAsset: { select: { id: true, kind: true, name: true, code: true, status: true, usage: true } }, + project: { select: { id: true, projectNo: true, name: true } }, + purchase: { select: { id: true, title: true, status: true, amount: true } }, + occupancy: { select: { id: true, status: true, assetName: true } }, + inboundAsset: { select: { id: true, name: true, status: true, kind: true } }, +} satisfies Prisma.OfficeApplyInclude; + +const VEHICLE_NAME = /车|车辆|轿车|商务车|越野|皮卡|面包车/; + +export function canApproveOfficeApply(roles: string[]) { + return OFFICE_APPLY_APPROVER_ROLES.some((r) => roles.includes(r)); +} + +function kindLabel(kind: string) { + if (kind === OFFICE_APPLY_KIND.ASSET_BUY) return '资产购置'; + if (kind === OFFICE_APPLY_KIND.ASSET_USE) return '资产领用'; + if (kind === OFFICE_APPLY_KIND.VEHICLE) return '用车'; + return kind; +} + +function assetKindLabel(kind?: string | null) { + if (kind === 'DIGITAL') return '数字资产'; + if (kind === 'FIXED') return '固定资产'; + return '资产'; +} + +function isOverdue(d?: Date | null) { + return Boolean(d && d.getTime() < Date.now()); +} + +export async function listOfficeApplies(prisma: PrismaService, user: AuthUser, bucket?: string, scope?: string) { + const where: Prisma.OfficeApplyWhereInput = { tenantId: '1' }; + const mineOnly = !canApproveOfficeApply(user.roles) || bucket === 'mine' || scope === 'mine'; + if (mineOnly) { + where.applicantId = user.id; + } + if (bucket === 'pending') where.status = OFFICE_APPLY_STATUS.PENDING; + else if (bucket === 'approved') where.status = OFFICE_APPLY_STATUS.APPROVED; + else if (bucket === 'rejected') where.status = OFFICE_APPLY_STATUS.REJECTED; + const items = await prisma.officeApply.findMany({ + where, + include: APPLY_INCLUDE, + orderBy: { createdAt: 'desc' }, + }); + const filtered = + bucket === 'occupying' + ? items.filter((i) => i.occupancy?.status === ASSET_STATUS.IN_USE) + : items; + return { items: filtered }; +} + +export async function createOfficeApply( + prisma: PrismaService, + user: AuthUser, + dto: { + kind?: string; + title?: string; + reason?: string; + assetKind?: string; + registeredAssetId?: string; + amount?: number; + destination?: string; + startAt?: string; + endAt?: string; + projectId?: string; + }, +) { + const kind = dto.kind?.trim(); + if (!kind || !Object.values(OFFICE_APPLY_KIND).includes(kind as (typeof OFFICE_APPLY_KIND)[keyof typeof OFFICE_APPLY_KIND])) { + throw new BadRequestException('请选择申请类型:资产购置、资产领用或用车'); + } + let assetKind = dto.assetKind?.trim() || null; + if (kind === OFFICE_APPLY_KIND.VEHICLE) assetKind = 'FIXED'; + if (kind !== OFFICE_APPLY_KIND.VEHICLE && assetKind !== 'DIGITAL' && assetKind !== 'FIXED') { + throw new BadRequestException('请先选择数字资产或固定资产'); + } + + let registeredAsset: { id: string; name: string; kind: string; code: string | null; status: string } | null = null; + if (dto.registeredAssetId) { + registeredAsset = await prisma.registeredAsset.findFirst({ + where: { id: dto.registeredAssetId, tenantId: '1' }, + select: { id: true, name: true, kind: true, code: true, status: true }, + }); + if (!registeredAsset) throw new NotFoundException('台账中没有该资产'); + if (registeredAsset.status === 'PENDING_IN') throw new BadRequestException('该资产尚未入库,不能领用或用车'); + if (assetKind && registeredAsset.kind !== assetKind) { + throw new BadRequestException('所选资产与类型不符'); + } + } + if (kind === OFFICE_APPLY_KIND.ASSET_USE && !registeredAsset) { + throw new BadRequestException('领用请从资产台账选择具体资产'); + } + if (registeredAsset && (kind === OFFICE_APPLY_KIND.ASSET_USE || kind === OFFICE_APPLY_KIND.VEHICLE)) { + const busy = await prisma.assetOccupancy.findFirst({ + where: { registeredAssetId: registeredAsset.id, status: ASSET_STATUS.IN_USE }, + }); + if (busy) throw new BadRequestException(`「${registeredAsset.name}」已被 ${busy.holder} 占用,请先交回`); + } + if (kind === OFFICE_APPLY_KIND.ASSET_BUY && !(Number(dto.amount) > 0)) { + throw new BadRequestException('购置请填写金额'); + } + if (kind === OFFICE_APPLY_KIND.VEHICLE) { + if (!dto.destination?.trim()) throw new BadRequestException('请填写用车目的地'); + if (!dto.startAt || !dto.endAt) throw new BadRequestException('请填写用车起止时间'); + } + if (dto.projectId) { + const project = await prisma.project.findFirst({ where: { id: dto.projectId, tenantId: '1' } }); + if (!project) throw new NotFoundException('项目不存在'); + } + + const title = + dto.title?.trim() || + (kind === OFFICE_APPLY_KIND.ASSET_BUY + ? `购置${assetKindLabel(assetKind)}` + : kind === OFFICE_APPLY_KIND.ASSET_USE + ? `领用 ${registeredAsset?.name}` + : `用车 ${dto.destination?.trim()}`); + + const row = await prisma.$transaction(async (tx) => { + const created = await tx.officeApply.create({ + data: { + tenantId: '1', + kind, + title, + reason: dto.reason?.trim() || null, + assetKind, + registeredAssetId: registeredAsset?.id || null, + amount: kind === OFFICE_APPLY_KIND.ASSET_BUY ? dto.amount : null, + destination: dto.destination?.trim() || null, + startAt: dto.startAt ? new Date(dto.startAt) : null, + endAt: dto.endAt ? new Date(dto.endAt) : null, + projectId: dto.projectId || null, + applicantId: user.id, + status: OFFICE_APPLY_STATUS.PENDING, + }, + }); + await openApprovals(tx, created.id, `${kindLabel(kind)}:${title}`); + return tx.officeApply.findFirstOrThrow({ where: { id: created.id }, include: APPLY_INCLUDE }); + }); + return row; +} + +export async function decideOfficeApply( + prisma: PrismaService, + approvalId: string | null, + applyId: string | null, + user: AuthUser, + dto: { result: 'APPROVED' | 'REJECTED'; comment?: string }, +) { + if (!canApproveOfficeApply(user.roles)) throw new ForbiddenException('无权审批办公申请'); + let id = applyId; + if (approvalId) { + const task = await prisma.approvalTask.findFirst({ where: { id: approvalId, tenantId: '1' } }); + if (!task || task.bizType !== OFFICE_BIZ.APPLY || !task.bizId) { + throw new NotFoundException('审批任务不存在'); + } + id = task.bizId; + } + if (!id) throw new BadRequestException('缺少申请'); + const row = await prisma.officeApply.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('办公申请不存在'); + if (row.status !== OFFICE_APPLY_STATUS.PENDING) throw new BadRequestException('该申请已办理'); + + return prisma.$transaction(async (tx) => { + let purchaseId: string | null = row.purchaseId; + let occupancyId: string | null = row.occupancyId; + let inboundAssetId: string | null = row.inboundAssetId; + if (dto.result === 'APPROVED') { + if (row.kind === OFFICE_APPLY_KIND.ASSET_BUY) { + const purchase = await tx.purchaseRequest.create({ + data: { + tenantId: '1', + title: row.title, + amount: row.amount ?? 0, + projectId: row.projectId, + remark: `办公购置 ${row.id}`, + applicantId: row.applicantId, + decidedById: user.id, + decidedAt: new Date(), + status: PURCHASE_STATUS.APPROVED, + }, + }); + purchaseId = purchase.id; + const inbound = await tx.registeredAsset.create({ + data: { + tenantId: '1', + kind: row.assetKind === 'DIGITAL' ? 'DIGITAL' : 'FIXED', + name: row.title, + amount: row.amount, + remark: `购置申请待入库。${row.reason || ''}`.trim(), + status: 'PENDING_IN', + usage: VEHICLE_NAME.test(row.title) ? 'VEHICLE' : 'GENERAL', + createdById: row.applicantId, + }, + }); + inboundAssetId = inbound.id; + if (Number(row.amount) > 0) { + const year = new Date().getFullYear(); + const count = await tx.paymentRecord.count({ where: { payNo: { startsWith: `PAY-${year}-` } } }); + await tx.paymentRecord.create({ + data: { + tenantId: '1', + payNo: `PAY-${year}-${String(count + 1).padStart(5, '0')}`, + kind: 'PURCHASE', + refId: purchase.id, + amount: row.amount ?? 0, + remark: `采购 ${row.title}`, + createdById: user.id, + status: 'TODO', + }, + }); + } + } + if (row.kind === OFFICE_APPLY_KIND.ASSET_USE || (row.kind === OFFICE_APPLY_KIND.VEHICLE && row.registeredAssetId)) { + if (!row.registeredAssetId) throw new BadRequestException('领用缺少台账资产'); + occupancyId = await occupyRegistered(tx, { + registeredAssetId: row.registeredAssetId, + holderId: row.applicantId, + projectId: row.projectId, + dueAt: row.endAt || row.startAt, + }); + await tx.todoTask.create({ + data: { + tenantId: '1', + title: `交回资产:${row.title}`, + bizType: OFFICE_BIZ.APPLY, + bizId: row.id, + assigneeId: row.applicantId, + dueAt: row.endAt, + }, + }); + } + } + await tx.officeApply.update({ + where: { id: row.id }, + data: { + status: dto.result === 'APPROVED' ? OFFICE_APPLY_STATUS.APPROVED : OFFICE_APPLY_STATUS.REJECTED, + remark: dto.comment?.trim() || row.remark, + decidedById: user.id, + decidedAt: new Date(), + purchaseId, + occupancyId, + inboundAssetId, + }, + }); + await closeApprovals(tx, row.id, dto.result, user.id); + return tx.officeApply.findFirstOrThrow({ where: { id: row.id }, include: APPLY_INCLUDE }); + }); +} + +export async function deleteOfficeApply(prisma: PrismaService, id: string, user: AuthUser) { + const row = await prisma.officeApply.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('办公申请不存在'); + if (row.applicantId !== user.id && !user.roles.includes('admin')) { + throw new ForbiddenException('只能删除自己的申请'); + } + if (row.status !== OFFICE_APPLY_STATUS.PENDING && !user.roles.includes('admin')) { + throw new BadRequestException('已办理的申请不能删除'); + } + await prisma.$transaction(async (tx) => { + await tx.approvalTask.updateMany({ + where: { bizType: OFFICE_BIZ.APPLY, bizId: id, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + await tx.officeApply.delete({ where: { id } }); + }); + return { ok: true }; +} + +export async function listOfficeCalendar(prisma: PrismaService, user: AuthUser) { + const where: Prisma.OfficeApplyWhereInput = { + tenantId: '1', + kind: OFFICE_APPLY_KIND.VEHICLE, + status: OFFICE_APPLY_STATUS.APPROVED, + startAt: { not: null }, + }; + if (!canApproveOfficeApply(user.roles)) where.applicantId = user.id; + const items = await prisma.officeApply.findMany({ + where, + include: { registeredAsset: { select: { name: true, code: true } } }, + orderBy: { startAt: 'asc' }, + }); + return items.map((r) => ({ + id: `vehicle-${r.id}`, + kind: 'VEHICLE', + title: `用车 ${r.destination || r.title}`, + start: r.startAt, + place: r.registeredAsset?.name || r.destination, + ref: r.registeredAsset?.code || null, + overdue: isOverdue(r.endAt), + path: '/office/apply', + })); +} + +async function occupyRegistered( + tx: Prisma.TransactionClient, + dto: { registeredAssetId: string; holderId: string; projectId?: string | null; dueAt?: Date | null }, +) { + const asset = await tx.registeredAsset.findFirst({ where: { id: dto.registeredAssetId, tenantId: '1' } }); + if (!asset) throw new NotFoundException('台账中没有该资产'); + if (asset.status === 'PENDING_IN') throw new BadRequestException('该资产尚未入库'); + const busy = await tx.assetOccupancy.findFirst({ + where: { registeredAssetId: asset.id, status: ASSET_STATUS.IN_USE }, + }); + if (busy) throw new BadRequestException(`「${asset.name}」已被 ${busy.holder} 占用,请先交回`); + const holder = await tx.user.findFirst({ where: { id: dto.holderId } }); + const occupancy = await tx.assetOccupancy.create({ + data: { + tenantId: '1', + assetName: asset.name, + holder: holder?.displayName || '未登记', + holderId: dto.holderId, + projectId: dto.projectId || null, + registeredAssetId: asset.id, + dueAt: dto.dueAt || null, + status: ASSET_STATUS.IN_USE, + }, + }); + return occupancy.id; +} + +async function openApprovals(tx: Prisma.TransactionClient, applyId: string, title: string) { + const approvers = await tx.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + roles: { some: { role: { code: { in: [...OFFICE_APPLY_APPROVER_ROLES] } } } }, + }, + }); + const ids = [...new Set(approvers.map((u) => u.id))]; + if (!ids.length) throw new BadRequestException('没有可审批办公申请的负责人'); + await tx.approvalTask.createMany({ + data: ids.map((assigneeId) => ({ + tenantId: '1', + title, + bizType: OFFICE_BIZ.APPLY, + bizId: applyId, + assigneeId, + })), + }); + emitSms('OFFICE_APPROVAL', { + userIds: ids, + params: { content: `待审批:${title}`, project_name: title, apply_no: 'OA' }, + }); +} + +async function closeApprovals(tx: Prisma.TransactionClient, bizId: string, result: string, actorId: string) { + await tx.approvalTask.updateMany({ + where: { bizType: OFFICE_BIZ.APPLY, bizId, assigneeId: actorId, status: 'PENDING' }, + data: { status: result }, + }); + await tx.approvalTask.updateMany({ + where: { bizType: OFFICE_BIZ.APPLY, bizId, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); +} + +export function isVehicleNamed(name: string, spec?: string | null) { + return VEHICLE_NAME.test(`${name}${spec || ''}`); +} + +export async function listMyFlow(prisma: PrismaService, user: AuthUser, bucket?: string, kind?: string) { + const [applies, seals, leaves, expenses, loans] = await Promise.all([ + prisma.officeApply.findMany({ + where: { tenantId: '1', applicantId: user.id }, + include: { occupancy: { select: { status: true } } }, + orderBy: { createdAt: 'desc' }, + }), + prisma.sealRequest.findMany({ + where: { tenantId: '1', applicantId: user.id }, + orderBy: { createdAt: 'desc' }, + }), + user.employeeId + ? prisma.leaveRequest.findMany({ + where: { tenantId: '1', employeeId: user.employeeId }, + orderBy: { createdAt: 'desc' }, + }) + : Promise.resolve([]), + prisma.expenseClaim.findMany({ + where: { tenantId: '1', applicantId: user.id }, + orderBy: { createdAt: 'desc' }, + take: 50, + }), + prisma.loanRecord.findMany({ + where: { tenantId: '1', applicantId: user.id }, + orderBy: { createdAt: 'desc' }, + take: 50, + }), + ]); + const items = [ + ...applies.map((r) => ({ + id: r.id, + source: 'OFFICE', + kind: r.kind, + title: r.title, + status: r.status, + bucket: + r.occupancy?.status === ASSET_STATUS.IN_USE + ? 'occupying' + : r.status === 'PENDING' + ? 'pending' + : r.status === 'REJECTED' + ? 'rejected' + : 'approved', + createdAt: r.createdAt, + path: '/office/apply', + })), + ...seals.map((r) => ({ + id: r.id, + source: 'SEAL', + kind: 'SEAL', + title: `用章 ${r.sealType}:${r.reason}`, + status: r.status, + bucket: sealBucket(r), + createdAt: r.createdAt, + path: '/office/seal-apply', + })), + ...leaves.map((r) => ({ + id: r.id, + source: 'HR', + kind: r.kind, + title: r.reason || r.kind, + status: r.status, + bucket: r.status === 'PENDING' ? 'pending' : r.status === 'REJECTED' ? 'rejected' : 'approved', + createdAt: r.createdAt, + path: '/office/hr-apply', + })), + ...expenses.map((r) => ({ + id: r.id, + source: 'EXPENSE', + kind: 'EXPENSE', + title: r.purpose || r.claimNo, + status: r.status, + bucket: r.status === 'REJECTED' ? 'rejected' : r.status === 'APPROVED' ? 'approved' : 'pending', + createdAt: r.createdAt, + path: `/office/expenses/${r.id}`, + })), + ...loans.map((r) => ({ + id: r.id, + source: 'EXPENSE', + kind: 'LOAN', + title: r.purpose || r.loanNo, + status: r.status, + bucket: r.status === 'REJECTED' ? 'rejected' : r.status === 'OPEN' || r.status === 'OFFSET' ? 'approved' : 'pending', + createdAt: r.createdAt, + path: `/office/loans/${r.id}`, + })), + ].sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); + return { + items: items.filter((i) => { + if (kind && i.source !== kind) return false; + if (!bucket) return true; + return i.bucket === bucket; + }), + }; +} + +export function canManageNotice(roles: string[], permissions: string[] = []) { + if (['admin', 'owner', 'hr', ...DIRECTOR_ROLES].some((r) => roles.includes(r))) return true; + return permissions.some((p) => p === 'hr' || p === 'hr:notices' || p === 'hr:policies'); +} + +function stripNoticeText(html: string) { + return html + .replace(/<[^>]*>/g, ' ') + .replace(/ /gi, ' ') + .replace(/\s+/g, ' ') + .trim(); +} + +async function attachNoticeFiles(prisma: PrismaService, items: T[]) { + if (!items.length) return items.map((row) => ({ ...row, files: [], excerpt: '' })); + const files = await prisma.fileAsset.findMany({ + where: { tenantId: '1', bizType: 'NOTICE', bizId: { in: items.map((i) => i.id) } }, + orderBy: { createdAt: 'asc' }, + select: { id: true, fileName: true, size: true, mimeType: true, bizType: true, bizId: true }, + }); + const byId = new Map(); + for (const f of files) { + const list = byId.get(f.bizId) || []; + list.push(f); + byId.set(f.bizId, list); + } + return items.map((row) => ({ + ...row, + files: byId.get(row.id) || [], + excerpt: stripNoticeText(row.content || '').slice(0, 80), + })); +} + +export async function listNotices(prisma: PrismaService, publishedOnly = true, kind?: string, user?: AuthUser) { + const kindFilter = kind === 'POLICY' || kind === 'ANNOUNCEMENT' ? kind : undefined; + const items = await prisma.notice.findMany({ + where: { + tenantId: '1', + ...(publishedOnly ? { status: 'PUBLISHED' } : {}), + ...(kindFilter ? { kind: kindFilter } : {}), + }, + include: { createdBy: { select: { displayName: true } } }, + orderBy: { createdAt: 'desc' }, + take: 80, + }); + const visible = publishedOnly && user ? await filterNoticesForUser(prisma, items, user) : items; + return { items: await attachNoticeFiles(prisma, visible) }; +} + +export async function getNotice(prisma: PrismaService, user: AuthUser, id: string) { + const row = await prisma.notice.findFirst({ + where: { id, tenantId: '1' }, + include: { createdBy: { select: { displayName: true } } }, + }); + if (!row) throw new NotFoundException('通知不存在'); + const manage = canManageNotice(user.roles, user.permissions); + if (row.status !== 'PUBLISHED' && !manage) { + throw new NotFoundException('通知不存在'); + } + if (row.status === 'PUBLISHED' && !manage) { + const ok = (await filterNoticesForUser(prisma, [row], user)).length > 0; + if (!ok) throw new NotFoundException('通知不存在'); + } + const [withFiles] = await attachNoticeFiles(prisma, [row]); + return withFiles; +} + +export async function canSeeNotice( + prisma: PrismaService, + user: AuthUser, + row: { status: string; createdById: string; audience?: string | null; departmentIds?: string[]; userIds?: string[] }, +) { + if (canManageNotice(user.roles, user.permissions)) return true; + if (row.status !== 'PUBLISHED') return row.createdById === user.id; + return (await filterNoticesForUser(prisma, [row], user)).length > 0; +} + +type NoticeAudienceDto = { + title?: string; + content?: string; + kind?: string; + status?: string; + audience?: string; + departmentIds?: string[]; + userIds?: string[]; +}; + +function normalizeAudience(dto: NoticeAudienceDto, fallback?: { audience: string; departmentIds: string[]; userIds: string[] }) { + const audience = dto.audience || fallback?.audience || 'ALL'; + const departmentIds = audience === 'DEPT' ? [...new Set((dto.departmentIds || fallback?.departmentIds || []).filter(Boolean))] : []; + const userIds = audience === 'USER' ? [...new Set((dto.userIds || fallback?.userIds || []).filter(Boolean))] : []; + if (!['ALL', 'DEPT', 'USER'].includes(audience)) { + throw new BadRequestException('接收范围只能是全公司、按部门或按人员'); + } + return { audience, departmentIds, userIds }; +} + +async function noticeSmsUserIds( + prisma: PrismaService, + audience: string, + departmentIds: string[], + userIds: string[], +) { + if (audience === 'USER') return userIds; + if (audience === 'DEPT') { + const depts = await expandDeptIds(prisma, departmentIds); + const users = await prisma.user.findMany({ + where: { tenantId: '1', status: 'ACTIVE', employee: { departmentId: { in: depts } } }, + select: { id: true, employee: { select: { employmentStatus: true } } }, + }); + return users.filter((u) => u.employee?.employmentStatus !== 'LEFT').map((u) => u.id); + } + return []; +} + +async function expandDeptIds(prisma: PrismaService, ids: string[]) { + if (!ids.length) return []; + const all = await prisma.department.findMany({ + where: { tenantId: '1' }, + select: { id: true, parentId: true }, + }); + const children = new Map(); + for (const d of all) { + if (!d.parentId) continue; + const list = children.get(d.parentId) || []; + list.push(d.id); + children.set(d.parentId, list); + } + const out = new Set(ids); + const walk = (id: string) => { + for (const c of children.get(id) || []) { + if (!out.has(c)) { + out.add(c); + walk(c); + } + } + }; + ids.forEach(walk); + return [...out]; +} + +async function userDeptChain(prisma: PrismaService, departmentId?: string | null) { + if (!departmentId) return []; + const all = await prisma.department.findMany({ + where: { tenantId: '1' }, + select: { id: true, parentId: true }, + }); + const parentOf = new Map(all.map((d) => [d.id, d.parentId])); + const chain: string[] = []; + let cur: string | null | undefined = departmentId; + const seen = new Set(); + while (cur && !seen.has(cur)) { + chain.push(cur); + seen.add(cur); + cur = parentOf.get(cur); + } + return chain; +} + +async function filterNoticesForUser( + prisma: PrismaService, + items: T[], + user: AuthUser, +) { + const chain = await userDeptChain(prisma, user.departmentId); + return items.filter((row) => { + const audience = row.audience || 'ALL'; + if (audience === 'ALL') return true; + if (audience === 'USER') return (row.userIds || []).includes(user.id); + if (audience === 'DEPT') return (row.departmentIds || []).some((id) => chain.includes(id)); + return true; + }); +} + +export async function createNotice(prisma: PrismaService, user: AuthUser, dto: NoticeAudienceDto) { + if (!canManageNotice(user.roles, user.permissions)) throw new ForbiddenException('无权发布公告'); + const kind = dto.kind === 'POLICY' ? 'POLICY' : 'ANNOUNCEMENT'; + const publish = dto.status === 'PUBLISHED'; + const title = (dto.title || '').trim() || (publish ? '' : '未命名'); + const content = dto.content ?? ''; + if (publish && (!title || title === '未命名' || !stripNoticeText(content))) { + throw new BadRequestException('请填写标题和正文'); + } + const audience = normalizeAudience(dto); + if (publish && audience.audience === 'DEPT' && !audience.departmentIds.length) { + throw new BadRequestException('请选择要发送的部门'); + } + if (publish && audience.audience === 'USER' && !audience.userIds.length) { + throw new BadRequestException('请选择要发送的人员'); + } + const row = await prisma.notice.create({ + data: { + tenantId: '1', + title: title || '未命名', + content, + kind, + status: publish ? 'PUBLISHED' : 'DRAFT', + createdById: user.id, + ...audience, + }, + include: { createdBy: { select: { displayName: true } } }, + }); + if (publish) { + const smsUserIds = await noticeSmsUserIds(prisma, audience.audience, audience.departmentIds, audience.userIds); + emitSms('NOTICE', { + userIds: smsUserIds.length ? smsUserIds : undefined, + params: { content: `新${kind === 'POLICY' ? '制度' : '公告'}:${row.title}` }, + }); + } + const [withFiles] = await attachNoticeFiles(prisma, [row]); + return withFiles; +} + +export async function updateNotice(prisma: PrismaService, user: AuthUser, id: string, dto: NoticeAudienceDto) { + if (!canManageNotice(user.roles, user.permissions)) throw new ForbiddenException('无权编辑公告'); + const row = await prisma.notice.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('通知不存在'); + const publish = dto.status === 'PUBLISHED'; + const title = dto.title !== undefined ? dto.title.trim() : row.title; + const content = dto.content !== undefined ? dto.content : row.content; + if (publish && (!title || title === '未命名' || !stripNoticeText(content))) { + throw new BadRequestException('请填写标题和正文后再发布'); + } + const audience = normalizeAudience(dto, { + audience: row.audience, + departmentIds: row.departmentIds, + userIds: row.userIds, + }); + if (publish && audience.audience === 'DEPT' && !audience.departmentIds.length) { + throw new BadRequestException('请选择要发送的部门'); + } + if (publish && audience.audience === 'USER' && !audience.userIds.length) { + throw new BadRequestException('请选择要发送的人员'); + } + const next = await prisma.notice.update({ + where: { id }, + data: { + title: title || '未命名', + content, + ...audience, + ...(dto.status === 'DRAFT' || publish ? { status: publish ? 'PUBLISHED' : 'DRAFT' } : {}), + }, + include: { createdBy: { select: { displayName: true } } }, + }); + if (publish && row.status !== 'PUBLISHED') { + const smsUserIds = await noticeSmsUserIds(prisma, audience.audience, audience.departmentIds, audience.userIds); + emitSms('NOTICE', { + userIds: smsUserIds.length ? smsUserIds : undefined, + params: { content: `新${next.kind === 'POLICY' ? '制度' : '公告'}:${next.title}` }, + }); + } + const [withFiles] = await attachNoticeFiles(prisma, [next]); + return withFiles; +} + +export async function deleteNotice(prisma: PrismaService, user: AuthUser, id: string) { + if (!canManageNotice(user.roles, user.permissions)) throw new ForbiddenException('无权删除公告'); + const row = await prisma.notice.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('公告不存在'); + const files = await prisma.fileAsset.findMany({ where: { tenantId: '1', bizType: 'NOTICE', bizId: id } }); + const root = join(process.cwd(), 'uploads'); + for (const f of files) { + const full = join(root, f.storageKey); + if (existsSync(full)) unlinkSync(full); + } + await prisma.fileAsset.deleteMany({ where: { tenantId: '1', bizType: 'NOTICE', bizId: id } }); + await prisma.notice.delete({ where: { id } }); + return { ok: true }; +} diff --git a/apps/api/src/office/office.controller.ts b/apps/api/src/office/office.controller.ts new file mode 100644 index 0000000..7b80cb6 --- /dev/null +++ b/apps/api/src/office/office.controller.ts @@ -0,0 +1,740 @@ +import { Body, Controller, ForbiddenException, Get, Param, ParseUUIDPipe, Patch, Post, Query, BadRequestException, NotFoundException, Delete, Req, StreamableFile } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { IsIn, IsOptional, IsString } from 'class-validator'; +import { PrismaService } from '../prisma/prisma.service'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { BiddingService } from '../bidding/bidding.service'; +import { ContractService } from '../contract/contract.service'; +import { FinanceService } from '../finance/finance.service'; +import { HrService } from '../hr/hr.service'; +import { SealService } from '../seal/seal.service'; +import { CONTRACT_BIZ } from '../contract/contract-workflow'; +import { FINANCE_BIZ } from '../finance/finance-workflow'; +import { HR_BIZ } from '../hr/hr-workflow'; +import { SEAL_BIZ } from '../seal/seal-workflow'; +import { canAccessBidModule } from '../bidding/bid-workflow'; +import { canAccessSeal } from '../seal/seal-workflow'; +import { canAssignWork } from '../common/role-groups'; +import { LEAVE_STATUS } from '../hr/hr-workflow'; +import { emitSms } from '../sms/sms-bus'; +import { emitIm } from '../im/im-bus'; +import { dailyGreeting, geocodeAddress, locateByIp, reverseGeocode, suggestPlaces, tencentMapKey, weatherByLocation } from '../common/tencent-lbs'; +import { LOG_ACTION, writeOpLog } from '../system/op-log'; +import { + getReport, + hoursDraft, + listReports, + resolveReportRoute, + saveReport, + submitReport, +} from './work-report'; +import { + OFFICE_BIZ, + canApproveOfficeApply, + canManageNotice, + createNotice, + createOfficeApply, + decideOfficeApply, + deleteNotice, + deleteOfficeApply, + getNotice, + listMyFlow, + listNotices, + listOfficeApplies, + listOfficeCalendar, + updateNotice, +} from './office-apply'; + +class DecideDto { + @IsIn(['APPROVED', 'REJECTED']) + result!: 'APPROVED' | 'REJECTED'; + @IsOptional() + @IsString() + comment?: string; +} + +@ApiTags('office') +@ApiBearerAuth() +@Controller('office') +export class OfficeController { + constructor( + private readonly prisma: PrismaService, + private readonly bidding: BiddingService, + private readonly contracts: ContractService, + private readonly finance: FinanceService, + private readonly hr: HrService, + private readonly seals: SealService, + ) {} + + @Get('overview') + async overview(@CurrentUser() user: AuthUser) { + const canBid = canAccessBidModule(user.roles, user.permissions); + const canSeal = canAccessSeal(user.roles, user.permissions); + const [todos, approvals, bids, expenses, loans, leaves, overdueTodos, extras, officePending] = await Promise.all([ + this.prisma.todoTask.count({ + where: { assigneeId: user.id, status: 'OPEN' }, + }), + this.prisma.approvalTask.count({ + where: { assigneeId: user.id, status: 'PENDING' }, + }), + canBid + ? this.prisma.bidCase.count({ + where: { tenantId: '1', status: { notIn: ['TERMINATED', 'LOST', 'FAILED'] } }, + }) + : Promise.resolve(0), + this.prisma.expenseClaim.count({ + where: { applicantId: user.id, status: { in: ['DRAFT', 'PENDING', 'REJECTED'] } }, + }), + this.prisma.loanRecord.count({ + where: { applicantId: user.id, status: { in: ['DRAFT', 'PENDING', 'OPEN'] } }, + }), + user.employeeId + ? this.prisma.leaveRequest.count({ + where: { employeeId: user.employeeId, status: LEAVE_STATUS.PENDING }, + }) + : Promise.resolve(0), + this.prisma.todoTask.count({ + where: { assigneeId: user.id, status: 'OPEN', dueAt: { lt: new Date() } }, + }), + this.seals.overviewExtras(user), + this.prisma.officeApply.count({ + where: { applicantId: user.id, status: 'PENDING' }, + }), + ]); + return { + pendingTodos: todos, + pendingApprovals: approvals, + myPending: expenses + loans + leaves + officePending, + canBid, + canSeal, + canApprove: true, + activeBids: canBid ? bids : 0, + myExpenseDrafts: expenses + loans, + overdueTodos, + expiringQualifications: canSeal ? extras.expiringQualifications : 0, + overdueBorrows: canSeal ? extras.overdueBorrows : 0, + overdueSeals: canSeal ? extras.overdueSeals : 0, + upcomingOpens: canBid ? extras.upcomingOpens : 0, + warnings: { + qualifications: canSeal ? extras.warnings.qualifications : [], + borrows: canSeal ? extras.warnings.borrows : [], + seals: canSeal ? extras.warnings.seals : [], + opens: canBid ? extras.warnings.opens : [], + }, + }; + } + + @Get('todos') + async todos(@CurrentUser() user: AuthUser) { + const items = await this.prisma.todoTask.findMany({ + where: { assigneeId: user.id }, + orderBy: { createdAt: 'desc' }, + }); + return this.withBiz(items); + } + + @Get('approvals') + async approvals(@CurrentUser() user: AuthUser) { + const items = await this.prisma.approvalTask.findMany({ + where: { assigneeId: user.id }, + orderBy: { createdAt: 'desc' }, + }); + return this.withBiz(items); + } + + @Post('approvals/:id/decide') + async decide( + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: DecideDto, + @CurrentUser() user: AuthUser, + ) { + const task = await this.prisma.approvalTask.findFirst({ + where: { id, tenantId: '1' }, + }); + let result: unknown; + if (task?.bizType === CONTRACT_BIZ.APPROVAL) { + result = await this.contracts.decideApproval(id, user, dto); + } else if (task?.bizType === FINANCE_BIZ.EXPENSE || task?.bizType === FINANCE_BIZ.LOAN) { + result = await this.finance.decideApproval(id, user, dto); + } else if (task?.bizType === SEAL_BIZ.APPROVAL) { + result = await this.seals.decideApproval(id, user, dto); + } else if (task?.bizType === HR_BIZ.LEAVE) { + result = await this.hr.decideLeaveApproval(id, user, dto); + } else if (task?.bizType === OFFICE_BIZ.APPLY) { + result = await decideOfficeApply(this.prisma, id, null, user, dto); + } else { + result = await this.bidding.decideApproval(id, user, dto); + } + await writeOpLog(this.prisma, { + userId: user.id, + username: user.username, + action: LOG_ACTION.APPROVAL, + path: `/office/approvals/${id}/decide`, + detail: `${dto.result} ${task?.title || id}`, + }); + return result; + } + + @Get('calendar') + async calendar(@CurrentUser() user: AuthUser) { + const items = await this.seals.calendar(user); + const extra = await listOfficeCalendar(this.prisma, user); + return [...items, ...extra].sort( + (a, b) => new Date(a.start || 0).getTime() - new Date(b.start || 0).getTime(), + ); + } + + @Get('weather') + async weather( + @CurrentUser() user: AuthUser, + @Query('lat') lat?: string, + @Query('lng') lng?: string, + @Req() req?: { ip?: string; headers?: Record }, + ) { + const greeting = dailyGreeting(user.displayName, user.id); + let latN = Number(lat); + let lngN = Number(lng); + let city = ''; + if (!Number.isFinite(latN) || !Number.isFinite(lngN)) { + const fwd = req?.headers?.['x-forwarded-for']; + const ip = String(Array.isArray(fwd) ? fwd[0] : fwd || req?.ip || '') + .split(',')[0] + .trim(); + const located = await locateByIp(ip.startsWith('127.') || ip === '::1' ? undefined : ip); + if (located) { + latN = located.lat; + lngN = located.lng; + city = located.city; + } + } else { + const rev = await reverseGeocode(latN, lngN); + city = rev?.city || ''; + } + const weather = Number.isFinite(latN) && Number.isFinite(lngN) ? await weatherByLocation(latN, lngN, city) : null; + return { + greeting, + city: weather?.city || city || '', + lat: Number.isFinite(latN) ? latN : undefined, + lng: Number.isFinite(lngN) ? lngN : undefined, + weather: weather?.weather || '', + temperature: weather?.temperature, + wind: weather?.wind, + advice: weather?.advice || '', + summary: weather + ? `当前${weather.city}市天气${weather.weather}${weather.temperature != null ? ` ${weather.temperature}℃` : ''},${weather.advice}` + : '', + }; + } + + @Post('geo/geocode') + async geocode(@Body() body: { address?: string }) { + const address = body.address?.trim(); + if (!address) throw new BadRequestException('请填写地址'); + const hit = await geocodeAddress(address); + if (!hit) throw new BadRequestException('腾讯定位没有解析到这个地址,请写得更完整一些'); + return hit; + } + + @Get('geo/map-config') + mapConfig() { + const key = process.env.TENCENT_MAP_KEY || ''; + if (!key) throw new BadRequestException('还没配置地图密钥'); + return { key }; + } + + @Post('geo/reverse') + async reverse(@Body() body: { lat?: number; lng?: number }) { + const lat = Number(body.lat); + const lng = Number(body.lng); + if (!Number.isFinite(lat) || !Number.isFinite(lng)) throw new BadRequestException('请传入坐标'); + const hit = await reverseGeocode(lat, lng); + if (!hit) throw new BadRequestException('没有反查出这个点的地址'); + return { + lat, + lng, + address: hit.address, + title: hit.title || hit.address, + city: hit.city, + pois: hit.pois || [], + }; + } + + @Get('geo/suggest') + async suggest( + @Query('keyword') keyword?: string, + @Query('lat') lat?: string, + @Query('lng') lng?: string, + ) { + const q = (keyword || '').trim(); + if (!q) return []; + return suggestPlaces(q, Number(lat), Number(lng)); + } + + @Get('geo/static-map') + async staticMap(@Query('lat') lat?: string, @Query('lng') lng?: string) { + const la = Number(lat); + const ln = Number(lng); + if (!Number.isFinite(la) || !Number.isFinite(ln)) throw new BadRequestException('请传入坐标'); + const key = tencentMapKey(); + if (!key) throw new BadRequestException('还没配置地图密钥'); + const url = `https://apis.map.qq.com/ws/staticmap/v2/?size=600*280¢er=${la},${ln}&zoom=16&markers=size:large|color:0x07C160|${la},${ln}&key=${encodeURIComponent(key)}`; + const img = await fetch(url, { headers: { Referer: 'https://oa.fysxkj.com/' } }); + if (!img.ok) throw new BadRequestException('地图瓦片加载失败'); + const buf = Buffer.from(await img.arrayBuffer()); + return new StreamableFile(buf, { type: img.headers.get('content-type') || 'image/png' }); + } + + @Get('flow') + flow( + @CurrentUser() user: AuthUser, + @Query('bucket') bucket?: string, + @Query('kind') kind?: string, + ) { + return listMyFlow(this.prisma, user, bucket, kind); + } + + @Get('notices') + notices( + @CurrentUser() user: AuthUser, + @Query('kind') kind?: string, + @Query('scope') scope?: string, + ) { + const manage = scope === 'manage' && canManageNotice(user.roles, user.permissions); + return listNotices(this.prisma, !manage, kind, user); + } + + @Get('notices/:id') + noticeOne(@CurrentUser() user: AuthUser, @Param('id', ParseUUIDPipe) id: string) { + return getNotice(this.prisma, user, id); + } + + @Post('notices') + createNoticePost( + @CurrentUser() user: AuthUser, + @Body() body: { title?: string; content?: string; kind?: string; status?: string; audience?: string; departmentIds?: string[]; userIds?: string[] }, + ) { + return createNotice(this.prisma, user, body); + } + + @Patch('notices/:id') + patchNotice( + @CurrentUser() user: AuthUser, + @Param('id', ParseUUIDPipe) id: string, + @Body() body: { title?: string; content?: string; status?: string; audience?: string; departmentIds?: string[]; userIds?: string[] }, + ) { + return updateNotice(this.prisma, user, id, body); + } + + @Delete('notices/:id') + removeNotice(@CurrentUser() user: AuthUser, @Param('id', ParseUUIDPipe) id: string) { + return deleteNotice(this.prisma, user, id); + } + + @Get('applies') + applies( + @CurrentUser() user: AuthUser, + @Query('bucket') bucket?: string, + @Query('scope') scope?: string, + ) { + return listOfficeApplies(this.prisma, user, bucket, scope); + } + + @Post('applies') + createApply( + @CurrentUser() user: AuthUser, + @Body() + body: { + kind?: string; + title?: string; + reason?: string; + assetKind?: string; + registeredAssetId?: string; + amount?: number; + destination?: string; + startAt?: string; + endAt?: string; + projectId?: string; + }, + ) { + return createOfficeApply(this.prisma, user, body); + } + + @Get('applies/:id') + async applyOne(@CurrentUser() user: AuthUser, @Param('id', ParseUUIDPipe) id: string) { + const row = await this.prisma.officeApply.findFirst({ + where: { id, tenantId: '1' }, + include: { + applicant: { select: { id: true, displayName: true, username: true, employee: { select: { name: true, department: { select: { name: true } } } } } }, + decidedBy: { select: { id: true, displayName: true, username: true } }, + registeredAsset: { select: { id: true, kind: true, name: true, code: true, status: true } }, + project: { select: { id: true, projectNo: true, name: true } }, + purchase: { select: { id: true, title: true, status: true, amount: true } }, + occupancy: { select: { id: true, status: true, assetName: true } }, + inboundAsset: { select: { id: true, name: true, status: true, kind: true } }, + }, + }); + if (!row) throw new NotFoundException('申请不存在'); + if (row.applicantId !== user.id && !canApproveOfficeApply(user.roles)) { + throw new ForbiddenException('无权查看该申请'); + } + return row; + } + + @Patch('todos/:id') + async patchTodo( + @CurrentUser() user: AuthUser, + @Param('id', ParseUUIDPipe) id: string, + @Body() body: { status?: string }, + ) { + const row = await this.prisma.todoTask.findFirst({ where: { id, assigneeId: user.id } }); + if (!row) throw new NotFoundException('待办不存在'); + const status = body.status === 'DONE' ? 'DONE' : body.status === 'OPEN' ? 'OPEN' : row.status; + return this.prisma.todoTask.update({ where: { id }, data: { status } }); + } + + @Post('applies/:id/decide') + decideApply( + @CurrentUser() user: AuthUser, + @Param('id', ParseUUIDPipe) id: string, + @Body() dto: DecideDto, + ) { + if (!canApproveOfficeApply(user.roles)) throw new ForbiddenException('无权审批办公申请'); + return decideOfficeApply(this.prisma, null, id, user, dto); + } + + @Delete('applies/:id') + removeApply(@CurrentUser() user: AuthUser, @Param('id', ParseUUIDPipe) id: string) { + return deleteOfficeApply(this.prisma, id, user); + } + + @Get('reports/route') + reportRoute(@CurrentUser() user: AuthUser, @Query('periodType') periodType?: string) { + return resolveReportRoute(this.prisma, user, periodType); + } + + @Get('reports/hours-draft') + hoursDraftGet( + @CurrentUser() user: AuthUser, + @Query('periodType') periodType?: string, + @Query('date') date?: string, + ) { + return hoursDraft(this.prisma, user, periodType || 'DAY', date); + } + + @Get('reports') + reports(@CurrentUser() user: AuthUser) { + return listReports(this.prisma, user); + } + + @Get('reports/:id') + report(@CurrentUser() user: AuthUser, @Param('id', ParseUUIDPipe) id: string) { + return getReport(this.prisma, user, id); + } + + @Post('reports') + createReport( + @CurrentUser() user: AuthUser, + @Body() body: { periodType?: string; date?: string; content?: string; submit?: boolean }, + ) { + return saveReport(this.prisma, user, body); + } + + @Patch('reports/:id') + updateReport( + @CurrentUser() user: AuthUser, + @Param('id', ParseUUIDPipe) id: string, + @Body() body: { periodType?: string; date?: string; content?: string; submit?: boolean }, + ) { + return saveReport(this.prisma, user, body, id); + } + + @Post('reports/:id/submit') + submit(@CurrentUser() user: AuthUser, @Param('id', ParseUUIDPipe) id: string) { + return submitReport(this.prisma, user, id); + } + + @Get('my-expenses') + myExpenses(@CurrentUser() user: AuthUser) { + return this.finance.myWorkbench(user); + } + + @Get('work-assignments') + async workAssignments(@CurrentUser() user: AuthUser) { + const manage = canAssignWork(user.roles); + const items = await this.prisma.workAssignment.findMany({ + where: manage + ? { tenantId: '1', OR: [{ createdById: user.id }, { assigneeId: user.id }] } + : { tenantId: '1', assigneeId: user.id }, + include: { + assignee: { select: { id: true, displayName: true, username: true } }, + createdBy: { select: { id: true, displayName: true, username: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + return { items }; + } + + @Post('work-assignments') + async createWork( + @CurrentUser() user: AuthUser, + @Body() body: { title?: string; content?: string; assigneeId?: string; dueAt?: string }, + ) { + if (!canAssignWork(user.roles)) throw new ForbiddenException('无权分配工作'); + const title = body.title?.trim(); + if (!title) throw new BadRequestException('请填写工作标题'); + if (!body.assigneeId) throw new BadRequestException('请选择执行人'); + const assignee = await this.prisma.user.findFirst({ + where: { id: body.assigneeId, tenantId: '1', status: 'ACTIVE' }, + }); + if (!assignee) throw new NotFoundException('执行人不存在'); + return this.prisma.$transaction(async (tx) => { + const row = await tx.workAssignment.create({ + data: { + tenantId: '1', + title, + content: body.content?.trim() || '', + assigneeId: assignee.id, + dueAt: body.dueAt ? new Date(body.dueAt) : null, + createdById: user.id, + }, + include: { + assignee: { select: { id: true, displayName: true, username: true } }, + createdBy: { select: { id: true, displayName: true, username: true } }, + }, + }); + await tx.todoTask.create({ + data: { + tenantId: '1', + title: `工作安排:${title}`, + bizType: 'WORK_ASSIGN', + bizId: row.id, + assigneeId: assignee.id, + dueAt: row.dueAt, + }, + }); + emitSms('WORK_ASSIGN', { + userIds: [assignee.id], + params: { + assigner: user.displayName, + task_name: title, + task_code: `WA${row.id.replace(/-/g, '').slice(0, 10)}`, + }, + actorId: user.id, + }); + emitIm({ + userIds: [assignee.id], + kind: 'todo', + title: `工作安排:${title}`, + body: `${user.displayName} 安排了工作:${title}`, + bizType: 'WORK_ASSIGN', + bizId: row.id, + fromUserId: user.id, + }); + return row; + }); + } + + @Patch('work-assignments/:id') + async updateWork( + @Param('id', ParseUUIDPipe) id: string, + @CurrentUser() user: AuthUser, + @Body() body: { status?: string; title?: string; content?: string }, + ) { + const row = await this.prisma.workAssignment.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('工作安排不存在'); + const canEdit = row.createdById === user.id && canAssignWork(user.roles); + const canDone = row.assigneeId === user.id; + if (!canEdit && !canDone) throw new ForbiddenException('无权修改该工作安排'); + const nextStatus = body.status === 'DONE' || body.status === 'OPEN' ? body.status : undefined; + if (!canEdit && nextStatus !== 'DONE') throw new ForbiddenException('执行人只能标记完成'); + const updated = await this.prisma.workAssignment.update({ + where: { id }, + data: canEdit + ? { + title: body.title?.trim() || undefined, + content: body.content === undefined ? undefined : body.content.trim(), + status: nextStatus, + } + : { status: 'DONE' }, + }); + if (updated.status === 'DONE') { + await this.prisma.todoTask.updateMany({ + where: { bizId: id, bizType: 'WORK_ASSIGN', status: 'OPEN' }, + data: { status: 'DONE' }, + }); + } + return updated; + } + + @Delete('work-assignments/:id') + async deleteWork(@Param('id', ParseUUIDPipe) id: string, @CurrentUser() user: AuthUser) { + const row = await this.prisma.workAssignment.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('工作安排不存在'); + if (row.createdById !== user.id && !user.roles.includes('admin')) { + throw new ForbiddenException('只能删除自己分配的工作'); + } + await this.prisma.$transaction(async (tx) => { + await tx.todoTask.updateMany({ + where: { bizId: id, bizType: 'WORK_ASSIGN', status: 'OPEN' }, + data: { status: 'CANCELLED' }, + }); + await tx.workAssignment.delete({ where: { id } }); + }); + return { ok: true }; + } + + private async withBiz(items: T[]) { + const ids = [...new Set(items.map((i) => i.bizId).filter((id): id is string => Boolean(id)))]; + if (!ids.length) { + return items.map((i) => ({ + ...i, + bidCase: null, + contract: null, + expense: null, + loan: null, + project: null, + sealRequest: null, + borrow: null, + officeApply: null, + leaveRequest: null, + })); + } + const person = { + select: { + id: true, + displayName: true, + username: true, + employee: { select: { name: true, department: { select: { name: true } } } }, + }, + }; + const [bids, contracts, expenses, loans, projects, sealRequests, borrows, officeApplies, leaveRequests] = await Promise.all([ + this.prisma.bidCase.findMany({ + where: { id: { in: ids } }, + select: { + id: true, + bidNo: true, + externalNo: true, + name: true, + status: true, + remark: true, + summary: true, + quoteAmount: true, + createdBy: person, + party: { select: { name: true } }, + }, + }), + this.prisma.contract.findMany({ + where: { id: { in: ids } }, + select: { + id: true, + contractNo: true, + name: true, + status: true, + amount: true, + remark: true, + createdBy: person, + }, + }), + this.prisma.expenseClaim.findMany({ + where: { id: { in: ids } }, + select: { + id: true, + claimNo: true, + status: true, + amount: true, + purpose: true, + remark: true, + expenseCategory: true, + costType: true, + applicant: person, + bidCase: { select: { id: true, bidNo: true, externalNo: true, name: true } }, + project: { select: { id: true, projectNo: true, name: true } }, + }, + }), + this.prisma.loanRecord.findMany({ + where: { id: { in: ids } }, + select: { + id: true, + loanNo: true, + status: true, + amount: true, + purpose: true, + remark: true, + costType: true, + applicant: person, + bidCase: { select: { id: true, bidNo: true, externalNo: true, name: true } }, + project: { select: { id: true, projectNo: true, name: true } }, + }, + }), + this.prisma.project.findMany({ + where: { id: { in: ids } }, + select: { id: true, projectNo: true, name: true, status: true }, + }), + this.prisma.sealRequest.findMany({ + where: { id: { in: ids } }, + select: { id: true, sealType: true, status: true, takeOut: true, reason: true, applicant: person }, + }), + this.prisma.credentialBorrow.findMany({ + where: { id: { in: ids } }, + select: { id: true, itemName: true, status: true, borrower: true }, + }), + this.prisma.officeApply.findMany({ + where: { id: { in: ids } }, + select: { + id: true, + kind: true, + title: true, + status: true, + amount: true, + reason: true, + destination: true, + assetKind: true, + applicant: person, + registeredAsset: { select: { name: true, code: true, kind: true } }, + }, + }), + this.prisma.leaveRequest.findMany({ + where: { id: { in: ids } }, + select: { + id: true, + kind: true, + status: true, + days: true, + reason: true, + startAt: true, + endAt: true, + comment: true, + employee: { + select: { id: true, name: true, employeeNo: true, department: { select: { name: true } } }, + }, + }, + }), + ]); + const bidMap = new Map(bids.map((b) => [b.id, b])); + const contractMap = new Map(contracts.map((c) => [c.id, c])); + const expenseMap = new Map(expenses.map((e) => [e.id, e])); + const loanMap = new Map(loans.map((l) => [l.id, l])); + const projectMap = new Map(projects.map((p) => [p.id, p])); + const sealMap = new Map(sealRequests.map((s) => [s.id, s])); + const borrowMap = new Map(borrows.map((b) => [b.id, b])); + const applyMap = new Map(officeApplies.map((a) => [a.id, a])); + const leaveMap = new Map(leaveRequests.map((l) => [l.id, l])); + return items.map((i) => ({ + ...i, + bidCase: i.bizId ? bidMap.get(i.bizId) ?? null : null, + contract: i.bizId ? contractMap.get(i.bizId) ?? null : null, + expense: i.bizId ? expenseMap.get(i.bizId) ?? null : null, + loan: i.bizId ? loanMap.get(i.bizId) ?? null : null, + project: i.bizId ? projectMap.get(i.bizId) ?? null : null, + sealRequest: i.bizId ? sealMap.get(i.bizId) ?? null : null, + borrow: i.bizId ? borrowMap.get(i.bizId) ?? null : null, + officeApply: i.bizId ? applyMap.get(i.bizId) ?? null : null, + leaveRequest: i.bizId + ? leaveMap.has(i.bizId) + ? { ...leaveMap.get(i.bizId)!, days: Number(leaveMap.get(i.bizId)!.days) } + : null + : null, + })); + } +} diff --git a/apps/api/src/office/work-report.ts b/apps/api/src/office/work-report.ts new file mode 100644 index 0000000..4cf61f0 --- /dev/null +++ b/apps/api/src/office/work-report.ts @@ -0,0 +1,325 @@ +import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { DIRECTOR_ROLES } from '../common/role-groups'; +import { descendantDeptIds } from '../common/data-scope'; +import { PERSON_SELECT, parsePeopleIds, resolveReportPeople } from '../common/org-chain'; +import { canManageSystem, periodRange } from '../system/ops-workflow'; +import { emitIm } from '../im/im-bus'; + +export const REPORT_STATUS = { + DRAFT: 'DRAFT', + SENT: 'SENT', +} as const; + +export const REPORT_BIZ = { + NOTICE: 'WORK_REPORT', +} as const; + +export const PERIOD_LABEL: Record = { + DAY: '日报', + WEEK: '周报', + MONTH: '月报', +}; + +const REPORT_VIEW_ONLY = ['admin', 'owner', ...DIRECTOR_ROLES]; + +/** 总监、负责人只看下属汇报,不写日报。 */ +export function canWriteWorkReport(roles: string[]) { + return !REPORT_VIEW_ONLY.some((r) => roles.includes(r)); +} + +export function reportFlowKind(input: { + authorRoles: string[]; + hasSubordinates: boolean; + title: string; +}): 'DIRECTOR_TO_OWNER' | 'PM_TO_DIRECTOR' | 'STAFF_TO_PM' { + if (REPORT_VIEW_ONLY.some((r) => input.authorRoles.includes(r))) return 'DIRECTOR_TO_OWNER'; + if (input.hasSubordinates || /经理|主管/.test(input.title || '')) return 'PM_TO_DIRECTOR'; + return 'STAFF_TO_PM'; +} + +export function parseReportDate(value?: string) { + if (!value) return new Date(); + const m = /^(\d{4})-(\d{2})-(\d{2})/.exec(value); + if (m) return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3])); + return new Date(value); +} + +type Person = { id: string; displayName: string; username: string }; + +const CC_FIELD: Record = { + DAY: 'ccDaily', + WEEK: 'ccWeekly', + MONTH: 'ccMonthly', +}; + +function uniquePeople(...lists: Array) { + const seen = new Set(); + const out: Person[] = []; + for (const item of lists) { + const people = Array.isArray(item) ? item : item ? [item] : []; + for (const p of people) { + if (!p || seen.has(p.id)) continue; + seen.add(p.id); + out.push(p); + } + } + return out; +} + +export async function resolveReportRoute(prisma: PrismaService, user: AuthUser, periodType?: string) { + const { me, manager: managerUser, extraManagers, director } = await resolveReportPeople(prisma, user.id); + const owner = await prisma.user.findFirst({ + where: { tenantId: '1', username: 'owner', status: 'ACTIVE' }, + select: PERSON_SELECT, + }); + const kind = reportFlowKind({ + authorRoles: user.roles, + hasSubordinates: (me?.employee?.reports.length || 0) > 0, + title: me?.employee?.title || '', + }); + const ccKey = CC_FIELD[(periodType || 'DAY').toUpperCase()] || 'ccDaily'; + const ccEmpIds = parsePeopleIds(me?.employee?.[ccKey]); + const archiveCc = ccEmpIds.length + ? ( + await prisma.user.findMany({ + where: { tenantId: '1', status: 'ACTIVE', employeeId: { in: ccEmpIds } }, + select: PERSON_SELECT, + }) + ).filter((p) => p.id !== user.id) + : []; + const extraMgrPeople = extraManagers.filter((p) => p.id !== user.id); + + const canWrite = canWriteWorkReport(user.roles); + if (kind === 'DIRECTOR_TO_OWNER') { + return { + kind, + hint: '总监只查看下属的日报、周报、月报,不用自己写。', + to: null, + cc: null, + ccPeople: [] as Person[], + canWrite, + }; + } + if (kind === 'PM_TO_DIRECTOR') { + const to = pick(director, extraMgrPeople[0], owner); + const extra = uniquePeople( + extraMgrPeople.filter((p) => p.id !== to?.id), + archiveCc.filter((p) => p.id !== to?.id), + ); + return { + kind, + hint: '项目经理 → 部门总监', + to, + cc: extra[0] || null, + ccPeople: extra, + canWrite, + }; + } + const to = pick(managerUser, extraMgrPeople[0], director, owner); + const directorCc = + director && to && director.id !== to.id && director.id !== user.id ? [director] : []; + const ccPeople = uniquePeople( + managerUser && managerUser.id !== to?.id ? managerUser : null, + extraMgrPeople.filter((p) => p.id !== to?.id), + directorCc, + archiveCc.filter((p) => p.id !== to?.id && p.id !== director?.id), + ); + return { + kind, + hint: '普通员工 → 项目经理 → 部门总监', + to, + cc: ccPeople[0] || null, + ccPeople, + canWrite, + }; +} + +export async function hoursDraft(prisma: PrismaService, user: AuthUser, periodType: string, date?: string) { + const type = (periodType || 'DAY').toUpperCase(); + if (!['DAY', 'WEEK', 'MONTH'].includes(type)) throw new BadRequestException('周期须是日/周/月'); + const { start, end, period } = periodRange(type, parseReportDate(date)); + const sheets = await prisma.timesheet.findMany({ + where: { userId: user.id, workDate: { gte: start, lt: end } }, + include: { project: { select: { projectNo: true, name: true } }, task: { select: { name: true } } }, + }); + const tasks = await prisma.projectTask.findMany({ + where: { assigneeId: user.id, status: 'OPEN' }, + include: { project: { select: { projectNo: true } } }, + }); + const hours = sheets.reduce((s, r) => s + Number(r.hours), 0); + const lines = [ + `${PERIOD_LABEL[type] || type} ${period},本人工时 ${hours} 小时。`, + ...sheets.map((s) => + `- ${s.workDate.toISOString().slice(0, 10)} ${s.project.projectNo} ${s.task?.name || ''} ${Number(s.hours)}h ${s.note || ''}`.trim(), + ), + tasks.length + ? `未完成任务:${tasks.map((t) => `${t.project.projectNo} ${t.name}`).join(';')}` + : '当前没有未完成任务。', + ]; + return { period, periodType: type, hours, content: lines.join('\n') }; +} + +const INCLUDE = { + author: { select: PERSON_SELECT }, + toUser: { select: PERSON_SELECT }, + ccUser: { select: PERSON_SELECT }, +}; + +export function serializeReport(row: T) { + return { ...row, hours: Number(row.hours) }; +} + +export async function listReports(prisma: PrismaService, user: AuthUser) { + let where: { tenantId: string; OR?: object[] }; + if (canManageSystem(user.roles)) { + where = { tenantId: '1' }; + } else if (!canWriteWorkReport(user.roles)) { + const deptIds = await descendantDeptIds(prisma, user.departmentId); + where = { + tenantId: '1', + OR: [ + { toUserId: user.id }, + { ccUserId: user.id }, + ...(deptIds.length ? [{ author: { employee: { departmentId: { in: deptIds } } } }] : []), + ], + }; + } else { + where = { + tenantId: '1', + OR: [{ authorId: user.id }, { toUserId: user.id }, { ccUserId: user.id }], + }; + } + const items = await prisma.workReport.findMany({ + where, + include: INCLUDE, + orderBy: { createdAt: 'desc' }, + take: 80, + }); + return { items: items.map(serializeReport) }; +} + +export async function getReport(prisma: PrismaService, user: AuthUser, id: string) { + const row = await prisma.workReport.findFirst({ where: { id, tenantId: '1' }, include: INCLUDE }); + if (!row) throw new NotFoundException('汇报不存在'); + await assertCanSee(prisma, user, row); + return serializeReport(row); +} + +export async function saveReport( + prisma: PrismaService, + user: AuthUser, + dto: { periodType?: string; date?: string; content?: string; submit?: boolean }, + id?: string, +) { + if (!canWriteWorkReport(user.roles)) { + throw new ForbiddenException('总监只查看汇报,不写日报'); + } + const periodType = (dto.periodType || 'DAY').toUpperCase(); + if (!['DAY', 'WEEK', 'MONTH'].includes(periodType)) throw new BadRequestException('周期须是日/周/月'); + const content = (dto.content || '').trim(); + if (dto.submit && !content) throw new BadRequestException('请先写汇报内容再提交'); + const when = parseReportDate(dto.date); + const { start, period } = periodRange(periodType, when); + const draft = await hoursDraft(prisma, user, periodType, dto.date); + const route = await resolveReportRoute(prisma, user, periodType); + + if (id) { + const row = await prisma.workReport.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('汇报不存在'); + if (row.authorId !== user.id) throw new ForbiddenException('只能改自己的汇报'); + if (row.status !== REPORT_STATUS.DRAFT) throw new BadRequestException('已提交的汇报不能再改'); + } + + const data = { + period, + periodType, + periodStart: start, + hours: draft.hours, + content, + status: dto.submit ? REPORT_STATUS.SENT : REPORT_STATUS.DRAFT, + toUserId: dto.submit ? route.to?.id || null : null, + ccUserId: dto.submit ? route.cc?.id || null : null, + submittedAt: dto.submit ? new Date() : null, + }; + + const saved = id + ? await prisma.workReport.update({ where: { id }, data, include: INCLUDE }) + : await prisma.workReport.create({ + data: { tenantId: '1', authorId: user.id, ...data }, + include: INCLUDE, + }); + + if (dto.submit) await notifyRecipients(prisma, saved, route.ccPeople?.map((p) => p.id) || []); + return serializeReport(saved); +} + +export async function submitReport(prisma: PrismaService, user: AuthUser, id: string) { + const row = await prisma.workReport.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('汇报不存在'); + if (row.authorId !== user.id) throw new ForbiddenException('只能提交自己的汇报'); + if (row.status === REPORT_STATUS.SENT) throw new BadRequestException('已经提交过了'); + if (!row.content.trim()) throw new BadRequestException('请先写汇报内容再提交'); + return saveReport(prisma, user, { + periodType: row.periodType, + date: row.periodStart ? row.periodStart.toISOString().slice(0, 10) : undefined, + content: row.content, + submit: true, + }, id); +} + +async function assertCanSee( + prisma: PrismaService, + user: AuthUser, + row: { authorId: string; toUserId: string | null; ccUserId: string | null }, +) { + if (canManageSystem(user.roles)) return; + if (row.authorId === user.id || row.toUserId === user.id || row.ccUserId === user.id) return; + if (!canWriteWorkReport(user.roles) && user.departmentId) { + const author = await prisma.user.findUnique({ + where: { id: row.authorId }, + include: { employee: { select: { departmentId: true } } }, + }); + const deptIds = await descendantDeptIds(prisma, user.departmentId); + if (author?.employee?.departmentId && deptIds.includes(author.employee.departmentId)) return; + } + throw new ForbiddenException('无权查看这份汇报'); +} + +function pick(...people: Array) { + return people.find((p) => p) || null; +} + +async function notifyRecipients( + prisma: PrismaService, + row: { + id: string; + period: string; + periodType: string; + author: { displayName: string }; + toUserId: string | null; + ccUserId: string | null; + }, + extraCcIds: string[] = [], +) { + const kind = PERIOD_LABEL[row.periodType] || '汇报'; + const ccIds = [...new Set([row.ccUserId, ...extraCcIds].filter((id): id is string => Boolean(id)))]; + const targets = [ + row.toUserId ? { id: row.toUserId, title: `${row.author.displayName} 的${kind}(${row.period})` } : null, + ...ccIds + .filter((id) => id !== row.toUserId) + .map((id) => ({ id, title: `抄送:${row.author.displayName} 的${kind}(${row.period})` })), + ].filter((x): x is { id: string; title: string } => Boolean(x)); + if (!targets.length) return; + // 汇报是系统通知,不是需要执行/关闭的待办事项。 + // 接收人可在「工作汇报」查看,下发一条系统消息作为提醒。 + emitIm({ + userIds: targets.map((t) => t.id), + kind: 'todo', + title: '工作汇报', + body: `${row.author.displayName}提交了${kind}(${row.period})`, + bizType: REPORT_BIZ.NOTICE, + bizId: row.id, + }); +} diff --git a/apps/api/src/prisma/prisma.module.ts b/apps/api/src/prisma/prisma.module.ts new file mode 100644 index 0000000..7207426 --- /dev/null +++ b/apps/api/src/prisma/prisma.module.ts @@ -0,0 +1,9 @@ +import { Global, Module } from '@nestjs/common'; +import { PrismaService } from './prisma.service'; + +@Global() +@Module({ + providers: [PrismaService], + exports: [PrismaService], +}) +export class PrismaModule {} diff --git a/apps/api/src/prisma/prisma.service.ts b/apps/api/src/prisma/prisma.service.ts new file mode 100644 index 0000000..abcaa81 --- /dev/null +++ b/apps/api/src/prisma/prisma.service.ts @@ -0,0 +1,56 @@ +import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; +import { PrismaClient } from '@prisma/client'; + +@Injectable() +export class PrismaService + extends PrismaClient + implements OnModuleInit, OnModuleDestroy +{ + async onModuleInit() { + await this.$connect(); + this.hookTaskPush(); + } + + private hookTaskPush() { + const todoCreate = this.todoTask.create.bind(this.todoTask); + this.todoTask.create = (async (args: Parameters[0]) => { + const row = await todoCreate(args); + const { emitIm } = await import('../im/im-bus'); + if (row?.assigneeId && row.title) { + emitIm({ + userIds: [row.assigneeId], + kind: 'todo', + title: row.title, + body: row.title, + bizType: row.bizType, + bizId: row.bizId || undefined, + }); + } + return row; + }) as unknown as typeof todoCreate; + const approvalCreate = this.approvalTask.create.bind(this.approvalTask); + this.approvalTask.create = (async (args: Parameters[0]) => { + const row = await approvalCreate(args); + const { emitIm } = await import('../im/im-bus'); + if (row?.assigneeId && row.title) { + emitIm({ + userIds: [row.assigneeId], + kind: 'approval', + title: row.title, + body: `待审批:${row.title}`, + bizType: row.bizType, + bizId: row.bizId || undefined, + }); + } + return row; + }) as unknown as typeof approvalCreate; + } + + async onModuleDestroy() { + await this.$disconnect(); + } + + async transaction(fn: (tx: PrismaClient) => Promise): Promise { + return this.$transaction(fn); + } +} diff --git a/apps/api/src/project/project-workflow.spec.ts b/apps/api/src/project/project-workflow.spec.ts new file mode 100644 index 0000000..6d111da --- /dev/null +++ b/apps/api/src/project/project-workflow.spec.ts @@ -0,0 +1,73 @@ +import { + assertExpenseBudgetCategory, + budgetWouldExceed, + canAcceptCustomer, + canAcceptInternal, + canAccessProject, + canCreateProject, + canFillTimesheet, + canManageWbs, + canSetBudget, + encodeExpenseBudget, + envelopeOfExpense, + laborCost, + splitEven, +} from './project-workflow'; + +describe('project-workflow', () => { + it('lets tech directors manage WBS and employees fill timesheets', () => { + expect(canManageWbs(['tech_director'])).toBe(true); + expect(canManageWbs(['employee'])).toBe(false); + expect(canFillTimesheet(['employee'], ['project:timesheets'])).toBe(true); + expect(canFillTimesheet(['hr'], ['hr:org'])).toBe(false); + expect(canSetBudget(['finance'])).toBe(true); + expect(canSetBudget(['biz_staff'])).toBe(true); + expect(canSetBudget(['biz_director'])).toBe(true); + }); + + it('lets contract and finance roles open the project ledger', () => { + expect(canAccessProject(['biz_director'], ['contract:list'])).toBe(true); + expect(canAccessProject(['finance'], ['finance:expenses'])).toBe(true); + expect(canAccessProject(['employee'], ['project:timesheets'])).toBe(false); + expect(canAccessProject(['hr'], ['hr:org'])).toBe(false); + }); + + it('splits internal and customer acceptance', () => { + expect(canAcceptInternal(['tech_director'])).toBe(true); + expect(canAcceptInternal(['biz_director'])).toBe(false); + expect(canAcceptCustomer(['biz_director'])).toBe(true); + expect(canAcceptCustomer(['employee'])).toBe(false); + }); + + it('lets pm and biz director create a ledger project', () => { + expect(canCreateProject(['pm'])).toBe(true); + expect(canCreateProject(['biz_director'])).toBe(true); + expect(canCreateProject(['employee'])).toBe(false); + }); + + it('splits contract amount evenly between business and tech', () => { + expect(splitEven(100)).toEqual({ business: 50, tech: 50 }); + expect(splitEven(100.01)).toEqual({ business: 50.01, tech: 50 }); + expect(splitEven(0)).toEqual({ business: 0, tech: 0 }); + }); + + it('computes labor cost and budget overflow', () => { + expect(laborCost(2, 200)).toBe(400); + expect(budgetWouldExceed(400, 800, 1000)).toBe(true); + expect(budgetWouldExceed(400, 200, 1000)).toBe(false); + expect(budgetWouldExceed(400, 200, null)).toBe(false); + }); + + it('requires travel or purchase on project expenses', () => { + expect(assertExpenseBudgetCategory('PROJECT')).toMatch(/差旅或采购/); + expect(assertExpenseBudgetCategory('PROJECT', 'LABOR')).toMatch(/差旅或采购/); + expect(assertExpenseBudgetCategory('PROJECT', 'TRAVEL')).toBeNull(); + expect(assertExpenseBudgetCategory('PROJECT', 'TECH')).toBeNull(); + expect(assertExpenseBudgetCategory('BID', undefined)).toBeNull(); + expect(envelopeOfExpense('TRAVEL')).toBe('BUSINESS'); + expect(envelopeOfExpense('TECH')).toBe('TECH'); + expect(encodeExpenseBudget('TRAVEL', 'TECH')).toBe('TECH'); + expect(encodeExpenseBudget('TRAVEL', 'BUSINESS')).toBe('TRAVEL'); + expect(encodeExpenseBudget('PURCHASE', 'BUSINESS')).toBe('PURCHASE'); + }); +}); diff --git a/apps/api/src/project/project-workflow.ts b/apps/api/src/project/project-workflow.ts new file mode 100644 index 0000000..f1786d9 --- /dev/null +++ b/apps/api/src/project/project-workflow.ts @@ -0,0 +1,190 @@ +import { isTechLead } from '../common/role-groups'; + +export const PROJECT_STATUS = { + NOT_STARTED: 'NOT_STARTED', + ACTIVE: 'ACTIVE', + INTERNAL_ACCEPTED: 'INTERNAL_ACCEPTED', + ACCEPTED: 'ACCEPTED', + DONE: 'DONE', +} as const; + +export const PROJECT_LEDGER_STATUSES = [PROJECT_STATUS.NOT_STARTED, PROJECT_STATUS.ACTIVE] as const; +export const PROJECT_ACCEPTANCE_STATUSES = [ + PROJECT_STATUS.INTERNAL_ACCEPTED, + PROJECT_STATUS.ACCEPTED, + PROJECT_STATUS.DONE, +] as const; + +export function isProjectClosed(status: string) { + return status === PROJECT_STATUS.ACCEPTED || status === PROJECT_STATUS.DONE; +} + +export function partyFromContractRemark(remark?: string | null) { + const m = String(remark || '').match(/甲方:([^/]+)/); + return m?.[1]?.trim() || ''; +} + +export function compactProjectName(name: string) { + return String(name || '') + .replace(/[()()\[\]【】\s\-—_.,,、]/g, '') + .replace(/招标公告|采购项目|技术服务合同|合同/g, '') + .replace(/检查院/g, '检察院'); +} + +export function namesOverlap(a: string, b: string) { + const na = compactProjectName(a); + const nb = compactProjectName(b); + if (!na || !nb) return false; + if (na === nb) return true; + const shorter = na.length <= nb.length ? na : nb; + const longer = na.length <= nb.length ? nb : na; + return shorter.length >= 8 && longer.includes(shorter); +} + +export const BUDGET_CATEGORY = { + LABOR: 'LABOR', + TRAVEL: 'TRAVEL', + PURCHASE: 'PURCHASE', + BUSINESS: 'BUSINESS', + TECH: 'TECH', +} as const; + +export const EXPENSE_BUDGET_CATEGORIES = [BUDGET_CATEGORY.TRAVEL, BUDGET_CATEGORY.PURCHASE, BUDGET_CATEGORY.TECH] as const; +export const LINE_BUDGET_CATEGORIES = [ + BUDGET_CATEGORY.LABOR, + BUDGET_CATEGORY.TRAVEL, + BUDGET_CATEGORY.PURCHASE, +] as const; +export const ENVELOPE_BUDGET_CATEGORIES = [BUDGET_CATEGORY.BUSINESS, BUDGET_CATEGORY.TECH] as const; + +/** 合同额(或项目总额)按商务、技术各一半。分到分后余数给技术。 */ +export function splitEven(amount: number): { business: number; tech: number } { + const n = roundMoney(Math.max(0, Number(amount) || 0)); + const business = roundMoney(n / 2); + return { business, tech: roundMoney(n - business) }; +} + +export function envelopeLimit( + side: 'BUSINESS' | 'TECH', + budgets: { category: string; amount: unknown }[], + contractAmount?: number | null, +): number | null { + const saved = budgets.find((b) => b.category === side); + if (saved) return Number(saved.amount); + if (contractAmount && contractAmount > 0) { + const split = splitEven(contractAmount); + return side === 'BUSINESS' ? split.business : split.tech; + } + return null; +} + +export const ACCEPT_KIND = { + INTERNAL: 'INTERNAL', + CUSTOMER: 'CUSTOMER', +} as const; + +export const TASK_STATUS = { + OPEN: 'OPEN', + DONE: 'DONE', +} as const; + +export const PROJECT_BIZ = { + TIMESHEET: 'PROJECT_TIMESHEET', + ACCEPT: 'PROJECT_ACCEPT', +} as const; + +export const DEFAULT_ACCEPT_MATERIALS = [ + '验收报告', + '交付清单 / 竣工资料', + '培训记录', + '客户签章验收单', + '质保承诺', +]; + +export function canAccessProject(roles: string[], permissions: string[]) { + if (roles.includes('admin')) return true; + if (permissions.some((p) => p === 'contract' || p.startsWith('contract:') || p === 'finance' || p.startsWith('finance:'))) { + return true; + } + return permissions.some((p) => p === 'project' || (p.startsWith('project:') && p !== 'project:timesheets')); +} + +export function canFillTimesheet(roles: string[], permissions: string[]) { + if (roles.includes('admin') || roles.includes('owner') || isTechLead(roles)) return true; + return permissions.some((p) => p === 'project' || p === 'project:timesheets' || p.startsWith('project:')); +} + +export function canManageWbs(roles: string[]) { + return ['admin', 'owner'].some((r) => roles.includes(r)) || isTechLead(roles); +} + +export const PROJECT_TYPES = ['软件', '三维', '摄制', '物资', '集成'] as const; + +export function normalizeProjectType(v?: string | null) { + const s = String(v || '').trim(); + if (!s) return ''; + if (s.includes('三维') || s.includes('动画')) return '三维'; + if (s.includes('摄制') || s.includes('视频') || s.includes('摄影')) return '摄制'; + if (s.includes('物资')) return '物资'; + if (s.includes('集成')) return '集成'; + if (s.includes('软件') || s.includes('开发') || s.includes('信息化')) return '软件'; + if ((PROJECT_TYPES as readonly string[]).includes(s)) return s; + return ''; +} + +/** 台账手动立项:直签、补登历史项目。 */ +export function canCreateProject(roles: string[]) { + return ['admin', 'owner', 'pm', 'biz_director', 'biz_staff'].some((r) => roles.includes(r)) || isTechLead(roles); +} + +export function canSetBudget(roles: string[]) { + return ['admin', 'owner', 'finance', 'biz_director', 'biz_staff'].some((r) => roles.includes(r)) || isTechLead(roles); +} + +export function canAcceptInternal(roles: string[]) { + return ['admin', 'owner'].some((r) => roles.includes(r)) || isTechLead(roles); +} + +export function canAcceptCustomer(roles: string[]) { + return ['admin', 'owner', 'biz_director'].some((r) => roles.includes(r)) || isTechLead(roles); +} + +export function roundMoney(n: number) { + return Math.round(n * 100) / 100; +} + +export function laborCost(hours: number, hourRate: number) { + return roundMoney(hours * hourRate); +} + +export function budgetWouldExceed(used: number, add: number, limit?: number | null) { + if (limit === undefined || limit === null) return false; + return roundMoney(used + add) - limit > 0.001; +} + +export function envelopeOfExpense(category?: string | null): 'BUSINESS' | 'TECH' | null { + if (category === BUDGET_CATEGORY.TRAVEL) return 'BUSINESS'; + if ( + category === BUDGET_CATEGORY.PURCHASE || + category === BUDGET_CATEGORY.LABOR || + category === BUDGET_CATEGORY.TECH + ) { + return 'TECH'; + } + return null; +} + +export function encodeExpenseBudget(kind?: string | null, envelope?: string | null) { + if (kind === BUDGET_CATEGORY.PURCHASE) return BUDGET_CATEGORY.PURCHASE; + if (kind === BUDGET_CATEGORY.TECH || envelope === BUDGET_CATEGORY.TECH) return BUDGET_CATEGORY.TECH; + if (kind === BUDGET_CATEGORY.TRAVEL || envelope === BUDGET_CATEGORY.BUSINESS) return BUDGET_CATEGORY.TRAVEL; + return kind || ''; +} + +export function assertExpenseBudgetCategory(costType: string, budgetCategory?: string) { + if (costType !== 'PROJECT') return null; + if (!(EXPENSE_BUDGET_CATEGORIES as readonly string[]).includes(budgetCategory || '')) { + return '挂项目报销请选择差旅或采购,并标明商务或技术预算'; + } + return null; +} diff --git a/apps/api/src/project/project.controller.ts b/apps/api/src/project/project.controller.ts new file mode 100644 index 0000000..c5120cc --- /dev/null +++ b/apps/api/src/project/project.controller.ts @@ -0,0 +1,273 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator'; +import { ProjectService } from './project.service'; + +@ApiTags('project') +@ApiBearerAuth() +@Controller() +export class ProjectController { + constructor(private readonly projects: ProjectService) {} + + @Get('projects') + list( + @CurrentUser() user: AuthUser, + @Query('page') page = '1', + @Query('pageSize') pageSize = '20', + @Query('keyword') keyword?: string, + @Query('bucket') bucket?: string, + @Query('projectType') projectType?: string, + @Query('status') status?: string, + ) { + return this.projects.list( + user, + Number(page) || 1, + Number(pageSize) || 20, + keyword, + bucket, + projectType, + status, + ); + } + + @Post('projects') + create( + @CurrentUser() user: AuthUser, + @Body() + body: { + name: string; + contractId?: string; + partyName?: string; + amount?: number; + status?: string; + projectType?: string; + }, + ) { + return this.projects.create(user, body); + } + + @Get('project-tasks') + tasks(@CurrentUser() user: AuthUser) { + return this.projects.listTasks(user); + } + + @Get('timesheets') + timesheets(@CurrentUser() user: AuthUser, @Query('projectId') projectId?: string) { + return this.projects.listTimesheets(user, projectId); + } + + @Post('timesheets') + createTimesheet( + @CurrentUser() user: AuthUser, + @Body() body: { projectId: string; taskId?: string; workDate: string; hours: number; note?: string }, + ) { + return this.projects.createTimesheet(user, body); + } + + @Delete('timesheets/:id') + deleteTimesheet(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.projects.deleteTimesheet(id, user); + } + + @Get('projects/:id') + get(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.projects.get(id, user); + } + + @Patch('projects/:id') + update( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { hourRate?: number; amount?: number; projectType?: string; name?: string }, + ) { + return this.projects.update(id, user, body); + } + + @Put('projects/:id/budgets') + budgets( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { items: Array<{ category: string; amount: number }> }, + ) { + return this.projects.replaceBudgets(id, user, body.items || []); + } + + @Post('projects/:id/budgets/sync') + syncBudgets( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { amount?: number }, + ) { + return this.projects.syncSplitBudgets(id, user, body); + } + + @Post('projects/:id/tasks') + createTask( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() + body: { name: string; departmentId?: string; assigneeId?: string; dueAt?: string; sortNo?: number }, + ) { + return this.projects.createTask(id, user, body); + } + + @Patch('projects/:id/tasks/:taskId') + updateTask( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Param('taskId') taskId: string, + @Body() + body: { + name?: string; + departmentId?: string; + assigneeId?: string; + dueAt?: string; + sortNo?: number; + status?: string; + }, + ) { + return this.projects.updateTask(id, taskId, user, { name: body.name || '', ...body }); + } + + @Post('projects/:id/tasks/:taskId/done') + completeTask(@CurrentUser() user: AuthUser, @Param('id') id: string, @Param('taskId') taskId: string) { + return this.projects.completeTask(id, taskId, user); + } + + @Delete('projects/:id/tasks/:taskId') + deleteTask(@CurrentUser() user: AuthUser, @Param('id') id: string, @Param('taskId') taskId: string) { + return this.projects.deleteTask(id, taskId, user); + } + + @Post('projects/:id/accept') + accept( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { kind: 'INTERNAL' | 'CUSTOMER'; result: 'APPROVED' | 'REJECTED'; comment?: string }, + ) { + return this.projects.accept(id, user, body); + } + + @Post('projects/:id/accept-materials') + addMaterial( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { name: string; remark?: string; required?: boolean }, + ) { + return this.projects.addAcceptMaterial(id, user, body); + } + + @Post('projects/:id/accept-materials/seed') + seedMaterials(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.projects.seedAcceptMaterials(id, user); + } + + @Patch('projects/:id/accept-materials/:materialId') + updateMaterial( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Param('materialId') materialId: string, + @Body() body: { name?: string; remark?: string | null; required?: boolean; sortNo?: number }, + ) { + return this.projects.updateAcceptMaterial(id, materialId, user, body); + } + + @Delete('projects/:id/accept-materials/:materialId') + deleteMaterial( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Param('materialId') materialId: string, + ) { + return this.projects.deleteAcceptMaterial(id, materialId, user); + } + + @Get('project-changes') + changes(@CurrentUser() user: AuthUser) { + return this.projects.listChanges(user); + } + + @Post('project-changes') + createChange( + @CurrentUser() user: AuthUser, + @Body() body: { projectId: string; summary: string; amountDelta?: number }, + ) { + return this.projects.createChange(user, body); + } + + @Post('project-changes/:id/review') + reviewChange( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { result: 'APPROVED' | 'REJECTED'; comment?: string }, + ) { + return this.projects.reviewChange(id, user, body); + } + + @Delete('project-changes/:id') + deleteChange(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.projects.deleteChange(id, user); + } + + @Get('purchase-requests') + purchases(@CurrentUser() user: AuthUser) { + return this.projects.listPurchases(user); + } + + @Post('purchase-requests') + createPurchase( + @CurrentUser() user: AuthUser, + @Body() body: { title: string; amount: number; projectId?: string; remark?: string }, + ) { + return this.projects.createPurchase(user, body); + } + + @Post('purchase-requests/:id/decide') + decidePurchase( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { result: 'APPROVED' | 'REJECTED'; comment?: string }, + ) { + return this.projects.decidePurchase(id, user, body); + } + + @Delete('purchase-requests/:id') + deletePurchase(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.projects.deletePurchase(id, user); + } + + @Get('assets') + assets(@CurrentUser() user: AuthUser) { + return this.projects.listAssets(user); + } + + @Post('assets') + occupy( + @CurrentUser() user: AuthUser, + @Body() + body: { + registeredAssetId?: string; + assetName?: string; + holderId?: string; + holder?: string; + projectId?: string; + dueAt?: string; + }, + ) { + return this.projects.occupyAsset(user, body); + } + + @Post('assets/:id/return') + returnAsset(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.projects.returnAsset(id, user); + } + + @Delete('assets/:id') + deleteOccupancy(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.projects.deleteOccupancy(id, user); + } + + @Get('resource-board') + board(@CurrentUser() user: AuthUser) { + return this.projects.resourceBoard(user); + } +} diff --git a/apps/api/src/project/project.service.ts b/apps/api/src/project/project.service.ts new file mode 100644 index 0000000..4d097d7 --- /dev/null +++ b/apps/api/src/project/project.service.ts @@ -0,0 +1,1300 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { EXPENSE_STATUS, INVOICE_STATUS } from '../finance/finance-workflow'; +import { + ACCEPT_KIND, + BUDGET_CATEGORY, + DEFAULT_ACCEPT_MATERIALS, + PROJECT_BIZ, + PROJECT_ACCEPTANCE_STATUSES, + PROJECT_LEDGER_STATUSES, + PROJECT_STATUS, + TASK_STATUS, + budgetWouldExceed, + canAcceptCustomer, + canAcceptInternal, + canAccessProject, + canCreateProject, + canFillTimesheet, + canManageWbs, + canSetBudget, + envelopeLimit, + envelopeOfExpense, + isProjectClosed, + laborCost, + namesOverlap, + normalizeProjectType, + partyFromContractRemark, + roundMoney, + splitEven, +} from './project-workflow'; +import { canAccessBidModule } from '../bidding/bid-workflow'; +import { internalHourRate } from '../hr/hr-workflow'; +import { + ASSET_STATUS, + CHANGE_STATUS, + PURCHASE_STATUS, + canApprovePurchase, + canChangeProject, + canFilePurchase, + resourceLoad, +} from '../system/ops-workflow'; +import { nextBizNo } from '../system/numbering'; + +const USER_SELECT = { id: true, displayName: true, username: true }; +const DEPT_SELECT = { id: true, name: true, code: true, deptType: true }; + +type Tx = Prisma.TransactionClient; + +export type BudgetItemInput = { category: string; amount: number }; +export type TaskInput = { + name: string; + departmentId?: string; + assigneeId?: string; + dueAt?: string; + sortNo?: number; +}; +export type TimesheetInput = { + projectId: string; + taskId?: string; + workDate: string; + hours: number; + note?: string; +}; +export type AcceptInput = { + kind: 'INTERNAL' | 'CUSTOMER'; + result: 'APPROVED' | 'REJECTED'; + comment?: string; +}; + +const HOLD_EXPENSE = [EXPENSE_STATUS.PENDING, EXPENSE_STATUS.APPROVED]; + +@Injectable() +export class ProjectService { + constructor(private readonly prisma: PrismaService) {} + + assertAccess(user: AuthUser) { + if (!canAccessProject(user.roles, user.permissions)) { + throw new ForbiddenException('无权访问项目管理'); + } + } + + assertTimesheet(user: AuthUser) { + if (!canFillTimesheet(user.roles, user.permissions)) { + throw new ForbiddenException('无权填报工时'); + } + } + + async list( + user: AuthUser, + page: number, + pageSize: number, + keyword?: string, + bucket?: string, + projectType?: string, + status?: string, + ) { + this.assertAccess(user); + const where: Prisma.ProjectWhereInput = { tenantId: '1' }; + if (keyword) { + where.OR = [ + { projectNo: { contains: keyword } }, + { name: { contains: keyword } }, + { partyName: { contains: keyword } }, + ]; + } + if (projectType) where.projectType = projectType; + if (status) where.status = status; + if (bucket === 'active') where.status = PROJECT_STATUS.ACTIVE; + if (bucket === 'internal') where.status = PROJECT_STATUS.INTERNAL_ACCEPTED; + if (bucket === 'accepted') where.status = PROJECT_STATUS.ACCEPTED; + if (bucket === 'pending') where.status = { notIn: [PROJECT_STATUS.ACCEPTED, PROJECT_STATUS.DONE] }; + if (bucket === 'ledger') where.status = { in: [...PROJECT_LEDGER_STATUSES] }; + if (bucket === 'acceptance') { + where.AND = [ + ...(Array.isArray(where.AND) ? where.AND : where.AND ? [where.AND] : []), + { + OR: [{ status: { in: [...PROJECT_ACCEPTANCE_STATUSES] } }, { acceptMaterials: { some: {} } }], + }, + ]; + } + const seeBid = canAccessBidModule(user.roles, user.permissions); + const [rows, total] = await this.prisma.$transaction([ + this.prisma.project.findMany({ + where, + include: { + contract: { + include: { + bidCase: { select: { id: true, bidNo: true, projectType: true, party: { select: { name: true } } } }, + }, + }, + budgets: true, + _count: { select: { acceptMaterials: true, expenses: true } }, + }, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.project.count({ where }), + ]); + const bids = seeBid + ? await this.prisma.bidCase.findMany({ + where: { tenantId: '1' }, + select: { name: true, party: { select: { name: true } } }, + }) + : []; + const usage = await this.usageMap(rows.map((r) => r.id)); + return { + items: rows.map((r) => ({ + ...r, + usage: usage.get(r.id) || this.emptyUsage(Number(r.hourRate), r.budgets), + projectType: r.projectType || r.contract?.bidCase?.projectType || '', + amount: Number(r.amount) || Number(r.contract?.amount || 0), + budgetTotal: r.budgets.reduce((sum, b) => sum + Number(b.amount), 0), + acceptMaterialCount: r._count.acceptMaterials, + expenseCount: r._count.expenses, + contract: seeBid + ? r.contract + : r.contract + ? { ...r.contract, bidCase: null } + : r.contract, + partyName: seeBid + ? r.partyName || + r.contract?.partyA || + r.contract?.bidCase?.party?.name || + partyFromContractRemark(r.contract?.remark) || + bids.find((b) => namesOverlap(b.name, r.name))?.party?.name || + null + : r.partyName || r.contract?.partyA || partyFromContractRemark(r.contract?.remark) || null, + })), + page, + pageSize, + total, + }; + } + + async get(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.project.findFirst({ + where: { id, tenantId: '1' }, + include: { + contract: { + include: { + bidCase: { select: { id: true, bidNo: true, name: true, projectType: true } }, + legalEntity: true, + milestones: { include: { invoice: true }, orderBy: { dueAt: 'asc' } }, + }, + }, + tasks: { + include: { dept: { select: DEPT_SELECT }, assignee: { select: USER_SELECT } }, + orderBy: [{ sortNo: 'asc' }, { createdAt: 'asc' }], + }, + timesheets: { + include: { user: { select: USER_SELECT }, task: { select: { id: true, name: true } } }, + orderBy: { workDate: 'desc' }, + take: 200, + }, + budgets: true, + acceptances: { + include: { actor: { select: USER_SELECT } }, + orderBy: { decidedAt: 'asc' }, + }, + expenses: { + where: { + OR: [{ applicantId: user.id }, { status: { not: EXPENSE_STATUS.DRAFT } }], + }, + include: { applicant: { select: USER_SELECT } }, + orderBy: { createdAt: 'desc' }, + take: 200, + }, + purchases: { + include: { applicant: { select: USER_SELECT } }, + orderBy: { createdAt: 'desc' }, + take: 100, + }, + }, + }); + if (!row) throw new NotFoundException('项目不存在'); + if (!canAccessBidModule(user.roles, user.permissions) && row.contract) { + (row.contract as { bidCase?: unknown }).bidCase = null; + } + const usage = (await this.usageMap([row.id])).get(row.id) || this.emptyUsage(Number(row.hourRate), row.budgets); + const files = await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizId: id, bizType: { startsWith: 'PROJECT' } }, + orderBy: { createdAt: 'asc' }, + }); + const materials = await this.withMaterialFiles(id); + return { + ...row, + projectType: row.projectType || row.contract?.bidCase?.projectType || '', + amount: Number(row.amount) || Number(row.contract?.amount || 0), + usage, + files: files.filter((f) => f.bizType !== PROJECT_BIZ.ACCEPT), + acceptMaterials: materials, + acceptMaterialsReady: this.materialsReady(materials), + myActions: this.myActions(row, user), + }; + } + + async create( + user: AuthUser, + dto: { + name: string; + contractId?: string; + partyName?: string; + amount?: number; + status?: string; + projectType?: string; + }, + ) { + this.assertAccess(user); + if (!canCreateProject(user.roles)) throw new ForbiddenException('无权新建项目台账'); + if (!dto.name?.trim()) throw new BadRequestException('请填写项目名称'); + const status = + dto.status === PROJECT_STATUS.ACTIVE ? PROJECT_STATUS.ACTIVE : PROJECT_STATUS.NOT_STARTED; + let contract: { id: string; amount: Prisma.Decimal; partyA: string | null; name: string } | null = null; + if (dto.contractId) { + const found = await this.prisma.contract.findFirst({ + where: { id: dto.contractId, tenantId: '1' }, + include: { projects: { select: { id: true } }, bidCase: { select: { projectType: true } } }, + }); + if (!found) throw new NotFoundException('合同不存在'); + if (found.projects.length) throw new BadRequestException('该合同已经立过项'); + contract = found; + } + const amount = dto.amount && dto.amount > 0 ? dto.amount : Number(contract?.amount || 0); + const created = await this.prisma.$transaction(async (tx) => { + const projectNo = await nextBizNo(tx, 'PRJ', (start) => + tx.project.count({ where: { projectNo: { startsWith: start } } }), + ); + const project = await tx.project.create({ + data: { + tenantId: '1', + projectNo, + name: dto.name.trim(), + contractId: contract?.id || null, + partyName: dto.partyName?.trim() || contract?.partyA || null, + projectType: + normalizeProjectType(dto.projectType) || + normalizeProjectType((contract as { bidCase?: { projectType?: string } } | null)?.bidCase?.projectType) || + '', + amount, + status, + }, + }); + if (amount > 0) { + const split = splitEven(amount); + await tx.projectBudget.createMany({ + data: [ + { projectId: project.id, category: BUDGET_CATEGORY.BUSINESS, amount: split.business }, + { projectId: project.id, category: BUDGET_CATEGORY.TECH, amount: split.tech }, + ], + }); + } + return project; + }); + return this.get(created.id, user); + } + + async update( + id: string, + user: AuthUser, + dto: { hourRate?: number; amount?: number; projectType?: string; name?: string }, + ) { + this.assertAccess(user); + if (!canSetBudget(user.roles) && !canCreateProject(user.roles)) { + throw new ForbiddenException('无权调整项目台账'); + } + const row = await this.require(id); + this.assertOpen(row.status); + const data: Prisma.ProjectUpdateInput = {}; + if (dto.hourRate !== undefined) { + if (dto.hourRate <= 0) throw new BadRequestException('请填写大于 0 的人工单价'); + data.hourRate = dto.hourRate; + } + if (dto.projectType !== undefined) data.projectType = normalizeProjectType(dto.projectType); + if (dto.name?.trim()) data.name = dto.name.trim(); + if (dto.amount !== undefined) { + if (dto.amount < 0) throw new BadRequestException('项目总额不能为负'); + data.amount = dto.amount; + } + if (!Object.keys(data).length) throw new BadRequestException('没有要保存的内容'); + await this.prisma.project.update({ where: { id }, data }); + if (dto.amount !== undefined && dto.amount > 0) { + await this.syncSplitBudgets(id, user, { amount: dto.amount }); + } + return this.get(id, user); + } + + async replaceBudgets(id: string, user: AuthUser, items: BudgetItemInput[]) { + this.assertAccess(user); + if (!canSetBudget(user.roles)) throw new ForbiddenException('无权设置项目预算'); + const row = await this.require(id); + this.assertOpen(row.status); + const allowed = new Set(Object.values(BUDGET_CATEGORY)); + const seen = new Set(); + const cleaned: BudgetItemInput[] = []; + for (const item of items || []) { + if (!allowed.has(item.category as (typeof BUDGET_CATEGORY)[keyof typeof BUDGET_CATEGORY])) { + throw new BadRequestException('预算类别必须是商务、技术、人工、差旅或采购'); + } + if (seen.has(item.category)) throw new BadRequestException('同一预算类别不要重复'); + if (item.amount === undefined || item.amount < 0) throw new BadRequestException('预算金额不能为负'); + seen.add(item.category); + cleaned.push({ category: item.category, amount: roundMoney(item.amount) }); + } + await this.prisma.$transaction(async (tx) => { + await tx.projectBudget.deleteMany({ where: { projectId: id } }); + if (cleaned.length) { + await tx.projectBudget.createMany({ + data: cleaned.map((c) => ({ projectId: id, category: c.category, amount: c.amount })), + }); + } + }); + return this.get(id, user); + } + + async syncSplitBudgets(id: string, user: AuthUser, dto?: { amount?: number }) { + this.assertAccess(user); + if (!canSetBudget(user.roles) && !canCreateProject(user.roles)) { + throw new ForbiddenException('无权同步项目预算'); + } + const row = await this.prisma.project.findFirst({ + where: { id, tenantId: '1' }, + include: { contract: { select: { amount: true } } }, + }); + if (!row) throw new NotFoundException('项目不存在'); + this.assertOpen(row.status); + const amount = + dto?.amount && dto.amount > 0 + ? dto.amount + : Number(row.amount || 0) || Number(row.contract?.amount || 0); + if (amount <= 0) { + throw new BadRequestException('请填写项目总额,或先关联带金额的合同'); + } + const split = splitEven(amount); + await this.prisma.$transaction(async (tx) => { + await tx.project.update({ where: { id }, data: { amount } }); + await tx.projectBudget.deleteMany({ + where: { projectId: id, category: { in: [BUDGET_CATEGORY.BUSINESS, BUDGET_CATEGORY.TECH] } }, + }); + await tx.projectBudget.createMany({ + data: [ + { projectId: id, category: BUDGET_CATEGORY.BUSINESS, amount: split.business }, + { projectId: id, category: BUDGET_CATEGORY.TECH, amount: split.tech }, + ], + }); + }); + return this.get(id, user); + } + + async listTasks(user: AuthUser) { + this.assertAccess(user); + return this.prisma.projectTask.findMany({ + include: { + project: { select: { id: true, projectNo: true, name: true, status: true } }, + dept: { select: DEPT_SELECT }, + assignee: { select: USER_SELECT }, + }, + orderBy: { createdAt: 'desc' }, + }); + } + + async createTask(projectId: string, user: AuthUser, dto: TaskInput) { + this.assertAccess(user); + if (!canManageWbs(user.roles)) throw new ForbiddenException('无权分解任务'); + const project = await this.require(projectId); + this.assertOpen(project.status); + if (!dto.name?.trim()) throw new BadRequestException('请填写任务名称'); + const dept = dto.departmentId ? await this.requireDept(dto.departmentId) : null; + if (dto.assigneeId) await this.requireUser(dto.assigneeId); + const created = await this.prisma.$transaction(async (tx) => { + const task = await tx.projectTask.create({ + data: { + projectId, + name: dto.name.trim(), + departmentId: dept?.id || null, + department: dept?.name || null, + assigneeId: dto.assigneeId || null, + dueAt: dto.dueAt ? new Date(dto.dueAt) : null, + sortNo: dto.sortNo ?? 0, + status: TASK_STATUS.OPEN, + }, + }); + if (dto.assigneeId) { + await tx.todoTask.create({ + data: { + tenantId: '1', + title: `填工时:${project.projectNo} ${task.name}`, + bizType: PROJECT_BIZ.TIMESHEET, + bizId: project.id, + assigneeId: dto.assigneeId, + dueAt: task.dueAt, + }, + }); + } + return task; + }); + return created; + } + + async updateTask(projectId: string, taskId: string, user: AuthUser, dto: TaskInput & { status?: string }) { + this.assertAccess(user); + if (!canManageWbs(user.roles)) throw new ForbiddenException('无权分解任务'); + const project = await this.require(projectId); + this.assertOpen(project.status); + const task = await this.requireTask(projectId, taskId); + const dept = dto.departmentId ? await this.requireDept(dto.departmentId) : null; + if (dto.assigneeId) await this.requireUser(dto.assigneeId); + if (dto.status && dto.status !== TASK_STATUS.OPEN && dto.status !== TASK_STATUS.DONE) { + throw new BadRequestException('任务状态无效'); + } + await this.prisma.$transaction(async (tx) => { + await tx.projectTask.update({ + where: { id: taskId }, + data: { + name: dto.name?.trim() || task.name, + departmentId: dto.departmentId === undefined ? task.departmentId : dept?.id || null, + department: dto.departmentId === undefined ? task.department : dept?.name || null, + assigneeId: dto.assigneeId === undefined ? task.assigneeId : dto.assigneeId || null, + dueAt: dto.dueAt === undefined ? task.dueAt : dto.dueAt ? new Date(dto.dueAt) : null, + sortNo: dto.sortNo ?? task.sortNo, + status: dto.status || task.status, + }, + }); + if (dto.assigneeId && dto.assigneeId !== task.assigneeId) { + await tx.todoTask.create({ + data: { + tenantId: '1', + title: `填工时:${project.projectNo} ${dto.name?.trim() || task.name}`, + bizType: PROJECT_BIZ.TIMESHEET, + bizId: project.id, + assigneeId: dto.assigneeId, + dueAt: dto.dueAt ? new Date(dto.dueAt) : task.dueAt, + }, + }); + } + }); + return this.get(projectId, user); + } + + async completeTask(projectId: string, taskId: string, user: AuthUser) { + return this.updateTask(projectId, taskId, user, { name: '', status: TASK_STATUS.DONE }); + } + + async deleteTask(projectId: string, taskId: string, user: AuthUser) { + this.assertAccess(user); + if (!canManageWbs(user.roles)) throw new ForbiddenException('无权分解任务'); + await this.require(projectId); + await this.requireTask(projectId, taskId); + const used = await this.prisma.timesheet.count({ where: { taskId } }); + if (used) throw new BadRequestException('已有工时的任务不能删除'); + await this.prisma.projectTask.delete({ where: { id: taskId } }); + return this.get(projectId, user); + } + + async listTimesheets(user: AuthUser, projectId?: string) { + this.assertTimesheet(user); + const where: Prisma.TimesheetWhereInput = { tenantId: '1' }; + if (projectId) where.projectId = projectId; + if (!canManageWbs(user.roles) && !canSetBudget(user.roles)) where.userId = user.id; + return this.prisma.timesheet.findMany({ + where, + include: { + project: { select: { id: true, projectNo: true, name: true, status: true } }, + task: { select: { id: true, name: true, department: true } }, + user: { select: USER_SELECT }, + }, + orderBy: { workDate: 'desc' }, + }); + } + + async createTimesheet(user: AuthUser, dto: TimesheetInput) { + this.assertTimesheet(user); + if (!dto.hours || dto.hours <= 0 || dto.hours > 24) { + throw new BadRequestException('工时请填写 0 到 24 之间'); + } + if (!dto.workDate) throw new BadRequestException('请选择工作日期'); + const project = await this.require(dto.projectId); + this.assertOpen(project.status); + if (dto.taskId) await this.requireTask(dto.projectId, dto.taskId); + const emp = await this.prisma.employee.findFirst({ + where: { users: { some: { id: user.id } } }, + }); + if (!emp) throw new BadRequestException('当前账号未关联员工档案,工时成本按薪酬自动折算'); + const hourRate = internalHourRate({ + baseSalary: Number(emp.baseSalary), + postSalary: Number(emp.postSalary), + phoneSubsidy: Number(emp.phoneSubsidy), + transportSubsidy: Number(emp.transportSubsidy), + mealSubsidy: Number(emp.mealSubsidy), + perfBase: Number(emp.perfBase), + restSchedule: emp.restSchedule, + }); + const addCost = laborCost(dto.hours, hourRate); + await this.assertBudgetRoom(dto.projectId, BUDGET_CATEGORY.LABOR, addCost); + const created = await this.prisma.timesheet.create({ + data: { + tenantId: '1', + projectId: dto.projectId, + taskId: dto.taskId || null, + userId: user.id, + workDate: new Date(dto.workDate), + hours: dto.hours, + costAmount: addCost, + note: dto.note, + }, + }); + await this.prisma.todoTask.updateMany({ + where: { bizId: dto.projectId, bizType: PROJECT_BIZ.TIMESHEET, assigneeId: user.id, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + return created; + } + + async deleteTimesheet(id: string, user: AuthUser) { + this.assertTimesheet(user); + const row = await this.prisma.timesheet.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('工时不存在'); + if (row.userId !== user.id && !canManageWbs(user.roles)) { + throw new ForbiddenException('只能删除自己的工时'); + } + const project = await this.require(row.projectId); + this.assertOpen(project.status); + await this.prisma.timesheet.delete({ where: { id } }); + return { ok: true }; + } + + async accept(id: string, user: AuthUser, dto: AcceptInput) { + this.assertAccess(user); + if (dto.result === 'REJECTED' && !dto.comment?.trim()) { + throw new BadRequestException('不通过请填写意见'); + } + const invoices: { id: string; status: string; amount: Prisma.Decimal }[] = []; + await this.prisma.$transaction(async (tx) => { + const row = await tx.project.findFirst({ + where: { id, tenantId: '1' }, + include: { contract: { include: { milestones: { include: { invoice: true } } } } }, + }); + if (!row) throw new NotFoundException('项目不存在'); + if (dto.kind === ACCEPT_KIND.INTERNAL) { + if (!canAcceptInternal(user.roles)) throw new ForbiddenException('无权做内部验收'); + if (row.status !== PROJECT_STATUS.ACTIVE) throw new BadRequestException('当前不能做内部验收'); + await tx.projectAcceptance.create({ + data: { + projectId: id, + kind: ACCEPT_KIND.INTERNAL, + result: dto.result, + comment: dto.comment?.trim() || null, + actorId: user.id, + }, + }); + if (dto.result === 'APPROVED') { + await tx.project.update({ + where: { id }, + data: { status: PROJECT_STATUS.INTERNAL_ACCEPTED, internalAcceptedAt: new Date() }, + }); + const owners = await this.usersByRoles(tx, ['owner', 'biz_director']); + if (owners.length) { + await tx.todoTask.createMany({ + data: owners.map((u) => ({ + tenantId: '1', + title: `客户验收:${row.projectNo}`, + bizType: PROJECT_BIZ.ACCEPT, + bizId: id, + assigneeId: u.id, + })), + }); + } + } + return; + } + if (dto.kind !== ACCEPT_KIND.CUSTOMER) throw new BadRequestException('验收类型无效'); + if (!canAcceptCustomer(user.roles)) throw new ForbiddenException('无权做客户验收'); + if (row.status !== PROJECT_STATUS.INTERNAL_ACCEPTED) { + throw new BadRequestException('请先完成内部验收'); + } + if (dto.result === 'APPROVED') { + await this.assertMaterialsReady(id, tx); + } + await tx.projectAcceptance.create({ + data: { + projectId: id, + kind: ACCEPT_KIND.CUSTOMER, + result: dto.result, + comment: dto.comment?.trim() || null, + actorId: user.id, + }, + }); + if (dto.result !== 'APPROVED') return; + await tx.project.update({ + where: { id }, + data: { status: PROJECT_STATUS.ACCEPTED, customerAcceptedAt: new Date() }, + }); + await tx.todoTask.updateMany({ + where: { bizId: id, bizType: PROJECT_BIZ.ACCEPT, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + const contract = row.contract; + if (!contract) return; + for (const mile of contract.milestones) { + if (mile.invoice) continue; + const amount = roundMoney((Number(contract.amount) * Number(mile.ratio)) / 100); + if (amount <= 0) continue; + const created = await tx.invoiceRecord.create({ + data: { + tenantId: '1', + contractId: contract.id, + milestoneId: mile.id, + amount, + dueAt: mile.dueAt, + createdById: user.id, + status: INVOICE_STATUS.TO_ISSUE, + remark: `项目验收 ${row.projectNo}`, + }, + }); + invoices.push(created); + } + }); + const detail = await this.get(id, user); + return { ...detail, invoicesCreated: invoices.length }; + } + + async assertBudgetRoom( + projectId: string, + category: string, + addAmount: number, + opts?: { excludeExpenseId?: string; excludeTimesheetId?: string; tx?: Tx }, + ) { + const db = opts?.tx || this.prisma; + const project = await db.project.findFirst({ + where: { id: projectId, tenantId: '1' }, + include: { budgets: true, contract: { select: { amount: true } } }, + }); + if (!project) throw new NotFoundException('项目不存在'); + const usage = await this.usageFor(projectId, Number(project.hourRate), db, opts); + const lineUsed = + category === BUDGET_CATEGORY.LABOR + ? usage.labor + : category === BUDGET_CATEGORY.TRAVEL + ? usage.travel + : category === BUDGET_CATEGORY.TECH + ? usage.techTravel + : category === BUDGET_CATEGORY.PURCHASE + ? usage.purchase + : null; + const lineRow = project.budgets.find((b) => b.category === category); + if (lineRow && lineUsed !== null && budgetWouldExceed(lineUsed, addAmount, Number(lineRow.amount))) { + const label = + category === BUDGET_CATEGORY.LABOR + ? '人工' + : category === BUDGET_CATEGORY.TRAVEL + ? '商务差旅' + : category === BUDGET_CATEGORY.TECH + ? '技术差旅' + : '采购'; + throw new BadRequestException( + `${label}预算红线 ${Number(lineRow.amount)},已占用 ${lineUsed},本单 ${addAmount} 将超支`, + ); + } + const contractAmount = Number(project.contract?.amount || 0); + const envelopeSide = envelopeOfExpense(category); + if (envelopeSide) { + const cap = envelopeLimit(envelopeSide, project.budgets, contractAmount); + const envelopeUsed = + envelopeSide === BUDGET_CATEGORY.BUSINESS ? usage.businessUsed : usage.techUsed; + const envLabel = envelopeSide === BUDGET_CATEGORY.BUSINESS ? '商务' : '技术'; + if (budgetWouldExceed(envelopeUsed, addAmount, cap)) { + throw new BadRequestException( + `${envLabel}预算 ${cap},已占用 ${envelopeUsed},本单 ${addAmount} 将超支`, + ); + } + } + } + + private async usageMap(ids: string[]) { + const map = new Map>(); + if (!ids.length) return map; + const projects = await this.prisma.project.findMany({ + where: { id: { in: ids } }, + include: { budgets: true, contract: { select: { amount: true } } }, + }); + for (const p of projects) { + map.set( + p.id, + await this.usageFor(p.id, Number(p.hourRate), this.prisma, undefined, { + presalesCost: Number(p.presalesCost), + contractAmount: Number(p.contract?.amount || 0), + budgets: p.budgets, + }), + ); + } + return map; + } + + private async usageFor( + projectId: string, + hourRate: number, + db: PrismaService | Tx, + opts?: { excludeExpenseId?: string; excludeTimesheetId?: string }, + extras?: { + presalesCost?: number; + contractAmount?: number; + budgets?: { category: string; amount: Prisma.Decimal }[]; + }, + ) { + const [sheets, expenses, budgets, purchases, project] = await Promise.all([ + db.timesheet.findMany({ + where: { + projectId, + ...(opts?.excludeTimesheetId ? { id: { not: opts.excludeTimesheetId } } : {}), + }, + select: { hours: true, costAmount: true }, + }), + db.expenseClaim.findMany({ + where: { + projectId, + status: { in: HOLD_EXPENSE }, + ...(opts?.excludeExpenseId ? { id: { not: opts.excludeExpenseId } } : {}), + }, + select: { amount: true, budgetCategory: true }, + }), + extras?.budgets + ? Promise.resolve(extras.budgets) + : db.projectBudget.findMany({ where: { projectId } }), + db.purchaseRequest.findMany({ + where: { projectId, status: { in: ['PENDING', 'APPROVED'] } }, + select: { amount: true }, + }), + extras?.presalesCost !== undefined && extras?.contractAmount !== undefined + ? Promise.resolve(null) + : db.project.findFirst({ + where: { id: projectId }, + select: { presalesCost: true, amount: true, contract: { select: { amount: true } } }, + }), + ]); + const hours = sheets.reduce((s, r) => s + Number(r.hours), 0); + const labor = roundMoney(sheets.reduce((s, r) => s + Number(r.costAmount), 0)); + const travel = expenses + .filter((e) => e.budgetCategory === BUDGET_CATEGORY.TRAVEL) + .reduce((s, e) => s + Number(e.amount), 0); + const techTravel = expenses + .filter((e) => e.budgetCategory === BUDGET_CATEGORY.TECH) + .reduce((s, e) => s + Number(e.amount), 0); + const purchase = + expenses + .filter((e) => e.budgetCategory === BUDGET_CATEGORY.PURCHASE) + .reduce((s, e) => s + Number(e.amount), 0) + + purchases.reduce((s, p) => s + Number(p.amount), 0); + const presalesCost = extras?.presalesCost ?? Number(project?.presalesCost || 0); + const fromProject = Number((project as { amount?: unknown } | null)?.amount || 0); + const fromContract = Number(project?.contract?.amount || 0); + const contractAmount = extras?.contractAmount ?? (fromProject || fromContract); + const suggested = contractAmount > 0 ? splitEven(contractAmount) : { business: 0, tech: 0 }; + const businessUsed = roundMoney(presalesCost + travel); + const techUsed = roundMoney(labor + purchase + techTravel); + return { + hours: roundMoney(hours), + labor, + travel: roundMoney(travel), + techTravel: roundMoney(techTravel), + purchase: roundMoney(purchase), + presalesCost: roundMoney(presalesCost), + businessUsed, + techUsed, + contractAmount: roundMoney(contractAmount), + suggested, + hourRate, + limits: { + labor: this.limitOf(budgets, BUDGET_CATEGORY.LABOR), + travel: this.limitOf(budgets, BUDGET_CATEGORY.TRAVEL), + purchase: this.limitOf(budgets, BUDGET_CATEGORY.PURCHASE), + business: envelopeLimit(BUDGET_CATEGORY.BUSINESS, budgets, contractAmount), + tech: envelopeLimit(BUDGET_CATEGORY.TECH, budgets, contractAmount), + }, + }; + } + + private emptyUsage( + hourRate: number, + budgets: { category: string; amount: Prisma.Decimal }[], + extras?: { presalesCost?: number; contractAmount?: number }, + ) { + const contractAmount = extras?.contractAmount ?? 0; + const presalesCost = extras?.presalesCost ?? 0; + const suggested = contractAmount > 0 ? splitEven(contractAmount) : { business: 0, tech: 0 }; + return { + hours: 0, + labor: 0, + travel: 0, + techTravel: 0, + purchase: 0, + presalesCost: roundMoney(presalesCost), + businessUsed: roundMoney(presalesCost), + techUsed: 0, + contractAmount: roundMoney(contractAmount), + suggested, + hourRate, + limits: { + labor: this.limitOf(budgets, BUDGET_CATEGORY.LABOR), + travel: this.limitOf(budgets, BUDGET_CATEGORY.TRAVEL), + purchase: this.limitOf(budgets, BUDGET_CATEGORY.PURCHASE), + business: envelopeLimit(BUDGET_CATEGORY.BUSINESS, budgets, contractAmount), + tech: envelopeLimit(BUDGET_CATEGORY.TECH, budgets, contractAmount), + }, + }; + } + + private limitOf(budgets: { category: string; amount: Prisma.Decimal }[], category: string) { + const row = budgets.find((b) => b.category === category); + return row ? Number(row.amount) : null; + } + + private myActions( + row: { status: string }, + user: AuthUser, + ) { + const open = !isProjectClosed(row.status); + return { + canSetBudget: open && canSetBudget(user.roles), + canManageWbs: open && canManageWbs(user.roles), + canFillTimesheet: open && canFillTimesheet(user.roles, user.permissions), + canAcceptInternal: row.status === PROJECT_STATUS.ACTIVE && canAcceptInternal(user.roles), + canAcceptCustomer: row.status === PROJECT_STATUS.INTERNAL_ACCEPTED && canAcceptCustomer(user.roles), + canEditMaterials: open && (canAcceptCustomer(user.roles) || canAcceptInternal(user.roles) || canManageWbs(user.roles) || canSetBudget(user.roles)), + }; + } + + private assertOpen(status: string) { + if (isProjectClosed(status)) { + throw new BadRequestException(status === PROJECT_STATUS.DONE ? '项目已完成,不能再改执行数据' : '项目已验收,不能再改执行数据'); + } + } + + private async require(id: string) { + const row = await this.prisma.project.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('项目不存在'); + return row; + } + + async addAcceptMaterial(projectId: string, user: AuthUser, dto: { name: string; remark?: string; required?: boolean }) { + this.assertAccess(user); + const project = await this.require(projectId); + this.assertOpen(project.status); + this.assertCanEditMaterials(user); + if (!dto.name?.trim()) throw new BadRequestException('请填写材料名称'); + const max = await this.prisma.projectAcceptMaterial.aggregate({ + where: { projectId }, + _max: { sortNo: true }, + }); + await this.prisma.projectAcceptMaterial.create({ + data: { + projectId, + name: dto.name.trim(), + remark: dto.remark?.trim() || null, + required: dto.required !== false, + sortNo: (max._max.sortNo ?? 0) + 1, + }, + }); + return this.get(projectId, user); + } + + async seedAcceptMaterials(projectId: string, user: AuthUser) { + this.assertAccess(user); + const project = await this.require(projectId); + this.assertOpen(project.status); + this.assertCanEditMaterials(user); + const existing = await this.prisma.projectAcceptMaterial.count({ where: { projectId } }); + if (existing) throw new BadRequestException('已经有材料清单,不必再带入'); + await this.prisma.projectAcceptMaterial.createMany({ + data: DEFAULT_ACCEPT_MATERIALS.map((name, i) => ({ + projectId, + name, + required: true, + sortNo: i + 1, + })), + }); + return this.get(projectId, user); + } + + async updateAcceptMaterial( + projectId: string, + materialId: string, + user: AuthUser, + dto: { name?: string; remark?: string | null; required?: boolean; sortNo?: number }, + ) { + this.assertAccess(user); + const project = await this.require(projectId); + this.assertOpen(project.status); + this.assertCanEditMaterials(user); + const row = await this.prisma.projectAcceptMaterial.findFirst({ where: { id: materialId, projectId } }); + if (!row) throw new NotFoundException('验收材料不存在'); + await this.prisma.projectAcceptMaterial.update({ + where: { id: materialId }, + data: { + name: dto.name?.trim() || undefined, + remark: dto.remark === undefined ? undefined : dto.remark?.trim() || null, + required: dto.required, + sortNo: dto.sortNo, + }, + }); + return this.get(projectId, user); + } + + async deleteAcceptMaterial(projectId: string, materialId: string, user: AuthUser) { + this.assertAccess(user); + const project = await this.require(projectId); + this.assertOpen(project.status); + this.assertCanEditMaterials(user); + const row = await this.prisma.projectAcceptMaterial.findFirst({ where: { id: materialId, projectId } }); + if (!row) throw new NotFoundException('验收材料不存在'); + await this.prisma.$transaction(async (tx) => { + await tx.fileAsset.deleteMany({ where: { bizType: PROJECT_BIZ.ACCEPT, bizId: materialId } }); + await tx.projectAcceptMaterial.delete({ where: { id: materialId } }); + }); + return this.get(projectId, user); + } + + private assertCanEditMaterials(user: AuthUser) { + if ( + canAcceptCustomer(user.roles) || + canAcceptInternal(user.roles) || + canManageWbs(user.roles) || + canSetBudget(user.roles) + ) { + return; + } + throw new ForbiddenException('无权维护验收材料'); + } + + private async withMaterialFiles(projectId: string) { + const rows = await this.prisma.projectAcceptMaterial.findMany({ + where: { projectId }, + orderBy: [{ sortNo: 'asc' }, { createdAt: 'asc' }], + }); + if (!rows.length) return []; + const files = await this.prisma.fileAsset.findMany({ + where: { tenantId: '1', bizType: PROJECT_BIZ.ACCEPT, bizId: { in: rows.map((r) => r.id) } }, + orderBy: { createdAt: 'asc' }, + }); + return rows.map((r) => ({ + ...r, + files: files.filter((f) => f.bizId === r.id), + uploaded: files.some((f) => f.bizId === r.id), + })); + } + + private materialsReady(materials: { required: boolean; uploaded: boolean }[]) { + const required = materials.filter((m) => m.required); + if (!required.length) return false; + return required.every((m) => m.uploaded); + } + + private async assertMaterialsReady(projectId: string, db: PrismaService | Tx) { + const rows = await db.projectAcceptMaterial.findMany({ where: { projectId, required: true } }); + if (!rows.length) { + throw new BadRequestException('请先在验收材料表里写好需要哪些材料,并逐份上传'); + } + const files = await db.fileAsset.findMany({ + where: { bizType: PROJECT_BIZ.ACCEPT, bizId: { in: rows.map((r) => r.id) } }, + select: { bizId: true }, + }); + const have = new Set(files.map((f) => f.bizId)); + const missing = rows.filter((r) => !have.has(r.id)).map((r) => r.name); + if (missing.length) { + throw new BadRequestException(`还有未上传的验收材料:${missing.join('、')}`); + } + } + + private async requireTask(projectId: string, taskId: string) { + const row = await this.prisma.projectTask.findFirst({ where: { id: taskId, projectId } }); + if (!row) throw new NotFoundException('任务不存在'); + return row; + } + + private async requireDept(id: string) { + const row = await this.prisma.department.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('部门不存在'); + return row; + } + + private async requireUser(id: string) { + const row = await this.prisma.user.findFirst({ where: { id, tenantId: '1', status: 'ACTIVE' } }); + if (!row) throw new NotFoundException('人员不存在'); + return row; + } + + private async usersByRoles(tx: Tx, roles: string[]) { + return tx.user.findMany({ + where: { + tenantId: '1', + status: 'ACTIVE', + roles: { some: { role: { code: { in: roles } } } }, + }, + }); + } + + async listChanges(user: AuthUser) { + this.assertAccess(user); + const items = await this.prisma.projectChange.findMany({ + include: { project: { select: { id: true, projectNo: true, name: true } }, actor: { select: USER_SELECT } }, + orderBy: { createdAt: 'desc' }, + }); + return { items }; + } + + async createChange(user: AuthUser, dto: { projectId: string; summary: string; amountDelta?: number }) { + this.assertAccess(user); + if (!canChangeProject(user.roles)) throw new ForbiddenException('无权登记项目变更'); + if (!dto.summary?.trim()) throw new BadRequestException('请填写变更说明'); + const project = await this.require(dto.projectId); + return this.prisma.projectChange.create({ + data: { + tenantId: '1', + projectId: project.id, + summary: dto.summary.trim(), + amountDelta: dto.amountDelta ?? 0, + actorId: user.id, + }, + include: { project: { select: { id: true, projectNo: true, name: true } } }, + }); + } + + async reviewChange(id: string, user: AuthUser, dto: { result: 'APPROVED' | 'REJECTED'; comment?: string }) { + this.assertAccess(user); + if (!canChangeProject(user.roles) && !canApprovePurchase(user.roles)) { + throw new ForbiddenException('无权审批项目变更'); + } + const row = await this.prisma.projectChange.findFirst({ where: { id }, include: { project: true } }); + if (!row) throw new NotFoundException('变更不存在'); + if (row.status !== CHANGE_STATUS.PENDING) throw new BadRequestException('该变更已办理'); + const status = dto.result === 'APPROVED' ? CHANGE_STATUS.APPROVED : CHANGE_STATUS.REJECTED; + if (status === CHANGE_STATUS.APPROVED && Number(row.amountDelta) !== 0 && row.project.contractId) { + const contract = await this.prisma.contract.findFirst({ where: { id: row.project.contractId } }); + if (contract) { + await this.prisma.contract.update({ + where: { id: contract.id }, + data: { amount: Number(contract.amount) + Number(row.amountDelta) }, + }); + } + } + return this.prisma.projectChange.update({ + where: { id }, + data: { status, comment: dto.comment?.trim() || null, decidedAt: new Date(), actorId: user.id }, + include: { project: { select: { id: true, projectNo: true, name: true } } }, + }); + } + + async deleteChange(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.projectChange.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('变更不存在'); + if (row.status === CHANGE_STATUS.APPROVED && !user.roles.includes('admin')) { + throw new BadRequestException('已通过的变更不能删除'); + } + await this.prisma.projectChange.delete({ where: { id } }); + return { ok: true }; + } + + async listPurchases(user: AuthUser) { + this.assertAccess(user); + const items = await this.prisma.purchaseRequest.findMany({ + include: { + project: { select: { id: true, projectNo: true, name: true } }, + applicant: { select: USER_SELECT }, + }, + orderBy: { createdAt: 'desc' }, + }); + return { items: items.map((i) => ({ ...i, amount: Number(i.amount) })) }; + } + + async createPurchase(user: AuthUser, dto: { title: string; amount: number; projectId?: string; remark?: string }) { + this.assertAccess(user); + if (!canFilePurchase(user.roles)) throw new ForbiddenException('无权提交采购申请'); + if (!dto.title?.trim() || !(dto.amount > 0)) throw new BadRequestException('请填写采购内容和金额'); + if (dto.projectId) { + await this.require(dto.projectId); + await this.assertBudgetRoom(dto.projectId, BUDGET_CATEGORY.PURCHASE, dto.amount); + } + return this.prisma.purchaseRequest.create({ + data: { + tenantId: '1', + title: dto.title.trim(), + amount: dto.amount, + projectId: dto.projectId || null, + remark: dto.remark?.trim() || null, + applicantId: user.id, + status: PURCHASE_STATUS.PENDING, + }, + include: { project: { select: { id: true, projectNo: true, name: true } } }, + }); + } + + async decidePurchase(id: string, user: AuthUser, dto: { result: 'APPROVED' | 'REJECTED'; comment?: string }) { + this.assertAccess(user); + if (!canApprovePurchase(user.roles)) throw new ForbiddenException('无权审批采购'); + const row = await this.prisma.purchaseRequest.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('采购申请不存在'); + if (row.status !== PURCHASE_STATUS.PENDING) throw new BadRequestException('该申请已办理'); + if (dto.result === 'APPROVED' && row.projectId) { + await this.assertBudgetRoom(row.projectId, BUDGET_CATEGORY.PURCHASE, Number(row.amount)); + } + return this.prisma.$transaction(async (tx) => { + const updated = await tx.purchaseRequest.update({ + where: { id }, + data: { + status: dto.result === 'APPROVED' ? PURCHASE_STATUS.APPROVED : PURCHASE_STATUS.REJECTED, + remark: dto.comment?.trim() || row.remark, + decidedById: user.id, + decidedAt: new Date(), + }, + include: { project: { select: { id: true, projectNo: true, name: true } } }, + }); + if (dto.result === 'APPROVED' && Number(row.amount) > 0) { + const year = new Date().getFullYear(); + const count = await tx.paymentRecord.count({ where: { payNo: { startsWith: `PAY-${year}-` } } }); + await tx.paymentRecord.create({ + data: { + tenantId: '1', + payNo: `PAY-${year}-${String(count + 1).padStart(5, '0')}`, + kind: 'PURCHASE', + refId: row.id, + amount: row.amount, + remark: `采购 ${row.title}`, + createdById: user.id, + status: 'TODO', + }, + }); + } + return updated; + }); + } + + async deletePurchase(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.purchaseRequest.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('采购申请不存在'); + if (row.status === PURCHASE_STATUS.APPROVED && !user.roles.includes('admin')) { + throw new BadRequestException('已通过的采购不能删除'); + } + await this.prisma.purchaseRequest.delete({ where: { id } }); + return { ok: true }; + } + + async listAssets(user: AuthUser) { + this.assertAccess(user); + const items = await this.prisma.assetOccupancy.findMany({ + include: { + project: { select: { id: true, projectNo: true, name: true } }, + holderUser: { select: USER_SELECT }, + registeredAsset: { select: { id: true, kind: true, name: true, code: true } }, + }, + orderBy: { createdAt: 'desc' }, + }); + return { items }; + } + + async occupyAsset( + user: AuthUser, + dto: { + registeredAssetId?: string; + assetName?: string; + holderId?: string; + holder?: string; + projectId?: string; + dueAt?: string; + }, + ) { + this.assertAccess(user); + if (!dto.registeredAssetId) throw new BadRequestException('请先选择数字资产或固定资产,再从台账里选具体器材'); + const asset = await this.prisma.registeredAsset.findFirst({ + where: { id: dto.registeredAssetId, tenantId: '1' }, + }); + if (!asset) throw new NotFoundException('台账中没有该资产,请先到资产管理登记'); + if (asset.status === 'PENDING_IN') throw new BadRequestException('该资产尚未入库,不能占用'); + const busy = await this.prisma.assetOccupancy.findFirst({ + where: { registeredAssetId: asset.id, status: ASSET_STATUS.IN_USE }, + }); + if (busy) throw new BadRequestException(`「${asset.name}」已被 ${busy.holder} 占用,请先交回`); + let holder = dto.holder?.trim() || user.displayName; + if (dto.holderId) { + const u = await this.requireUser(dto.holderId); + holder = u.displayName; + } + if (dto.projectId) await this.require(dto.projectId); + const row = await this.prisma.assetOccupancy.create({ + data: { + tenantId: '1', + assetName: asset.name, + holder, + holderId: dto.holderId || user.id, + projectId: dto.projectId || null, + registeredAssetId: asset.id, + dueAt: dto.dueAt ? new Date(dto.dueAt) : null, + status: ASSET_STATUS.IN_USE, + }, + include: { + project: { select: { id: true, projectNo: true, name: true } }, + registeredAsset: { select: { id: true, kind: true, name: true, code: true } }, + }, + }); + return row; + } + + async returnAsset(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.assetOccupancy.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('占用记录不存在'); + return this.prisma.assetOccupancy.update({ + where: { id }, + data: { status: ASSET_STATUS.RETURNED, returnedAt: new Date() }, + }); + } + + async deleteOccupancy(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.assetOccupancy.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('占用记录不存在'); + await this.prisma.assetOccupancy.delete({ where: { id } }); + return { ok: true }; + } + + async resourceBoard(user: AuthUser) { + this.assertAccess(user); + const start = new Date(new Date().getFullYear(), new Date().getMonth(), 1); + const employees = await this.prisma.employee.findMany({ + where: { tenantId: '1', employmentStatus: { not: 'LEFT' } }, + include: { department: true, users: true }, + orderBy: { employeeNo: 'asc' }, + }); + const tasks = await this.prisma.projectTask.findMany({ + where: { status: 'OPEN', assigneeId: { not: null } }, + include: { project: { select: { projectNo: true, name: true } } }, + }); + const sheets = await this.prisma.timesheet.findMany({ where: { workDate: { gte: start } } }); + const items = employees.map((e) => { + const userIds = e.users.map((u) => u.id); + const mine = tasks.filter((t) => t.assigneeId && userIds.includes(t.assigneeId)); + const monthHours = sheets.filter((s) => userIds.includes(s.userId)).reduce((s, r) => s + Number(r.hours), 0); + const load = resourceLoad(mine.length, monthHours); + return { + id: e.id, + name: e.name, + employeeNo: e.employeeNo, + departmentName: e.department.name, + openTasks: mine.length, + monthHours: roundMoney(monthHours), + load, + projects: [...new Set(mine.map((t) => t.project.projectNo))].join('、') || '—', + }; + }); + return { items }; + } +} diff --git a/apps/api/src/push/push.controller.ts b/apps/api/src/push/push.controller.ts new file mode 100644 index 0000000..1780697 --- /dev/null +++ b/apps/api/src/push/push.controller.ts @@ -0,0 +1,27 @@ +import { Body, Controller, Delete, Post } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator'; +import { PushService } from './push.service'; + +@ApiTags('push') +@ApiBearerAuth() +@Controller('push') +export class PushController { + constructor(private readonly push: PushService) {} + + @Post('devices') + register( + @CurrentUser() user: AuthUser, + @Body() body: { vendor?: string; regId?: string; brand?: string; model?: string }, + ) { + return this.push.register(user.id, body); + } + + @Delete('devices') + unregister( + @CurrentUser() user: AuthUser, + @Body() body: { vendor?: string; regId?: string }, + ) { + return this.push.unregister(user.id, body.vendor, body.regId); + } +} diff --git a/apps/api/src/push/push.module.ts b/apps/api/src/push/push.module.ts new file mode 100644 index 0000000..9e80bc8 --- /dev/null +++ b/apps/api/src/push/push.module.ts @@ -0,0 +1,11 @@ +import { Global, Module } from '@nestjs/common'; +import { PushController } from './push.controller'; +import { PushService } from './push.service'; + +@Global() +@Module({ + controllers: [PushController], + providers: [PushService], + exports: [PushService], +}) +export class PushModule {} diff --git a/apps/api/src/push/push.service.ts b/apps/api/src/push/push.service.ts new file mode 100644 index 0000000..c4f6ba1 --- /dev/null +++ b/apps/api/src/push/push.service.ts @@ -0,0 +1,439 @@ +import { createHash, randomUUID } from 'crypto'; +import { Injectable, Logger } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; + +export type VendorPush = { + title: string; + content: string; + conversationId?: string; + kind?: string; + category?: 'IM' | 'TODO' | 'ACCOUNT'; + bizType?: string; + bizId?: string; + callId?: string; + callKind?: string; + fromName?: string; + action?: string; +}; + +const SKIP_KINDS = new Set(['receipt', 'typing', 'screenshot']); + +@Injectable() +export class PushService { + private readonly log = new Logger(PushService.name); + private vivoToken = ''; + private vivoTokenAt = 0; + private oppoToken = ''; + private oppoTokenAt = 0; + + constructor(private readonly prisma: PrismaService) {} + + async register( + userId: string, + dto: { vendor?: string; regId?: string; brand?: string; model?: string }, + ) { + const vendor = (dto.vendor || 'vivo').trim().toLowerCase(); + const regId = (dto.regId || '').trim(); + if (!regId || !vendor) return { ok: false }; + await this.prisma.pushDevice.upsert({ + where: { vendor_regId: { vendor, regId } }, + create: { + userId, + vendor, + regId, + brand: dto.brand?.slice(0, 64) || null, + model: dto.model?.slice(0, 64) || null, + }, + update: { + userId, + brand: dto.brand?.slice(0, 64) || undefined, + model: dto.model?.slice(0, 64) || undefined, + }, + }); + return { ok: true, vendor }; + } + + async unregister(userId: string, vendor?: string, regId?: string) { + await this.prisma.pushDevice.deleteMany({ + where: { + userId, + ...(vendor ? { vendor } : {}), + ...(regId ? { regId } : {}), + }, + }); + return { ok: true }; + } + + async notifyUsers(userIds: string[], msg: VendorPush) { + const kind = msg.kind || ''; + if (SKIP_KINDS.has(kind)) return; + const ids = [...new Set(userIds.filter(Boolean))]; + if (!ids.length) return; + const devices = await this.prisma.pushDevice.findMany({ + where: { userId: { in: ids } }, + }); + await Promise.all( + devices.map((d) => { + if (d.vendor === 'vivo') return this.sendVivo(d.regId, msg); + if (d.vendor === 'oppo') return this.sendOppo(d.regId, msg); + if (d.vendor === 'meizu') return this.sendMeizu(d.regId, msg); + if (d.vendor === 'huawei' || d.vendor === 'honor') return this.sendHuawei(d.regId, msg); + return Promise.resolve(); + }), + ); + } + + private async sendVivo(regId: string, msg: VendorPush) { + const appId = Number(process.env.VIVO_PUSH_APP_ID || ''); + const appKey = process.env.VIVO_PUSH_APP_KEY || ''; + const appSecret = process.env.VIVO_PUSH_APP_SECRET || ''; + if (!appId || !appKey || !appSecret) return; + const token = await this.vivoAuth(appId, appKey, appSecret); + if (!token) return; + const title = clip(msg.title || '风影办公', 20); + const content = clip(msg.content || '你有一条新消息', 50); + const category = msg.category || (msg.kind === 'todo' || msg.kind === 'approval' ? 'TODO' : 'IM'); + const body: Record = { + regId, + notifyType: 4, + title, + content, + timeToLive: 86400, + skipType: 1, + networkType: -1, + classification: 1, + category, + requestId: randomUUID().replace(/-/g, '').slice(0, 32), + clientCustomMap: { + conversationId: msg.conversationId || '', + kind: msg.kind || 'chat', + bizType: msg.bizType || '', + bizId: msg.bizId || '', + callId: msg.callId || '', + callKind: msg.callKind || '', + fromName: msg.fromName || '', + action: msg.action || '', + }, + }; + try { + const res = await fetch('https://api-push.vivo.com.cn/message/send', { + method: 'POST', + headers: { 'Content-Type': 'application/json', authToken: token }, + body: JSON.stringify(body), + }); + const json = (await res.json()) as { result?: number; desc?: string }; + if (json.result === 10302) { + await this.prisma.pushDevice.deleteMany({ where: { vendor: 'vivo', regId } }); + this.log.warn(`vivo regId 失效已清理`); + return; + } + if (json.result && json.result !== 0) { + this.log.warn(`vivo send ${json.result} ${json.desc || ''}`); + } + } catch (e) { + this.log.warn(`vivo send: ${(e as Error).message}`); + } + } + + private async vivoAuth(appId: number, appKey: string, appSecret: string) { + if (this.vivoToken && Date.now() - this.vivoTokenAt < 20 * 60 * 60 * 1000) return this.vivoToken; + const timestamp = Date.now(); + const sign = createHash('md5') + .update(`${appId}${appKey}${timestamp}${appSecret}`) + .digest('hex'); + try { + const res = await fetch('https://api-push.vivo.com.cn/message/auth', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ appId, appKey, timestamp, sign }), + }); + const json = (await res.json()) as { result?: number; authToken?: string; desc?: string }; + if (json.result !== 0 || !json.authToken) { + this.log.warn(`vivo auth ${json.result} ${json.desc || ''}`); + return ''; + } + this.vivoToken = json.authToken; + this.vivoTokenAt = Date.now(); + return this.vivoToken; + } catch (e) { + this.log.warn(`vivo auth: ${(e as Error).message}`); + return ''; + } + } + + private async sendOppo(regId: string, msg: VendorPush) { + const appKey = process.env.OPPO_PUSH_APP_KEY || ''; + const master = process.env.OPPO_PUSH_MASTER_SECRET || process.env.OPPO_PUSH_APP_SECRET || ''; + if (!appKey || !master) return; + const token = await this.oppoAuth(appKey, master); + if (!token) return; + const title = clip(msg.title || '风影办公', 32); + const content = clip(msg.content || '你有一条新消息', 100); + const category = msg.category || (msg.kind === 'todo' || msg.kind === 'approval' ? 'TODO' : 'IM'); + const channelId = msg.kind === 'call' ? 'fy_call_v2' : category === 'IM' ? 'fy_im_custom' : 'fy_todo'; + const message = { + target_type: 2, + target_value: regId, + notification: { + app_message_id: randomUUID().replace(/-/g, '').slice(0, 32), + title, + content, + click_action_type: 0, + channel_id: channelId, + off_line: true, + off_line_ttl: 86400, + category, + notify_level: category === 'IM' ? 2 : 1, + action_parameters: JSON.stringify({ + conversationId: msg.conversationId || '', + kind: msg.kind || 'chat', + bizType: msg.bizType || '', + bizId: msg.bizId || '', + callId: msg.callId || '', + callKind: msg.callKind || '', + fromName: msg.fromName || '', + action: msg.action || '', + }), + }, + }; + try { + const form = new URLSearchParams({ + auth_token: token, + message: JSON.stringify(message), + }); + const res = await fetch('https://api.push.oppomobile.com/server/v1/message/notification/unicast', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, + body: form.toString(), + }); + const json = (await res.json()) as { code?: number; message?: string }; + if (json.code === 33 || json.code === 10000) { + await this.prisma.pushDevice.deleteMany({ where: { vendor: 'oppo', regId } }); + this.log.warn(`oppo regId 失效已清理`); + return; + } + if (json.code && json.code !== 0) { + this.log.warn(`oppo send ${json.code} ${json.message || ''}`); + } + } catch (e) { + this.log.warn(`oppo send: ${(e as Error).message}`); + } + } + + private async oppoAuth(appKey: string, masterSecret: string) { + if (this.oppoToken && Date.now() - this.oppoTokenAt < 20 * 60 * 60 * 1000) return this.oppoToken; + const timestamp = String(Date.now()); + const sign = createHash('sha256').update(`${appKey}${timestamp}${masterSecret}`).digest('hex'); + try { + const form = new URLSearchParams({ app_key: appKey, timestamp, sign }); + const res = await fetch('https://api.push.oppomobile.com/server/v1/auth', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, + body: form.toString(), + }); + const json = (await res.json()) as { + code?: number; + message?: string; + data?: { auth_token?: string }; + }; + if (json.code !== 0 || !json.data?.auth_token) { + this.log.warn(`oppo auth ${json.code} ${json.message || ''}`); + return ''; + } + this.oppoToken = json.data.auth_token; + this.oppoTokenAt = Date.now(); + return this.oppoToken; + } catch (e) { + this.log.warn(`oppo auth: ${(e as Error).message}`); + return ''; + } + } + + private async sendMeizu(pushId: string, msg: VendorPush) { + const appId = process.env.MEIZU_PUSH_APP_ID || ''; + const appSecret = process.env.MEIZU_PUSH_APP_SECRET || ''; + if (!appId || !appSecret) return; + const title = clip(msg.title || '风影办公', 16); + const content = clip(msg.content || '你有一条新消息', 50); + const messageJson = JSON.stringify({ + noticeBarInfo: { + noticeMsgType: 1, + noticeBarType: 0, + title, + content, + }, + clickTypeInfo: { + clickType: 0, + parameters: { + conversationId: msg.conversationId || '', + kind: msg.kind || 'chat', + bizType: msg.bizType || '', + bizId: msg.bizId || '', + callId: msg.callId || '', + callKind: msg.callKind || '', + fromName: msg.fromName || '', + action: msg.action || '', + }, + }, + pushTimeInfo: { offLine: 1, validTime: 24 }, + advanceInfo: { + suspend: 1, + clearNoticeBar: 1, + notificationType: { vibrate: 1, lights: 1, sound: 1 }, + }, + }); + const params: Record = { appId, pushIds: pushId, messageJson }; + params.sign = meizuSign(params, appSecret); + try { + const res = await fetch('https://server-api-push.meizu.com/garcia/api/server/push/varnished/pushByPushId', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' }, + body: new URLSearchParams(params).toString(), + }); + const json = (await res.json()) as { code?: string | number; message?: string; msg?: string }; + const code = String(json.code ?? ''); + if (code === '200' || code === '0') return; + if (code === '512' || code === '513' || /invalid|不存在|失效/i.test(json.message || json.msg || '')) { + await this.prisma.pushDevice.deleteMany({ where: { vendor: 'meizu', regId: pushId } }); + this.log.warn(`meizu pushId 失效已清理`); + return; + } + this.log.warn(`meizu send ${code} ${json.message || json.msg || ''}`); + } catch (e) { + this.log.warn(`meizu send: ${(e as Error).message}`); + } + } + + private huaweiToken = ''; + private huaweiTokenAt = 0; + + private async sendHuawei(token: string, msg: VendorPush) { + const appId = process.env.HUAWEI_PUSH_APP_ID || '118825899'; + const clientId = process.env.HUAWEI_PUSH_CLIENT_ID || appId; + const secret = process.env.HUAWEI_PUSH_CLIENT_SECRET || ''; + if (!secret) return; + const auth = await this.huaweiAuth(clientId, secret); + if (!auth) return; + const data = { + conversationId: msg.conversationId || '', + kind: msg.kind || '', + bizType: msg.bizType || '', + bizId: msg.bizId || '', + callId: msg.callId || '', + callKind: msg.callKind || '', + fromName: msg.fromName || '', + action: msg.action || '', + }; + const qs = new URLSearchParams(data).toString() ? `?${new URLSearchParams(data).toString()}` : ''; + const intent = `intent://chat${qs}#Intent;scheme=fengyingoa;launchFlags=0x04000000;end`; + // 安卓 / 能装 APK 的鸿蒙:HMS Push v1 + await this.huaweiPost( + // Push Kit REST v1 使用 AGC 项目设置中的 OAuth client_id 作为路径参数, + // 不是 App ID;两者相同的项目也统一走 clientId,避免凭据与应用错配。 + `https://push-api.cloud.huawei.com/v1/${clientId}/messages:send`, + auth, + { + message: { + android: { + notification: { + title: clip(msg.title, 40), + body: clip(msg.content, 100), + click_action: { type: 1, intent }, + channel_id: msg.kind === 'call' ? 'fy_call_v2' : msg.category === 'TODO' ? 'fy_todo' : 'fy_im_custom', + importance: msg.category === 'TODO' ? 'NORMAL' : 'HIGH', + default_sound: false, + use_default_vibrate: true, + }, + category: msg.category === 'TODO' ? 'WORK' : 'IM', + urgency: msg.category === 'TODO' ? 'NORMAL' : 'HIGH', + }, + token: [token], + data: JSON.stringify(data), + }, + }, + token, + ); + } + + private async huaweiPost( + url: string, + auth: string, + body: unknown, + token: string, + extraHeaders: Record = {}, + ) { + try { + const res = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${auth}`, + 'Content-Type': 'application/json', + ...extraHeaders, + }, + body: JSON.stringify(body), + }); + const json = (await res.json()) as { code?: string; msg?: string }; + if (json.code === '80300007' || json.code === '80100003') { + await this.prisma.pushDevice.deleteMany({ + where: { vendor: { in: ['huawei', 'honor'] }, regId: token }, + }); + this.log.warn('huawei token 失效已清理'); + return true; + } + if (json.code === '80000000') return true; + this.log.warn(`huawei send ${url.includes('/v3/') ? 'v3' : 'v1'} ${json.code} ${json.msg || ''}`); + return false; + } catch (e) { + this.log.warn(`huawei send: ${(e as Error).message}`); + return false; + } + } + + private async huaweiAuth(clientId: string, clientSecret: string) { + if (this.huaweiToken && Date.now() - this.huaweiTokenAt < 50 * 60 * 1000) return this.huaweiToken; + try { + const res = await fetch('https://oauth-login.cloud.huawei.com/oauth2/v3/token', { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: new URLSearchParams({ + grant_type: 'client_credentials', + client_id: clientId, + client_secret: clientSecret, + }).toString(), + }); + const json = (await res.json()) as { access_token?: string; error?: string }; + if (!json.access_token) { + this.log.warn(`huawei auth ${json.error || res.status}`); + return ''; + } + this.huaweiToken = json.access_token; + this.huaweiTokenAt = Date.now(); + return this.huaweiToken; + } catch (e) { + this.log.warn(`huawei auth: ${(e as Error).message}`); + return ''; + } + } +} + +function clip(text: string, maxHans: number) { + const raw = (text || '').replace(/\s+/g, ' ').trim(); + let units = 0; + let out = ''; + for (const ch of raw) { + units += /[\u4e00-\u9fff]/.test(ch) ? 1 : 0.5; + if (units > maxHans) break; + out += ch; + } + return out || '通知'; +} + +function meizuSign(params: Record, appSecret: string) { + const raw = + Object.keys(params) + .filter((k) => k !== 'sign' && params[k] !== '') + .sort() + .map((k) => `${k}=${params[k]}`) + .join('') + appSecret; + return createHash('md5').update(raw).digest('hex'); +} diff --git a/apps/api/src/report/report.controller.ts b/apps/api/src/report/report.controller.ts new file mode 100644 index 0000000..05f5df9 --- /dev/null +++ b/apps/api/src/report/report.controller.ts @@ -0,0 +1,36 @@ +import { Controller, Get } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator'; +import { ReportService } from './report.service'; + +@ApiTags('reports') +@ApiBearerAuth() +@Controller('reports') +export class ReportController { + constructor(private readonly reports: ReportService) {} + + @Get('bid-conversion') + bidConversion(@CurrentUser() user: AuthUser) { + return this.reports.bidConversion(user); + } + + @Get('project-profit') + projectProfit(@CurrentUser() user: AuthUser) { + return this.reports.projectProfit(user); + } + + @Get('dept-efficiency') + deptEfficiency(@CurrentUser() user: AuthUser) { + return this.reports.deptEfficiency(user); + } + + @Get('cash-lock') + cashLock(@CurrentUser() user: AuthUser) { + return this.reports.cashLock(user); + } + + @Get('competitor-wins') + competitorWins(@CurrentUser() user: AuthUser) { + return this.reports.competitorWins(user); + } +} diff --git a/apps/api/src/report/report.service.ts b/apps/api/src/report/report.service.ts new file mode 100644 index 0000000..5925a8d --- /dev/null +++ b/apps/api/src/report/report.service.ts @@ -0,0 +1,170 @@ +import { ForbiddenException, Injectable } from '@nestjs/common'; +import { PrismaService } from '../prisma/prisma.service'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { BID_STATUS } from '../bidding/bid-workflow'; +import { EXPENSE_STATUS, BOND_STATUS, INVOICE_STATUS } from '../finance/finance-workflow'; +import { canAccessReport, FUNNEL_STAGES, PAY_STATUS, PURCHASE_STATUS } from '../system/ops-workflow'; + +@Injectable() +export class ReportService { + constructor(private readonly prisma: PrismaService) {} + + assert(user: AuthUser) { + if (!canAccessReport(user.roles, user.permissions)) { + throw new ForbiddenException('无权查看经营报表'); + } + } + + async bidConversion(user: AuthUser) { + this.assert(user); + const [bids, expenses] = await Promise.all([ + this.prisma.bidCase.findMany({ + where: { tenantId: '1' }, + select: { id: true, status: true, printCost: true }, + }), + this.prisma.expenseClaim.findMany({ + where: { + tenantId: '1', + costType: 'BID', + status: EXPENSE_STATUS.APPROVED, + bidCaseId: { not: null }, + }, + select: { bidCaseId: true, amount: true }, + }), + ]); + const spend = new Map(); + for (const e of expenses) { + if (!e.bidCaseId) continue; + spend.set(e.bidCaseId, (spend.get(e.bidCaseId) || 0) + Number(e.amount)); + } + for (const b of bids) { + spend.set(b.id, (spend.get(b.id) || 0) + Number(b.printCost || 0)); + } + return FUNNEL_STAGES.map((stage) => { + const rows = bids.filter((b) => (stage.statuses as readonly string[]).includes(b.status)); + return { + id: stage.id, + status: stage.label, + count: rows.length, + presalesSpend: round(rows.reduce((s, b) => s + (spend.get(b.id) || 0), 0)), + }; + }); + } + + async projectProfit(user: AuthUser) { + this.assert(user); + const projects = await this.prisma.project.findMany({ + include: { contract: true, expenses: true, timesheets: true, purchases: true }, + orderBy: { createdAt: 'desc' }, + }); + return projects.map((p) => { + const revenue = Number(p.contract?.amount ?? 0); + const expenseCost = p.expenses + .filter((e) => e.status === EXPENSE_STATUS.APPROVED) + .reduce((s, e) => s + Number(e.amount), 0); + const laborCost = p.timesheets.reduce((s, t) => s + Number(t.costAmount), 0); + const purchaseCost = p.purchases + .filter((x) => x.status === PURCHASE_STATUS.APPROVED || x.status === PURCHASE_STATUS.PENDING) + .reduce((s, x) => s + Number(x.amount), 0); + const cost = round(expenseCost + laborCost + purchaseCost); + return { + id: p.id, + name: p.name, + projectNo: p.projectNo, + revenue, + laborCost: round(laborCost), + expenseCost: round(expenseCost), + purchaseCost: round(purchaseCost), + presalesCost: Number(p.presalesCost), + cost, + profit: round(revenue - cost), + }; + }); + } + + async deptEfficiency(user: AuthUser) { + this.assert(user); + const start = new Date(new Date().getFullYear(), new Date().getMonth(), 1); + const depts = await this.prisma.department.findMany({ + where: { deptType: 'TECH' }, + include: { employees: true, projectTasks: true }, + orderBy: { sortNo: 'asc' }, + }); + const sheets = await this.prisma.timesheet.findMany({ + where: { workDate: { gte: start } }, + include: { task: true }, + }); + return depts.map((d) => { + const monthHours = sheets + .filter((s) => s.task?.departmentId === d.id) + .reduce((s, r) => s + Number(r.hours), 0); + const openTasks = d.projectTasks.filter((t) => t.status === 'OPEN').length; + const headcount = d.employees.filter((e) => e.employmentStatus !== 'LEFT').length; + return { + id: d.id, + name: d.name, + headcount, + openTasks, + monthHours: round(monthHours), + hoursPerHead: headcount ? round(monthHours / headcount) : 0, + }; + }); + } + + async competitorWins(user: AuthUser) { + this.assert(user); + const rows = await this.prisma.bidCase.findMany({ + where: { + tenantId: '1', + status: { in: [BID_STATUS.TERMINATED, BID_STATUS.LOST, BID_STATUS.FAILED] }, + }, + include: { + legalEntity: { select: { name: true, shortName: true } }, + party: { select: { name: true } }, + }, + orderBy: [{ winnerAmount: { sort: 'desc', nulls: 'last' } }, { updatedAt: 'desc' }], + }); + return rows.map((b) => ({ + id: b.id, + bidNo: b.bidNo, + externalNo: b.externalNo, + name: b.name, + status: b.status, + projectType: b.projectType, + partyName: b.party?.name || '', + legalEntity: b.legalEntity?.name || b.legalEntity?.shortName || '', + quoteAmount: b.quoteAmount, + winnerName: b.winnerName || '', + winnerAmount: b.winnerAmount != null ? Number(b.winnerAmount) : null, + winnerNote: b.winnerNote || '', + openedResultAt: b.openedResultAt, + createdAt: b.createdAt, + updatedAt: b.updatedAt, + })); + } + + async cashLock(user: AuthUser) { + this.assert(user); + const [bonds, invoices, payments] = await Promise.all([ + this.prisma.bondRecord.findMany({ where: { status: BOND_STATUS.PAID } }), + this.prisma.invoiceRecord.findMany({ + where: { status: { in: [INVOICE_STATUS.ISSUED, INVOICE_STATUS.OVERDUE] } }, + }), + this.prisma.paymentRecord.findMany({ where: { status: PAY_STATUS.TODO } }), + ]); + const won = await this.prisma.bidCase.count({ where: { status: BID_STATUS.WON } }); + return [ + { + id: 'cash', + unpaidBonds: round(bonds.reduce((s, b) => s + Number(b.amount), 0)), + outstandingInvoices: round(invoices.reduce((s, i) => s + Number(i.amount), 0)), + pendingPayments: round(payments.reduce((s, p) => s + Number(p.amount), 0)), + wonBids: won, + }, + ]; + } +} + +function round(n: number) { + return Math.round(n * 100) / 100; +} diff --git a/apps/api/src/seal/seal-workflow.spec.ts b/apps/api/src/seal/seal-workflow.spec.ts new file mode 100644 index 0000000..5ab840d --- /dev/null +++ b/apps/api/src/seal/seal-workflow.spec.ts @@ -0,0 +1,41 @@ +import { + borrowBucket, + canAccessSeal, + canApplySeal, + canApproveSeal, + isOverdue, + qualBucket, + sealBucket, + sealStatusAfterApprove, +} from './seal-workflow'; + +describe('seal-workflow', () => { + it('lets biz roles apply and owner/director approve', () => { + expect(canApplySeal(['biz_staff'], ['seal:requests'])).toBe(true); + expect(canApproveSeal(['owner'])).toBe(true); + expect(canApproveSeal(['biz_staff'])).toBe(false); + expect(canAccessSeal(['hr'], ['hr:org'])).toBe(false); + expect(canApplySeal(['employee'], ['office:seal-apply'])).toBe(true); + expect(canAccessSeal(['biz_director'], ['seal:qualifications'])).toBe(true); + }); + + it('warns qualifications within 30 days or already expired', () => { + expect(qualBucket(null)).toBe('valid'); + expect(qualBucket(new Date(Date.now() + 40 * 86400000))).toBe('valid'); + expect(qualBucket(new Date(Date.now() + 10 * 86400000))).toBe('expiring'); + expect(qualBucket(new Date(Date.now() - 86400000))).toBe('expired'); + }); + + it('flags overdue borrows and take-out seals', () => { + expect(borrowBucket({ status: 'RETURNED' })).toBe('returned'); + expect(borrowBucket({ status: 'BORROWED', dueAt: new Date(Date.now() - 86400000) })).toBe('overdue'); + expect(borrowBucket({ status: 'BORROWED', dueAt: new Date(Date.now() + 86400000) })).toBe('due'); + expect(sealBucket({ status: 'PENDING', takeOut: true })).toBe('pending'); + expect(sealBucket({ status: 'OUT', takeOut: true, returnAt: new Date(Date.now() - 86400000) })).toBe( + 'overdue', + ); + expect(sealStatusAfterApprove(true)).toBe('OUT'); + expect(sealStatusAfterApprove(false)).toBe('APPROVED'); + expect(isOverdue(new Date(Date.now() - 1000))).toBe(true); + }); +}); diff --git a/apps/api/src/seal/seal-workflow.ts b/apps/api/src/seal/seal-workflow.ts new file mode 100644 index 0000000..8e5d59c --- /dev/null +++ b/apps/api/src/seal/seal-workflow.ts @@ -0,0 +1,82 @@ +export const QUAL_WARN_DAYS = 30; + +export const SEAL_STATUS = { + PENDING: 'PENDING', + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', + OUT: 'OUT', + RETURNED: 'RETURNED', +} as const; + +export const BORROW_STATUS = { + BORROWED: 'BORROWED', + RETURNED: 'RETURNED', +} as const; + +export const SEAL_BIZ = { + APPROVAL: 'SEAL_APPROVAL', + RETURN: 'SEAL_RETURN', + CREDENTIAL: 'CREDENTIAL_RETURN', +} as const; + +export function canAccessSeal(roles: string[], permissions: string[]) { + if (roles.includes('admin')) return true; + return permissions.some((p) => p === 'seal' || p.startsWith('seal:')); +} + +export const SEAL_TYPES = ['公章', '合同章', '法人章', '法人签名章', '项目负责人章', '其他章'] as const; + +export function canApplySeal(roles: string[], permissions: string[]) { + if (['admin', 'owner', 'biz_director', 'biz_staff'].some((r) => roles.includes(r))) return true; + if (permissions.includes('office:seal-apply')) return true; + return canAccessSeal(roles, permissions); +} + +export function canApproveSeal(roles: string[]) { + return ['admin', 'owner', 'biz_director'].some((r) => roles.includes(r)); +} + +export function qualBucket(expiresAt?: Date | string | null) { + if (!expiresAt) return 'valid'; + const t = new Date(expiresAt).getTime(); + const now = Date.now(); + if (t < now) return 'expired'; + if (t <= now + QUAL_WARN_DAYS * 86400000) return 'expiring'; + return 'valid'; +} + +export function borrowBucket(row: { + status: string; + dueAt?: Date | string | null; + returnedAt?: Date | string | null; +}) { + if (row.status === BORROW_STATUS.RETURNED || row.returnedAt) return 'returned'; + if (row.dueAt && new Date(row.dueAt).getTime() < Date.now()) return 'overdue'; + if (row.dueAt) return 'due'; + return 'borrowed'; +} + +export function sealBucket(row: { + status: string; + takeOut: boolean; + returnAt?: Date | string | null; + returnedAt?: Date | string | null; +}) { + if (row.status === SEAL_STATUS.PENDING) return 'pending'; + if (row.status === SEAL_STATUS.REJECTED) return 'rejected'; + if (row.status === SEAL_STATUS.RETURNED || row.returnedAt) return 'returned'; + if (row.status === SEAL_STATUS.OUT || (row.takeOut && row.status === SEAL_STATUS.APPROVED && !row.returnedAt)) { + if (row.returnAt && new Date(row.returnAt).getTime() < Date.now()) return 'overdue'; + return 'out'; + } + if (row.status === SEAL_STATUS.APPROVED) return 'approved'; + return row.status.toLowerCase(); +} + +export function sealStatusAfterApprove(takeOut: boolean) { + return takeOut ? SEAL_STATUS.OUT : SEAL_STATUS.APPROVED; +} + +export function isOverdue(dueAt?: Date | string | null) { + return Boolean(dueAt && new Date(dueAt).getTime() < Date.now()); +} diff --git a/apps/api/src/seal/seal.controller.ts b/apps/api/src/seal/seal.controller.ts new file mode 100644 index 0000000..7e3dc8a --- /dev/null +++ b/apps/api/src/seal/seal.controller.ts @@ -0,0 +1,152 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser, type AuthUser } from '../common/decorators/current-user.decorator'; +import { SealService } from './seal.service'; + +@ApiTags('seal') +@ApiBearerAuth() +@Controller() +export class SealController { + constructor(private readonly seals: SealService) {} + + @Get('qualifications') + quals(@CurrentUser() user: AuthUser, @Query('bucket') bucket?: string) { + return this.seals.listQualifications(user, bucket); + } + + @Post('qualifications') + createQual( + @CurrentUser() user: AuthUser, + @Body() body: { name: string; code: string; expiresAt?: string; keeper?: string }, + ) { + return this.seals.createQualification(user, body); + } + + @Patch('qualifications/:id') + updateQual( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { name?: string; code?: string; expiresAt?: string | null; keeper?: string }, + ) { + return this.seals.updateQualification(id, user, body); + } + + @Delete('qualifications/:id') + deleteQual(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.seals.deleteQualification(id, user); + } + + @Get('credential-borrows') + borrows(@CurrentUser() user: AuthUser, @Query('bucket') bucket?: string) { + return this.seals.listBorrows(user, bucket); + } + + @Post('credential-borrows') + createBorrow( + @CurrentUser() user: AuthUser, + @Body() + body: { + itemName: string; + borrower?: string; + borrowerId?: string; + qualificationId?: string; + bidCaseId?: string; + dueAt?: string; + remark?: string; + }, + ) { + return this.seals.createBorrow(user, body); + } + + @Post('credential-borrows/:id/return') + returnBorrow(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.seals.returnBorrow(id, user); + } + + @Delete('credential-borrows/:id') + deleteBorrow(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.seals.deleteBorrow(id, user); + } + + @Get('seal-requests/:id') + requestOne(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.seals.getRequest(id, user); + } + + @Get('seal-requests') + requests( + @CurrentUser() user: AuthUser, + @Query('bucket') bucket?: string, + @Query('scope') scope?: string, + ) { + return this.seals.listSeals(user, bucket, scope); + } + + @Post('seal-requests') + createRequest( + @CurrentUser() user: AuthUser, + @Body() + body: { + sealType: string; + reason: string; + bidCaseId?: string; + contractId?: string; + applicantId?: string; + approverId?: string; + takeOut?: boolean; + returnAt?: string; + }, + ) { + return this.seals.createSealRequest(user, body); + } + + @Post('seal-requests/:id/reviews') + review( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { result: 'APPROVED' | 'REJECTED'; comment?: string }, + ) { + return this.seals.reviewSeal(id, user, body); + } + + @Post('seal-requests/:id/return') + returnSeal(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.seals.returnSeal(id, user); + } + + @Delete('seal-requests/:id') + deleteRequest(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.seals.deleteSealRequest(id, user); + } + + @Get('seals') + registry( + @CurrentUser() user: AuthUser, + @Query('legalEntityId') legalEntityId?: string, + @Query('sealType') sealType?: string, + ) { + return this.seals.listRegistry(user, legalEntityId, sealType); + } + + @Post('seals') + createSeal( + @CurrentUser() user: AuthUser, + @Body() body: { name: string; keeper?: string; sealType?: string; legalEntityId?: string }, + ) { + return this.seals.createRegistryItem(user, body); + } + + @Patch('seals/:id') + updateSeal( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { name?: string; keeper?: string; status?: string; sealType?: string; legalEntityId?: string | null }, + ) { + return this.seals.updateRegistryItem(id, user, body); + } + + @Delete('seals/:id') + deleteSeal(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.seals.deleteRegistryItem(id, user); + } +} diff --git a/apps/api/src/seal/seal.service.ts b/apps/api/src/seal/seal.service.ts new file mode 100644 index 0000000..bb7acf6 --- /dev/null +++ b/apps/api/src/seal/seal.service.ts @@ -0,0 +1,759 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import { Prisma } from '@prisma/client'; +import { PrismaService } from '../prisma/prisma.service'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { CONTRACT_STATUS } from '../contract/contract-workflow'; +import { emitSms } from '../sms/sms-bus'; +import { ownerScope, withAnd } from '../common/data-scope'; +import { canAccessBidModule } from '../bidding/bid-workflow'; +import { + BORROW_STATUS, + SEAL_BIZ, + SEAL_STATUS, + SEAL_TYPES, + borrowBucket, + canAccessSeal, + canApplySeal, + canApproveSeal, + isOverdue, + qualBucket, + sealBucket, + sealStatusAfterApprove, +} from './seal-workflow'; + +const USER_SELECT = { id: true, displayName: true, username: true }; +const BID_SELECT = { id: true, bidNo: true, name: true, status: true }; +const CONTRACT_SELECT = { id: true, contractNo: true, name: true, status: true }; +const QUAL_SELECT = { id: true, name: true, code: true, expiresAt: true }; + +type Tx = Prisma.TransactionClient; +export type ReviewInput = { result: 'APPROVED' | 'REJECTED'; comment?: string }; + +@Injectable() +export class SealService { + constructor(private readonly prisma: PrismaService) {} + + assertAccess(user: AuthUser) { + if (!canAccessSeal(user.roles, user.permissions)) { + throw new ForbiddenException('无权访问证照用章'); + } + } + + async listQualifications(user: AuthUser, bucket?: string) { + this.assertAccess(user); + const rows = await this.prisma.qualification.findMany({ + where: { tenantId: '1' }, + orderBy: { name: 'asc' }, + }); + const items = rows.map((r) => ({ ...r, bucket: qualBucket(r.expiresAt) })); + const filtered = bucket ? items.filter((i) => i.bucket === bucket) : items; + return { items: filtered, total: filtered.length }; + } + + async createQualification( + user: AuthUser, + dto: { name: string; code: string; expiresAt?: string; keeper?: string }, + ) { + this.assertAccess(user); + if (!dto.name?.trim() || !dto.code?.trim()) throw new BadRequestException('请填写资质名称和编码'); + const created = await this.prisma.qualification.create({ + data: { + tenantId: '1', + name: dto.name.trim(), + code: dto.code.trim(), + keeper: dto.keeper?.trim() || null, + expiresAt: dto.expiresAt ? new Date(dto.expiresAt) : null, + }, + }); + return { ...created, bucket: qualBucket(created.expiresAt) }; + } + + async updateQualification( + id: string, + user: AuthUser, + dto: { name?: string; code?: string; expiresAt?: string | null; keeper?: string }, + ) { + this.assertAccess(user); + const row = await this.prisma.qualification.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('资质不存在'); + const updated = await this.prisma.qualification.update({ + where: { id }, + data: { + name: dto.name?.trim() || row.name, + code: dto.code?.trim() || row.code, + keeper: dto.keeper === undefined ? row.keeper : dto.keeper?.trim() || null, + expiresAt: dto.expiresAt === undefined ? row.expiresAt : dto.expiresAt ? new Date(dto.expiresAt) : null, + }, + }); + return { ...updated, bucket: qualBucket(updated.expiresAt) }; + } + + async deleteQualification(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.qualification.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('资质不存在'); + await this.prisma.$transaction(async (tx) => { + await tx.credentialBorrow.updateMany({ where: { qualificationId: id }, data: { qualificationId: null } }); + await tx.qualification.delete({ where: { id } }); + }); + return { ok: true }; + } + + async listBorrows(user: AuthUser, bucket?: string) { + this.assertAccess(user); + const rows = await this.prisma.credentialBorrow.findMany({ + where: { tenantId: '1' }, + include: { + borrowerUser: { select: USER_SELECT }, + qualification: { select: QUAL_SELECT }, + bidCase: { select: BID_SELECT }, + }, + orderBy: { createdAt: 'desc' }, + }); + const items = rows.map((r) => ({ ...r, bucket: borrowBucket(r) })); + const filtered = bucket ? items.filter((i) => i.bucket === bucket) : items; + return { items: filtered, total: filtered.length }; + } + + async createBorrow( + user: AuthUser, + dto: { + itemName: string; + borrower?: string; + borrowerId?: string; + qualificationId?: string; + bidCaseId?: string; + dueAt?: string; + remark?: string; + }, + ) { + this.assertAccess(user); + if (!canApplySeal(user.roles, user.permissions)) throw new ForbiddenException('无权登记外借'); + if (!dto.itemName?.trim()) throw new BadRequestException('请填写证照或物品名称'); + let borrowerName = dto.borrower?.trim() || ''; + if (dto.borrowerId) { + const u = await this.prisma.user.findFirst({ where: { id: dto.borrowerId, tenantId: '1' } }); + if (!u) throw new NotFoundException('借用人不存在'); + borrowerName = u.displayName; + } + if (!borrowerName) throw new BadRequestException('请选择或填写借用人'); + if (dto.qualificationId) { + const q = await this.prisma.qualification.findFirst({ + where: { id: dto.qualificationId, tenantId: '1' }, + }); + if (!q) throw new NotFoundException('资质不存在'); + } + if (dto.bidCaseId) { + const bid = await this.prisma.bidCase.findFirst({ where: { id: dto.bidCaseId, tenantId: '1' } }); + if (!bid) throw new NotFoundException('投标事项不存在'); + } + const created = await this.prisma.$transaction(async (tx) => { + const row = await tx.credentialBorrow.create({ + data: { + tenantId: '1', + itemName: dto.itemName.trim(), + borrower: borrowerName, + borrowerId: dto.borrowerId || null, + qualificationId: dto.qualificationId || null, + bidCaseId: dto.bidCaseId || null, + dueAt: dto.dueAt ? new Date(dto.dueAt) : null, + remark: dto.remark, + status: BORROW_STATUS.BORROWED, + }, + }); + const assigneeId = dto.borrowerId || user.id; + await tx.todoTask.create({ + data: { + tenantId: '1', + title: `归还证照:${row.itemName}`, + bizType: SEAL_BIZ.CREDENTIAL, + bizId: row.id, + assigneeId, + dueAt: row.dueAt, + }, + }); + return row; + }); + return this.getBorrow(created.id); + } + + async returnBorrow(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.credentialBorrow.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('借用记录不存在'); + if (row.status === BORROW_STATUS.RETURNED) throw new BadRequestException('已经归还'); + await this.prisma.$transaction(async (tx) => { + await tx.credentialBorrow.update({ + where: { id }, + data: { status: BORROW_STATUS.RETURNED, returnedAt: new Date() }, + }); + await tx.todoTask.updateMany({ + where: { bizId: id, bizType: SEAL_BIZ.CREDENTIAL, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + }); + return this.getBorrow(id); + } + + async deleteBorrow(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.credentialBorrow.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('借用记录不存在'); + await this.prisma.$transaction(async (tx) => { + await tx.todoTask.updateMany({ + where: { bizId: id, bizType: SEAL_BIZ.CREDENTIAL, status: 'OPEN' }, + data: { status: 'CANCELLED' }, + }); + await tx.credentialBorrow.delete({ where: { id } }); + }); + return { ok: true }; + } + + async listSeals(user: AuthUser, bucket?: string, scope?: string) { + const canModule = canAccessSeal(user.roles, user.permissions); + if (!canModule && !canApplySeal(user.roles, user.permissions)) { + throw new ForbiddenException('无权访问用章申请'); + } + const where: Prisma.SealRequestWhereInput = { tenantId: '1' }; + if (scope === 'mine' || (!canModule && !this.canSeeAllCompanySeals(user))) { + where.applicantId = user.id; + } else if (!this.canSeeAllCompanySeals(user)) { + withAnd(where, (await ownerScope(this.prisma, user, 'applicantId')) as Record); + } + const rows = await this.prisma.sealRequest.findMany({ + where, + include: { + applicant: { select: USER_SELECT }, + decidedBy: { select: USER_SELECT }, + bidCase: { select: BID_SELECT }, + contract: { select: CONTRACT_SELECT }, + }, + orderBy: { createdAt: 'desc' }, + }); + const items = rows.map((r) => ({ + ...r, + bucket: sealBucket(r), + myActions: { + canReview: canApproveSeal(user.roles) && r.status === SEAL_STATUS.PENDING, + canReturn: (r.status === SEAL_STATUS.OUT || sealBucket(r) === 'overdue') && !r.returnedAt, + }, + })); + const filtered = bucket ? items.filter((i) => i.bucket === bucket) : items; + return { items: filtered, total: filtered.length }; + } + + async createSealRequest( + user: AuthUser, + dto: { + sealType: string; + reason: string; + bidCaseId?: string; + contractId?: string; + applicantId?: string; + approverId?: string; + takeOut?: boolean; + returnAt?: string; + }, + ) { + if (!canApplySeal(user.roles, user.permissions)) throw new ForbiddenException('无权申请用章'); + if (!dto.sealType?.trim() || !dto.reason?.trim()) { + throw new BadRequestException('请填写印章类型和事由'); + } + if (dto.takeOut && !dto.returnAt) { + throw new BadRequestException('外带请填写预计归还日期'); + } + let contract: { id: string; status: string; bidCaseId: string | null } | null = null; + if (dto.contractId) { + contract = await this.prisma.contract.findFirst({ + where: { id: dto.contractId, tenantId: '1' }, + select: { id: true, status: true, bidCaseId: true }, + }); + if (!contract) throw new NotFoundException('合同不存在'); + } + if (dto.bidCaseId) { + const bid = await this.prisma.bidCase.findFirst({ where: { id: dto.bidCaseId, tenantId: '1' } }); + if (!bid) throw new NotFoundException('投标事项不存在'); + } + // 申请人始终绑定当前登录账号,避免前端篡改代申请人造成数据和短信错配。 + const applicantId = user.id; + const skipApproval = + Boolean(contract) && + (contract!.status === CONTRACT_STATUS.DRAFT || contract!.status === CONTRACT_STATUS.REJECTED); + let approverId: string | undefined; + if (!skipApproval) { + if (!dto.approverId?.trim()) throw new BadRequestException('请选择审批人'); + const approver = await this.prisma.user.findFirst({ + where: { id: dto.approverId.trim(), tenantId: '1', status: 'ACTIVE' }, + include: { roles: { include: { role: true } } }, + }); + if (!approver || !canApproveSeal(approver.roles.map((r) => r.role.code))) { + throw new BadRequestException('所选账号不是有效的用章审批人'); + } + approverId = approver.id; + } + const created = await this.prisma.$transaction(async (tx) => { + const row = await tx.sealRequest.create({ + data: { + tenantId: '1', + sealType: dto.sealType.trim(), + reason: dto.reason.trim(), + bidCaseId: dto.bidCaseId || contract?.bidCaseId || null, + contractId: dto.contractId || null, + applicantId, + takeOut: Boolean(dto.takeOut), + returnAt: dto.returnAt ? new Date(dto.returnAt) : null, + status: SEAL_STATUS.PENDING, + }, + }); + if (!skipApproval) { + await this.openApprovals(tx, row.id, `${dto.sealType}:${dto.reason}`, approverId!); + } + return row; + }); + return this.getSeal(created.id, user); + } + + async deleteSealRequest(id: string, user: AuthUser) { + const canModule = canAccessSeal(user.roles, user.permissions); + if (!canModule && !canApplySeal(user.roles, user.permissions)) { + throw new ForbiddenException('无权删除用章记录'); + } + const row = await this.prisma.sealRequest.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('用章申请不存在'); + if (!canModule && row.applicantId !== user.id) { + throw new ForbiddenException('只能删除自己的用章申请'); + } + await this.prisma.$transaction(async (tx) => { + await tx.approvalTask.updateMany({ + where: { bizId: id, bizType: SEAL_BIZ.APPROVAL, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + await tx.todoTask.updateMany({ + where: { bizId: id, bizType: { in: [SEAL_BIZ.APPROVAL, SEAL_BIZ.RETURN] }, status: 'OPEN' }, + data: { status: 'CANCELLED' }, + }); + await tx.sealRequest.delete({ where: { id } }); + }); + return { ok: true }; + } + + async reviewSeal(id: string, user: AuthUser, dto: ReviewInput) { + if (!canApproveSeal(user.roles)) throw new ForbiddenException('无权审批用章'); + if (dto.result === 'REJECTED' && !dto.comment?.trim()) { + throw new BadRequestException('驳回请填写意见'); + } + await this.prisma.$transaction(async (tx) => { + const row = await tx.sealRequest.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('用章申请不存在'); + if (row.status !== SEAL_STATUS.PENDING) throw new BadRequestException('当前不是待审批状态'); + if (dto.result === 'REJECTED') { + await tx.sealRequest.update({ + where: { id }, + data: { + status: SEAL_STATUS.REJECTED, + decidedAt: new Date(), + decidedById: user.id, + rejectComment: dto.comment!.trim(), + }, + }); + await this.closeApprovals(tx, id, 'REJECTED', user.id); + return; + } + const next = sealStatusAfterApprove(row.takeOut); + await tx.sealRequest.update({ + where: { id }, + data: { status: next, decidedAt: new Date(), decidedById: user.id }, + }); + await this.closeApprovals(tx, id, 'APPROVED', user.id); + if (row.takeOut) { + await this.createReturnTodo(tx, row); + } + }); + return this.getSeal(id, user); + } + + async returnSeal(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.sealRequest.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('用章申请不存在'); + const bucket = sealBucket(row); + if (bucket !== 'out' && bucket !== 'overdue') { + throw new BadRequestException('当前不是外带未还状态'); + } + await this.prisma.$transaction(async (tx) => { + await tx.sealRequest.update({ + where: { id }, + data: { status: SEAL_STATUS.RETURNED, returnedAt: new Date() }, + }); + await tx.todoTask.updateMany({ + where: { bizId: id, bizType: SEAL_BIZ.RETURN, status: 'OPEN' }, + data: { status: 'DONE' }, + }); + }); + return this.getSeal(id, user); + } + + async listRegistry(user: AuthUser, legalEntityId?: string, sealType?: string) { + this.assertAccess(user); + const items = await this.prisma.sealItem.findMany({ + where: { + tenantId: '1', + ...(legalEntityId ? { legalEntityId } : {}), + ...(sealType ? { sealType } : {}), + }, + include: { legalEntity: { select: { id: true, name: true, shortName: true } } }, + orderBy: [{ legalEntity: { shortName: 'asc' } }, { sealType: 'asc' }, { name: 'asc' }], + }); + return { + items: items.map((i) => ({ + ...i, + statusLabel: i.status === 'OUT' ? '外带' : '在库', + })), + }; + } + + async createRegistryItem( + user: AuthUser, + dto: { name: string; keeper?: string; sealType?: string; legalEntityId?: string }, + ) { + this.assertAccess(user); + if (!dto.name?.trim()) throw new BadRequestException('请填写印章名称'); + const sealType = dto.sealType?.trim() || '公章'; + if (!SEAL_TYPES.includes(sealType as (typeof SEAL_TYPES)[number])) { + throw new BadRequestException('请选择正确的印章类型'); + } + if (dto.legalEntityId) { + const entity = await this.prisma.legalEntity.findFirst({ where: { id: dto.legalEntityId, tenantId: '1' } }); + if (!entity) throw new NotFoundException('参与主体不存在'); + } + return this.prisma.sealItem.create({ + data: { + tenantId: '1', + name: dto.name.trim(), + sealType, + keeper: dto.keeper?.trim() || '行政', + legalEntityId: dto.legalEntityId || null, + }, + }); + } + + async updateRegistryItem( + id: string, + user: AuthUser, + dto: { name?: string; keeper?: string; status?: string; sealType?: string; legalEntityId?: string | null }, + ) { + this.assertAccess(user); + const row = await this.prisma.sealItem.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('印章不存在'); + if (dto.sealType && !SEAL_TYPES.includes(dto.sealType as (typeof SEAL_TYPES)[number])) { + throw new BadRequestException('请选择正确的印章类型'); + } + if (dto.legalEntityId) { + const entity = await this.prisma.legalEntity.findFirst({ where: { id: dto.legalEntityId, tenantId: '1' } }); + if (!entity) throw new NotFoundException('参与主体不存在'); + } + return this.prisma.sealItem.update({ + where: { id }, + data: { + name: dto.name?.trim() || undefined, + keeper: dto.keeper?.trim() || undefined, + sealType: dto.sealType?.trim() || undefined, + legalEntityId: dto.legalEntityId === undefined ? undefined : dto.legalEntityId || null, + status: dto.status === 'OUT' || dto.status === 'IN_STOCK' ? dto.status : undefined, + }, + }); + } + + async deleteRegistryItem(id: string, user: AuthUser) { + this.assertAccess(user); + const row = await this.prisma.sealItem.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('印章不存在'); + await this.prisma.sealItem.delete({ where: { id } }); + return { ok: true }; + } + + async decideApproval(approvalId: string, user: AuthUser, dto: ReviewInput) { + const task = await this.prisma.approvalTask.findFirst({ where: { id: approvalId, tenantId: '1' } }); + if (!task) throw new NotFoundException('审批任务不存在'); + if (task.assigneeId !== user.id) throw new ForbiddenException('这不是你的审批'); + if (task.status !== 'PENDING') throw new BadRequestException('该审批已办理'); + if (task.bizType !== SEAL_BIZ.APPROVAL || !task.bizId) throw new BadRequestException('不是用章审批'); + return this.reviewSeal(task.bizId, user, dto); + } + + async applyContractApproved(tx: Tx, contractId: string) { + const seals = await tx.sealRequest.findMany({ + where: { contractId, status: SEAL_STATUS.PENDING }, + }); + for (const row of seals) { + const next = sealStatusAfterApprove(row.takeOut); + await tx.sealRequest.update({ + where: { id: row.id }, + data: { status: next, decidedAt: new Date() }, + }); + if (row.takeOut) await this.createReturnTodo(tx, row); + } + } + + async overviewExtras(user?: AuthUser) { + const canBid = !user || canAccessBidModule(user.roles, user.permissions); + const canSeal = !user || canAccessSeal(user.roles, user.permissions); + const [quals, borrows, seals, opens] = await Promise.all([ + canSeal ? this.prisma.qualification.findMany({ where: { tenantId: '1' } }) : Promise.resolve([]), + canSeal + ? this.prisma.credentialBorrow.findMany({ + where: { tenantId: '1', status: BORROW_STATUS.BORROWED }, + }) + : Promise.resolve([]), + canSeal + ? this.prisma.sealRequest.findMany({ + where: { tenantId: '1', status: { in: [SEAL_STATUS.OUT, SEAL_STATUS.APPROVED] } }, + }) + : Promise.resolve([]), + canBid + ? this.prisma.bidCase.findMany({ + where: { + tenantId: '1', + openAt: { not: null, gte: new Date(), lte: new Date(Date.now() + 14 * 86400000) }, + status: { notIn: ['TERMINATED', 'WON', 'LOST', 'FAILED'] }, + }, + select: { + id: true, + bidNo: true, + externalNo: true, + name: true, + openAt: true, + openPlace: true, + assignees: { + where: { role: 'BIDDER' }, + select: { user: { select: { displayName: true, employee: { select: { name: true } } } } }, + }, + }, + orderBy: { openAt: 'asc' }, + take: 20, + }) + : Promise.resolve([]), + ]); + const qualWarn = quals + .map((q) => ({ ...q, bucket: qualBucket(q.expiresAt) })) + .filter((q) => q.bucket === 'expired' || q.bucket === 'expiring'); + const borrowOverdue = borrows.filter((b) => borrowBucket(b) === 'overdue'); + const sealOverdue = seals.filter((s) => sealBucket(s) === 'overdue'); + return { + expiringQualifications: qualWarn.length, + overdueBorrows: borrowOverdue.length, + overdueSeals: sealOverdue.length, + upcomingOpens: opens.length, + warnings: { + qualifications: qualWarn.slice(0, 8), + borrows: borrowOverdue.slice(0, 8), + seals: sealOverdue.slice(0, 8), + opens: opens.map((o) => ({ + id: o.id, + bidNo: o.bidNo, + externalNo: o.externalNo, + name: o.name, + openAt: o.openAt, + openPlace: o.openPlace, + bidders: o.assignees + .map((a) => a.user.employee?.name || a.user.displayName) + .filter(Boolean) + .join('、'), + })), + }, + }; + } + + async calendar(user: AuthUser) { + const [bids, tasks, todos, borrows, seals] = await Promise.all([ + this.prisma.bidCase.findMany({ + where: { + tenantId: '1', + openAt: { not: null }, + status: { notIn: ['TERMINATED', 'WON', 'LOST', 'FAILED'] }, + assignees: { some: { userId: user.id } }, + }, + select: { id: true, name: true, bidNo: true, externalNo: true, openAt: true, openPlace: true }, + }), + this.prisma.projectTask.findMany({ + where: { assigneeId: user.id, dueAt: { not: null }, status: { not: 'DONE' } }, + include: { project: { select: { id: true, projectNo: true, name: true } } }, + }), + this.prisma.todoTask.findMany({ + where: { assigneeId: user.id, status: 'OPEN', dueAt: { not: null } }, + }), + canAccessSeal(user.roles, user.permissions) + ? this.prisma.credentialBorrow.findMany({ + where: { tenantId: '1', status: BORROW_STATUS.BORROWED, dueAt: { not: null } }, + }) + : Promise.resolve([]), + canAccessSeal(user.roles, user.permissions) + ? this.prisma.sealRequest.findMany({ + where: { + tenantId: '1', + status: { in: [SEAL_STATUS.OUT, SEAL_STATUS.APPROVED] }, + takeOut: true, + returnedAt: null, + returnAt: { not: null }, + }, + }) + : Promise.resolve([]), + ]); + const items = [ + ...bids.map((b) => ({ + id: `bid-${b.id}`, + kind: 'BID_OPEN', + title: `开标 ${b.name}`, + start: b.openAt, + place: b.openPlace, + ref: b.bidNo, + overdue: isOverdue(b.openAt), + path: `/bid/cases/${b.id}`, + })), + ...tasks.map((t) => ({ + id: `task-${t.id}`, + kind: 'TASK', + title: `任务截止 ${t.name}`, + start: t.dueAt, + place: t.project.projectNo, + ref: t.project.projectNo, + overdue: isOverdue(t.dueAt), + path: `/project/${t.project.id}`, + })), + ...todos.map((t) => ({ + id: `todo-${t.id}`, + kind: 'TODO', + title: t.title, + start: t.dueAt, + place: null as string | null, + ref: null as string | null, + overdue: isOverdue(t.dueAt), + path: null as string | null, + })), + ...borrows.map((b) => ({ + id: `borrow-${b.id}`, + kind: 'BORROW', + title: `归还证照 ${b.itemName}`, + start: b.dueAt, + place: b.borrower, + ref: b.itemName, + overdue: borrowBucket(b) === 'overdue', + path: '/seal/borrows', + })), + ...seals + .filter((s) => sealBucket(s) === 'out' || sealBucket(s) === 'overdue') + .map((s) => ({ + id: `seal-${s.id}`, + kind: 'SEAL', + title: `归还印章 ${s.sealType}`, + start: s.returnAt, + place: s.reason, + ref: s.sealType, + overdue: sealBucket(s) === 'overdue', + path: '/seal/requests', + })), + ]; + items.sort((a, b) => new Date(a.start || 0).getTime() - new Date(b.start || 0).getTime()); + return items; + } + + private async getBorrow(id: string) { + const row = await this.prisma.credentialBorrow.findFirst({ + where: { id }, + include: { + borrowerUser: { select: USER_SELECT }, + qualification: { select: QUAL_SELECT }, + bidCase: { select: BID_SELECT }, + }, + }); + if (!row) throw new NotFoundException('借用记录不存在'); + return { ...row, bucket: borrowBucket(row) }; + } + + async getRequest(id: string, user: AuthUser) { + return this.getSeal(id, user); + } + + private async getSeal(id: string, user: AuthUser) { + const row = await this.prisma.sealRequest.findFirst({ + where: { id }, + include: { + applicant: { select: USER_SELECT }, + decidedBy: { select: USER_SELECT }, + bidCase: { select: BID_SELECT }, + contract: { select: CONTRACT_SELECT }, + }, + }); + if (!row) throw new NotFoundException('用章申请不存在'); + const bucket = sealBucket(row); + return { + ...row, + bucket, + myActions: { + canReview: canApproveSeal(user.roles) && row.status === SEAL_STATUS.PENDING, + canReturn: (bucket === 'out' || bucket === 'overdue') && !row.returnedAt, + }, + }; + } + + private async openApprovals(tx: Tx, sealId: string, reason: string, approverId: string) { + const approver = await tx.user.findFirst({ + where: { id: approverId, tenantId: '1', status: 'ACTIVE' }, + include: { roles: { include: { role: true } } }, + }); + if (!approver || !canApproveSeal(approver.roles.map((r) => r.role.code))) { + throw new BadRequestException('没有可审批用章的负责人'); + } + const ids = [approver.id]; + await tx.approvalTask.createMany({ + data: ids.map((assigneeId) => ({ + tenantId: '1', + title: `用章审批:${reason}`, + bizType: SEAL_BIZ.APPROVAL, + bizId: sealId, + assigneeId, + })), + }); + emitSms('SEAL_REQUEST', { userIds: ids, params: { content: `用章待审批:${reason}` } }); + } + + /** 潘兴辉、戴永康是用章台账的全公司查看账号;其他账号继续遵循数据范围。 */ + private canSeeAllCompanySeals(user: AuthUser) { + const name = String(user.displayName || '').trim(); + const account = String(user.username || '').trim().toLowerCase(); + return user.roles.includes('admin') || name === '潘兴辉' || name === '戴永康' || account === 'panxinghui'; + } + + private async closeApprovals(tx: Tx, bizId: string, result: string, actorId: string) { + await tx.approvalTask.updateMany({ + where: { bizType: SEAL_BIZ.APPROVAL, bizId, assigneeId: actorId, status: 'PENDING' }, + data: { status: result }, + }); + await tx.approvalTask.updateMany({ + where: { bizType: SEAL_BIZ.APPROVAL, bizId, status: 'PENDING' }, + data: { status: 'CANCELLED' }, + }); + } + + private async createReturnTodo( + tx: Tx, + row: { id: string; sealType: string; applicantId: string | null; returnAt: Date | null }, + ) { + if (!row.applicantId) return; + await tx.todoTask.create({ + data: { + tenantId: '1', + title: `归还印章:${row.sealType}`, + bizType: SEAL_BIZ.RETURN, + bizId: row.id, + assigneeId: row.applicantId, + dueAt: row.returnAt, + }, + }); + } +} diff --git a/apps/api/src/sms/sms-bus.ts b/apps/api/src/sms/sms-bus.ts new file mode 100644 index 0000000..4507fa4 --- /dev/null +++ b/apps/api/src/sms/sms-bus.ts @@ -0,0 +1,64 @@ +import { emitIm } from '../im/im-bus'; + +type SmsPayload = { + userIds?: string[]; + phones?: string[]; + params?: Record; + actorId?: string; + bizType?: string; + bizId?: string; +}; + +type SmsHandler = (eventKey: string, payload: SmsPayload) => Promise; + +let handler: SmsHandler | null = null; + +const IM_KIND: Record = { + BID_TECH_INITIAL: 'approval', + BID_REVIEW: 'approval', + BID_TECH_FINAL: 'approval', + OFFICE_APPROVAL: 'approval', + HR_LEAVE: 'approval', + EXPENSE: 'approval', + SEAL_REQUEST: 'approval', + BID_FETCH: 'todo', + BID_ASSIGN_FETCH: 'todo', + BID_DRAFT: 'todo', + BID_ASSIGN_DRAFT: 'todo', + BID_PRINT: 'todo', + BID_EXECUTION: 'todo', + BID_FINAL_PASSED: 'todo', + BID_DRAFT_DONE: 'todo', + BID_DONE: 'todo', + BID_REJECT: 'todo', + BID_TERMINATED: 'todo', + WORK_REPORT: 'todo', + NOTICE: 'todo', +}; + +export function registerSmsHandler(fn: SmsHandler) { + handler = fn; +} + +/** 业务步骤触发短信。未配置或步骤关闭时静默跳过,不打断主流程。 */ +export function emitSms(eventKey: string, payload: SmsPayload) { + if (handler) { + void handler(eventKey, payload).catch(() => undefined); + } + const kind = IM_KIND[eventKey]; + if (!kind || !payload.userIds?.length) return; + const title = + payload.params?.content || + payload.params?.task_name || + payload.params?.project_name || + eventKey; + emitIm({ + userIds: payload.userIds, + kind, + title, + body: title, + fromUserId: payload.actorId, + bizType: payload.bizType || eventKey, + bizId: payload.bizId, + }); +} diff --git a/apps/api/src/sms/sms-events.ts b/apps/api/src/sms/sms-events.ts new file mode 100644 index 0000000..206e603 --- /dev/null +++ b/apps/api/src/sms/sms-events.ts @@ -0,0 +1,238 @@ +export type SmsEventGroup = '登录' | '投标' | '个人办公' | '信息发布'; + +export type SmsEventSpec = { + key: string; + group: SmsEventGroup; + name: string; + when: string; + to: string; + defaultTemplate: string; + defaultEnabled: boolean; + /** 业务侧会传入的字段;真正发出去的 TemplateParam 以阿里云模板变量为准。 */ + sample: Record; +}; + +/** 步骤 → 短信。在「短信服务 · 步骤关联」里开关和换模板。模板变量必须和阿里云审核通过的内容一致。 */ +export const SMS_EVENTS: SmsEventSpec[] = [ + { + key: 'LOGIN_CODE', + group: '登录', + name: '登录验证码', + when: '登录页点获取验证码', + to: '登录账号绑定的手机', + defaultTemplate: 'SMS_510935060', + defaultEnabled: true, + sample: { code: '123456' }, + }, + { + key: 'BID_TECH_INITIAL', + group: '投标', + name: '技术初审待办', + when: '新建项目筛选,发给初审会签人', + to: '初审人', + defaultTemplate: 'SMS_508595151', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目' }, + }, + { + key: 'BID_FETCH', + group: '投标', + name: '取标待办', + when: '初审通过或派取标人', + to: '取标人', + defaultTemplate: 'SMS_508380165', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目' }, + }, + { + key: 'BID_ASSIGN_FETCH', + group: '投标', + name: '派取标人待办', + when: '初审通过,通知商务总监指定取标人', + to: '商务总监或特殊筛选协调人', + defaultTemplate: 'SMS_508485178', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目' }, + }, + { + key: 'BID_TECH_FINAL', + group: '投标', + name: '技术终审待办', + when: '取标完成,进入终审', + to: '终审人', + defaultTemplate: 'SMS_508400144', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目' }, + }, + { + key: 'BID_DRAFT', + group: '投标', + name: '制标待办', + when: '终审通过或派制标人', + to: '制标人', + defaultTemplate: 'SMS_508520168', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目' }, + }, + { + key: 'BID_ASSIGN_DRAFT', + group: '投标', + name: '派制标人待办', + when: '技术终审通过,通知负责人指定制标人', + to: '商务总监或特殊筛选协调人', + defaultTemplate: 'SMS_508485178', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目' }, + }, + { + key: 'BID_REVIEW', + group: '投标', + name: '商务审待办', + when: '提交标书进入一审,或一审通过进入二审', + to: '商务审批人', + defaultTemplate: 'SMS_508485178', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目' }, + }, + { + key: 'BID_ASSIGN', + group: '投标', + name: '分配投标人', + when: '印封后派出投标人', + to: '投标人', + defaultTemplate: 'SMS_511275271', + defaultEnabled: true, + sample: { + name: '谢晓晨', + date: '2026-08-13 09:00', + location: '市公共资源交易中心', + project_name: '某某智慧校园', + contact_person: '张三', + contact_phone: '13800138000', + }, + }, + { + key: 'BID_DONE', + group: '投标', + name: '开标结果通知', + when: '开标登记结果', + to: '申请人', + defaultTemplate: 'SMS_512165104', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目' }, + }, + { + key: 'BID_REJECT', + group: '投标', + name: '投标驳回', + when: '初审/终审/商务审驳回', + to: '申请人', + defaultTemplate: 'SMS_508695169', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目', reason: '资料不全' }, + }, + { + key: 'BID_TERMINATED', + group: '投标', + name: '投标终止通知', + when: '项目筛选任一环节终止投标', + to: '潘兴辉', + defaultTemplate: 'SMS_512165104', + defaultEnabled: true, + sample: { name: '潘兴辉', content: '某某项目终止,终止原因:资料不全' }, + }, + { + key: 'BID_PRINT', + group: '投标', + name: '印封待办', + when: '商务二审通过,进入印封', + to: '印封办理人', + defaultTemplate: 'SMS_508485178', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目' }, + }, + { + key: 'BID_EXECUTION', + group: '投标', + name: '投标执行待办', + when: '印封完成,待分配投标人', + to: '投标执行办理人', + defaultTemplate: 'SMS_508485178', + defaultEnabled: true, + sample: { name: '张三', apply_no: 'BID-00001', project_name: '某某项目' }, + }, + { + key: 'OFFICE_APPROVAL', + group: '个人办公', + name: '待我审批', + when: '提交人事/用章/办公申请、报销', + to: '审批人', + defaultTemplate: 'SMS_512165104', + defaultEnabled: true, + sample: { name: '张三', content: '请明日上午9点参加项目例会' }, + }, + { + key: 'WORK_ASSIGN', + group: '个人办公', + name: '工作安排', + when: '负责人给下属派工作', + to: '执行人', + defaultTemplate: 'SMS_508875184', + defaultEnabled: true, + sample: { name: '李四', assigner: '王经理', task_name: '提交周报', task_code: 'WA0001' }, + }, + { + key: 'WORK_REPORT', + group: '个人办公', + name: '工作汇报', + when: '提交日报/周报/月报', + to: '汇报对象和抄送人', + defaultTemplate: 'SMS_512165104', + defaultEnabled: true, + sample: { name: '王经理', content: '张三提交了本周周报' }, + }, + { + key: 'HR_LEAVE', + group: '个人办公', + name: '人事申请', + when: '提交请假、出差、加班等', + to: '审批人', + defaultTemplate: 'SMS_512165104', + defaultEnabled: true, + sample: { content: '有一条人事申请待审批' }, + }, + { + key: 'SEAL_REQUEST', + group: '个人办公', + name: '用章申请', + when: '提交用章', + to: '审批人', + defaultTemplate: 'SMS_512165104', + defaultEnabled: true, + sample: { content: '有一条用章申请待审批' }, + }, + { + key: 'EXPENSE', + group: '个人办公', + name: '费用确认', + when: '提交报销或借款', + to: '财务确认人', + defaultTemplate: 'SMS_512165104', + defaultEnabled: true, + sample: { content: '有一条报销待确认' }, + }, + { + key: 'NOTICE', + group: '信息发布', + name: '公告/制度发布', + when: '发布通知公告或规章制度', + to: '全体在职有手机号的同事', + defaultTemplate: 'SMS_512165104', + defaultEnabled: false, + sample: { content: '新公告:春节放假安排' }, + }, +]; + +export function smsEventOf(key: string) { + return SMS_EVENTS.find((e) => e.key === key); +} diff --git a/apps/api/src/sms/sms-fill.spec.ts b/apps/api/src/sms/sms-fill.spec.ts new file mode 100644 index 0000000..f0b1cf5 --- /dev/null +++ b/apps/api/src/sms/sms-fill.spec.ts @@ -0,0 +1,37 @@ +import { clipByAttr, fillTemplateParams, parseTemplateVars } from './sms-fill'; + +describe('sms-fill', () => { + it('clips others to 35 chars', () => { + expect(clipByAttr('甲'.repeat(40), 'others')).toHaveLength(35); + }); + + it('fills 初审待办 vars from bid payload and recipient name', () => { + const vars = parseTemplateVars( + '{"name":"user_nick","apply_no":"other_number2","project_name":"others"}', + '尊敬的${name}您好,项目筛选待您初审,单号${apply_no},项目名称${project_name}', + ); + const out = fillTemplateParams( + vars, + { apply_no: 'BID-2026-00001', project_name: '某市展厅数字化升级' }, + '吴软件', + ); + expect(out).toEqual({ + name: '吴软件', + apply_no: 'BID-2026-00001', + project_name: '某市展厅数字化升级', + }); + }); + + it('maps content onto 办公工作通知 ${content}', () => { + const vars = parseTemplateVars('{"content":"others"}', '办公通知:${content}'); + const out = fillTemplateParams(vars, { content: '请明日上午9点参加项目例会' }, '张三'); + expect(out).toEqual({ content: '请明日上午9点参加项目例会' }); + }); + + it('maps content onto templates that only have ${others}', () => { + const vars = parseTemplateVars('{"name":"user_nick","others":"others"}', '${name}您好,${others}'); + const out = fillTemplateParams(vars, { content: '请尽快办理技术初审' }, '戴永康'); + expect(out.name).toBe('戴永康'); + expect(out.others).toBe('请尽快办理技术初审'); + }); +}); diff --git a/apps/api/src/sms/sms-fill.ts b/apps/api/src/sms/sms-fill.ts new file mode 100644 index 0000000..ae3f89e --- /dev/null +++ b/apps/api/src/sms/sms-fill.ts @@ -0,0 +1,89 @@ +/** 按阿里云 TemplateRule / variableAttribute 截断,避免变量超长被拒。 */ +export const ATTR_LIMIT: Record = { + numberCaptcha: 6, + user_nick: 20, + name: 12, + others: 35, + other_number2: 35, + time: 24, + address: 50, + phone: 11, +}; + +const FALLBACK: Record = { + content: ['content', 'project_name', 'task_name', 'others'], + project_name: ['project_name', 'content', 'task_name', 'others'], + others: ['others', 'content', 'project_name', 'task_name'], + other_number2: ['other_number2', 'content', 'others'], + apply_no: ['apply_no', 'task_code'], + task_name: ['task_name', 'content', 'project_name'], + task_code: ['task_code', 'apply_no'], + reason: ['reason'], + assigner: ['assigner'], + date: ['date'], + location: ['location'], + contact_person: ['contact_person'], + contact_phone: ['contact_phone', 'phone'], + code: ['code'], + name: ['name'], +}; + +export function clipByAttr(value: string, attr: string) { + const s = String(value || '') + .replace(/\s+/g, ' ') + .trim(); + const n = ATTR_LIMIT[attr] || 35; + return s.length <= n ? s : s.slice(0, n); +} + +export function parseTemplateVars(variableAttribute?: string, templateContent?: string) { + const vars: Record = {}; + if (variableAttribute?.trim()) { + try { + const parsed = JSON.parse(variableAttribute) as Record; + for (const [k, v] of Object.entries(parsed)) { + if (k && v) vars[k] = v; + } + } catch { + /* 忽略坏 JSON,下面用正文里的 ${var} 兜底 */ + } + } + for (const m of String(templateContent || '').matchAll(/\$\{([a-zA-Z0-9_]+)\}/g)) { + if (!vars[m[1]]) vars[m[1]] = m[1] === 'code' ? 'numberCaptcha' : 'others'; + } + return vars; +} + +/** 只填模板里声明的变量;${name} 用收件人姓名。 */ +export function fillTemplateParams( + vars: Record, + payload: Record = {}, + recipientName?: string, +) { + if (!Object.keys(vars).length) { + const out = { ...payload }; + if (recipientName && !out.name) out.name = clipByAttr(recipientName, 'user_nick'); + return out; + } + const out: Record = {}; + for (const [key, attr] of Object.entries(vars)) { + let v = ''; + if (key === 'name') { + v = payload.name || recipientName || '同事'; + } else { + for (const src of FALLBACK[key] || [key]) { + if (payload[src]?.trim()) { + v = payload[src]; + break; + } + } + } + if (!v) v = attr === 'numberCaptcha' ? payload.code || '' : '无'; + out[key] = clipByAttr(v, attr) || (attr === 'numberCaptcha' ? v : '无'); + } + return out; +} + +export function extractVarNames(templateContent: string) { + return [...new Set([...String(templateContent || '').matchAll(/\$\{([a-zA-Z0-9_]+)\}/g)].map((m) => m[1]))]; +} diff --git a/apps/api/src/sms/sms.controller.ts b/apps/api/src/sms/sms.controller.ts new file mode 100644 index 0000000..5c3a0ba --- /dev/null +++ b/apps/api/src/sms/sms.controller.ts @@ -0,0 +1,168 @@ +import { Body, Controller, Get, Post, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { Public } from '../common/decorators/auth.decorators'; +import { SystemService } from '../system/system.service'; +import { SmsService } from './sms.service'; + +@ApiTags('sms') +@ApiBearerAuth() +@Controller('sms') +export class SmsController { + constructor( + private readonly sms: SmsService, + private readonly system: SystemService, + ) {} + + @Get('login-options') + @Public() + loginOptions() { + return this.sms.loginOptions(); + } + + @Get('events') + events(@CurrentUser() user: AuthUser) { + this.system.assertManage(user); + return this.sms.listEvents(); + } + + @Post('events') + saveEvent( + @CurrentUser() user: AuthUser, + @Body() body: { eventKey: string; enabled?: boolean; templateCode?: string; signName?: string }, + ) { + this.system.assertManage(user); + return this.sms.saveEvent(body); + } + + @Get('config') + config(@CurrentUser() user: AuthUser) { + this.system.assertManage(user); + return this.sms.config(); + } + + @Get('office-options') + officeOptions(@CurrentUser() user: AuthUser) { + return this.sms.officeOptions(user); + } + + @Get('signs') + signs(@CurrentUser() user: AuthUser, @Query('pageIndex') pageIndex?: string, @Query('pageSize') pageSize?: string) { + this.system.assertManage(user); + return this.sms.querySigns(Number(pageIndex) || 1, Number(pageSize) || 50); + } + + @Get('signs/detail') + signDetail(@CurrentUser() user: AuthUser, @Query('signName') signName: string) { + this.system.assertManage(user); + return this.sms.getSign(signName); + } + + @Post('signs') + createSign( + @CurrentUser() user: AuthUser, + @Body() + body: { + signName: string; + signSource: number; + qualificationId?: string; + remark?: string; + signType?: number; + }, + ) { + this.system.assertManage(user); + return this.sms.createSign(body); + } + + @Get('templates') + templates(@CurrentUser() user: AuthUser, @Query('pageIndex') pageIndex?: string, @Query('pageSize') pageSize?: string) { + this.system.assertManage(user); + return this.sms.queryTemplates(Number(pageIndex) || 1, Number(pageSize) || 50); + } + + @Get('templates/detail') + templateDetail(@CurrentUser() user: AuthUser, @Query('templateCode') templateCode: string) { + this.system.assertManage(user); + return this.sms.getTemplate(templateCode); + } + + @Post('templates') + createTemplate( + @CurrentUser() user: AuthUser, + @Body() + body: { + templateName: string; + templateContent: string; + templateType: number; + relatedSignName?: string; + remark?: string; + templateRule?: string | Record; + applySceneContent?: string; + }, + ) { + this.system.assertManage(user); + return this.sms.createTemplate(body); + } + + @Post('templates/update') + updateTemplate( + @CurrentUser() user: AuthUser, + @Body() + body: { + templateCode: string; + templateName: string; + templateContent: string; + templateType: number; + relatedSignName?: string; + remark?: string; + templateRule?: string | Record; + applySceneContent?: string; + }, + ) { + this.system.assertManage(user); + return this.sms.updateTemplate(body); + } + + @Post('send') + send( + @CurrentUser() user: AuthUser, + @Body() + body: { + phoneNumbers?: string; + extraPhones?: string; + departmentIds?: string[]; + userIds?: string[]; + signName?: string; + templateCode?: string; + templateParam?: string | Record; + content?: string; + }, + ) { + return this.sms.send(user, body); + } + + @Post('send-batch') + sendBatch( + @CurrentUser() user: AuthUser, + @Body() + body: { + templateCode: string; + items: { phone: string; signName?: string; templateParam?: Record }[]; + }, + ) { + return this.sms.sendBatch(user, body); + } + + @Get('qualifications') + qualifications(@CurrentUser() user: AuthUser) { + this.system.assertManage(user); + return this.sms.qualifications(); + } + + @Get('logs') + logs(@CurrentUser() user: AuthUser) { + this.system.assertManage(user); + return this.sms.logs(); + } +} diff --git a/apps/api/src/sms/sms.module.ts b/apps/api/src/sms/sms.module.ts new file mode 100644 index 0000000..b0fb0db --- /dev/null +++ b/apps/api/src/sms/sms.module.ts @@ -0,0 +1,12 @@ +import { Global, Module } from '@nestjs/common'; +import { SmsController } from './sms.controller'; +import { SmsService } from './sms.service'; +import { SystemService } from '../system/system.service'; + +@Global() +@Module({ + controllers: [SmsController], + providers: [SmsService, SystemService], + exports: [SmsService], +}) +export class SmsModule {} diff --git a/apps/api/src/sms/sms.service.ts b/apps/api/src/sms/sms.service.ts new file mode 100644 index 0000000..b72c555 --- /dev/null +++ b/apps/api/src/sms/sms.service.ts @@ -0,0 +1,806 @@ +import { BadRequestException, ForbiddenException, Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import Dysmsapi20170525, * as $Dysmsapi from '@alicloud/dysmsapi20170525'; +import { $OpenApiUtil } from '@alicloud/openapi-core'; +import * as $dara from '@darabonba/typescript'; +import { createHash, randomInt } from 'crypto'; +import { PrismaService } from '../prisma/prisma.service'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { canSendSms } from '../common/role-groups'; +import { SystemService } from '../system/system.service'; +import { registerSmsHandler } from './sms-bus'; +import { SMS_EVENTS, smsEventOf } from './sms-events'; +import { fillTemplateParams, parseTemplateVars, ATTR_LIMIT } from './sms-fill'; + +const PRESET_TEMPLATES = [ + { code: 'SMS_512165104', name: '办公工作通知' }, + { code: 'SMS_510935060', name: '登录验证码' }, + { code: 'SMS_511100061', name: '重置密码' }, + { code: 'SMS_511275271', name: '日程通知' }, + { code: 'SMS_508595151', name: '初审待办' }, + { code: 'SMS_508380165', name: '取标待办' }, + { code: 'SMS_508400144', name: '终审待办' }, + { code: 'SMS_508520168', name: '制标待办' }, + { code: 'SMS_508485178', name: '环节催办' }, + { code: 'SMS_508735170', name: '终审通过通知' }, + { code: 'SMS_508625169', name: '制标完成通知' }, + { code: 'SMS_508695169', name: '驳回通知' }, + { code: 'SMS_508875184', name: '任务通知' }, +]; + +const DEAD_TEMPLATE = 'SMS_508545096'; +const OFFICE_TEMPLATE = 'SMS_512165104'; +// Only login and bidding workflow notices are system-triggered. All office +// notices (announcements, reports, reimbursements, HR, seals, assignments) +// must be deliberately sent from the manual "发送工作通知短信" page. +const AUTOMATED_EVENT_KEYS = new Set([ + 'LOGIN_CODE', + 'BID_TECH_INITIAL', 'BID_FETCH', 'BID_ASSIGN_FETCH', 'BID_TECH_FINAL', + 'BID_DRAFT', 'BID_ASSIGN_DRAFT', + 'BID_REVIEW', 'BID_ASSIGN', 'BID_DONE', 'BID_REJECT', 'BID_TERMINATED', + 'BID_PRINT', 'BID_EXECUTION', +]); +const TEMPLATE_ALIAS: Record = { + SMS_508545096: OFFICE_TEMPLATE, + SMS_512185104: OFFICE_TEMPLATE, +}; + +function normalizeTemplate(code?: string) { + const raw = (code || OFFICE_TEMPLATE).trim(); + return TEMPLATE_ALIAS[raw] || raw || OFFICE_TEMPLATE; +} + +type AliyunErr = { data?: { Message?: string; Code?: string }; message?: string }; + +function maskAccessKey(id: string) { + if (id.length <= 10) return '********'; + return `${id.slice(0, 6)}********${id.slice(-4)}`; +} + +@Injectable() +export class SmsService implements OnModuleInit { + private readonly log = new Logger(SmsService.name); + private cached?: Dysmsapi20170525; + private cachedKey?: string; + private tplCache = new Map; content: string }>(); + + constructor( + private readonly prisma: PrismaService, + private readonly system: SystemService, + ) {} + + async onModuleInit() { + registerSmsHandler((eventKey, payload) => this.notifyEvent(eventKey, payload)); + try { + await this.prisma.smsEventBind.updateMany({ + where: { templateCode: { in: Object.keys(TEMPLATE_ALIAS) } }, + data: { templateCode: OFFICE_TEMPLATE }, + }); + await this.prisma.smsEventBind.updateMany({ + where: { eventKey: { notIn: [...AUTOMATED_EVENT_KEYS] } }, + data: { enabled: false }, + }); + } catch (e) { + this.log.warn(`sms bind migrate: ${(e as Error).message}`); + } + } + + configured() { + return Boolean(process.env.ALIYUN_SMS_ACCESS_KEY_ID && process.env.ALIYUN_SMS_ACCESS_KEY_SECRET); + } + + config() { + const accessKeyId = process.env.ALIYUN_SMS_ACCESS_KEY_ID || ''; + const configured = Boolean(accessKeyId && process.env.ALIYUN_SMS_ACCESS_KEY_SECRET); + return { + configured, + region: process.env.ALIYUN_SMS_REGION || 'cn-hangzhou', + endpoint: process.env.ALIYUN_SMS_ENDPOINT || 'dysmsapi.aliyuncs.com', + defaultSign: process.env.ALIYUN_SMS_DEFAULT_SIGN || '江苏风影随行科技', + ramLogin: process.env.ALIYUN_RAM_LOGIN || '', + accessKeyIdMasked: accessKeyId ? maskAccessKey(accessKeyId) : '', + secretConfigured: Boolean(process.env.ALIYUN_SMS_ACCESS_KEY_SECRET), + securityPhone: process.env.ALIYUN_RAM_SECURITY_PHONE || '', + securityEmail: process.env.ALIYUN_RAM_SECURITY_EMAIL || '', + presets: PRESET_TEMPLATES, + note: configured + ? '已接入阿里云短信(Dysmsapi)。申请签名须先有审核通过的资质;发送须用已通过的签名和模板。控制台登录密码不写入本系统,发信用 AccessKey。' + : '未配置 AccessKey。把 ALIYUN_SMS_ACCESS_KEY_ID / SECRET 写进环境变量后即可调用。', + }; + } + + private client() { + const accessKeyId = process.env.ALIYUN_SMS_ACCESS_KEY_ID; + const accessKeySecret = process.env.ALIYUN_SMS_ACCESS_KEY_SECRET; + if (!accessKeyId || !accessKeySecret) { + throw new BadRequestException('未配置阿里云短信 AccessKey,无法调用'); + } + const cacheKey = `${accessKeyId}:${accessKeySecret}`; + if (!this.cached || this.cachedKey !== cacheKey) { + const cfg = new $OpenApiUtil.Config({ accessKeyId, accessKeySecret }); + cfg.endpoint = process.env.ALIYUN_SMS_ENDPOINT || 'dysmsapi.aliyuncs.com'; + this.cached = new Dysmsapi20170525(cfg); + this.cachedKey = cacheKey; + } + return this.cached; + } + + private wrap(e: unknown): never { + const err = e as AliyunErr; + const msg = err.data?.Message || err.message || '阿里云短信调用失败'; + const code = err.data?.Code; + this.log.warn(`aliyun sms: ${code || ''} ${msg}`); + throw new BadRequestException(code ? `${code}: ${msg}` : msg); + } + + private body(resp: { body?: T } | undefined): T { + if (!resp?.body) throw new BadRequestException('阿里云未返回结果'); + return resp.body; + } + + async querySigns(pageIndex = 1, pageSize = 50) { + try { + const req = new $Dysmsapi.QuerySmsSignListRequest({ pageIndex, pageSize }); + const resp = await this.client().querySmsSignListWithOptions(req, new $dara.RuntimeOptions({})); + return this.body(resp); + } catch (e) { + this.wrap(e); + } + } + + async getSign(signName: string) { + if (!signName?.trim()) throw new BadRequestException('请填写签名名称'); + try { + const req = new $Dysmsapi.GetSmsSignRequest({ signName: signName.trim() }); + const resp = await this.client().getSmsSignWithOptions(req, new $dara.RuntimeOptions({})); + return this.body(resp); + } catch (e) { + this.wrap(e); + } + } + + async createSign(dto: { + signName: string; + signSource: number; + qualificationId?: string; + remark?: string; + signType?: number; + }) { + if (!dto.signName?.trim()) throw new BadRequestException('请填写签名名称'); + if (dto.signSource === undefined || dto.signSource === null) { + throw new BadRequestException('请选择签名来源'); + } + try { + const req = new $Dysmsapi.CreateSmsSignRequest({ + signName: dto.signName.trim(), + signSource: Number(dto.signSource), + qualificationId: dto.qualificationId ? Number(dto.qualificationId) : undefined, + remark: dto.remark?.trim() || undefined, + signType: dto.signType === undefined ? 1 : Number(dto.signType), + }); + const resp = await this.client().createSmsSignWithOptions(req, new $dara.RuntimeOptions({})); + return this.body(resp); + } catch (e) { + this.wrap(e); + } + } + + async queryTemplates(pageIndex = 1, pageSize = 50) { + try { + const req = new $Dysmsapi.QuerySmsTemplateListRequest({ pageIndex, pageSize }); + const resp = await this.client().querySmsTemplateListWithOptions(req, new $dara.RuntimeOptions({})); + return this.body(resp); + } catch (e) { + this.wrap(e); + } + } + + async getTemplate(templateCode: string) { + if (!templateCode?.trim()) throw new BadRequestException('请填写模板 Code'); + try { + const req = new $Dysmsapi.GetSmsTemplateRequest({ templateCode: templateCode.trim() }); + const resp = await this.client().getSmsTemplateWithOptions(req, new $dara.RuntimeOptions({})); + return this.body(resp); + } catch (e) { + this.wrap(e); + } + } + + async createTemplate(dto: { + templateName: string; + templateContent: string; + templateType: number; + relatedSignName?: string; + remark?: string; + templateRule?: string | Record; + applySceneContent?: string; + }) { + if (!dto.templateName?.trim() || !dto.templateContent?.trim()) { + throw new BadRequestException('请填写模板名称和内容'); + } + if (dto.templateType === undefined || dto.templateType === null) { + throw new BadRequestException('请选择模板类型'); + } + const content = dto.templateContent.trim(); + const vars = [...content.matchAll(/\$\{([a-zA-Z0-9_]+)\}/g)].map((m) => m[1]); + let templateRule = + typeof dto.templateRule === 'string' + ? dto.templateRule.trim() + : dto.templateRule + ? JSON.stringify(dto.templateRule) + : ''; + if (vars.length && !templateRule) { + throw new BadRequestException('模板含变量时必须填写 TemplateRule(变量属性),例如 {"content":"others"}'); + } + if (templateRule) { + try { + JSON.parse(templateRule); + } catch { + throw new BadRequestException('TemplateRule 必须是 JSON,例如 {"content":"others"}'); + } + } + try { + const req = new $Dysmsapi.CreateSmsTemplateRequest({ + templateName: dto.templateName.trim(), + templateContent: content, + templateType: Number(dto.templateType), + relatedSignName: dto.relatedSignName?.trim() || this.config().defaultSign, + remark: dto.remark?.trim() || undefined, + templateRule: templateRule || undefined, + applySceneContent: dto.applySceneContent?.trim() || 'https://oa.fysxkj.com', + }); + const resp = await this.client().createSmsTemplateWithOptions(req, new $dara.RuntimeOptions({})); + return this.body(resp); + } catch (e) { + this.wrap(e); + } + } + + async updateTemplate(dto: { + templateCode: string; + templateName: string; + templateContent: string; + templateType: number; + relatedSignName?: string; + remark?: string; + templateRule?: string | Record; + applySceneContent?: string; + }) { + if (!dto.templateCode?.trim()) throw new BadRequestException('请填写模板 Code'); + if (!dto.templateName?.trim() || !dto.templateContent?.trim()) { + throw new BadRequestException('请填写模板名称和内容'); + } + const templateRule = + typeof dto.templateRule === 'string' + ? dto.templateRule.trim() + : dto.templateRule + ? JSON.stringify(dto.templateRule) + : undefined; + try { + const req = new $Dysmsapi.UpdateSmsTemplateRequest({ + templateCode: dto.templateCode.trim(), + templateName: dto.templateName.trim(), + templateContent: dto.templateContent.trim(), + templateType: Number(dto.templateType), + relatedSignName: dto.relatedSignName?.trim() || this.config().defaultSign, + remark: dto.remark?.trim() || undefined, + templateRule, + applySceneContent: dto.applySceneContent?.trim() || 'https://oa.fysxkj.com', + }); + const resp = await this.client().updateSmsTemplateWithOptions(req, new $dara.RuntimeOptions({})); + this.tplCache.delete(dto.templateCode.trim()); + return this.body(resp); + } catch (e) { + this.wrap(e); + } + } + + async officeOptions(user: AuthUser) { + if (!canSendSms(user.roles, user.permissions)) throw new ForbiddenException('无权发送短信'); + const cfg = this.config(); + let templateContent = '办公通知:${content}'; + let contentLimit = ATTR_LIMIT.others || 35; + try { + const meta = await this.templateMeta(OFFICE_TEMPLATE); + if (meta.content) templateContent = meta.content; + const limits = Object.entries(meta.vars) + .filter(([key, attr]) => key !== 'name' && attr !== 'user_nick') + .map(([, attr]) => ATTR_LIMIT[attr] || 35); + if (limits.length) contentLimit = Math.min(...limits); + } catch { + /* 通道未配时仍给预览文案 */ + } + const previewHint = `【${cfg.defaultSign}】${templateContent + .replace(/\$\{name\}/g, '同事') + .replace(/\$\{content\}/g, '……') + .replace(/\$\{others\}/g, '……') + .replace(/\$\{project_name\}/g, '……') + .replace(/\$\{[^}]+\}/g, '……')}`; + return { + configured: cfg.configured, + defaultSign: cfg.defaultSign, + presets: PRESET_TEMPLATES.filter((p) => p.code !== DEAD_TEMPLATE), + officeTemplate: OFFICE_TEMPLATE, + templateContent, + contentLimit, + previewHint, + note: cfg.configured + ? '一次只发一种:部门群发、点名同事,或系统外手机号。对方收到的是运营商模板,你只填中间那句通知。' + : cfg.note, + }; + } + + async send( + user: AuthUser, + dto: { + phoneNumbers?: string; + extraPhones?: string; + departmentIds?: string[]; + userIds?: string[]; + signName?: string; + templateCode?: string; + templateParam?: string | Record; + content?: string; + }, + ) { + if (!canSendSms(user.roles, user.permissions)) throw new ForbiddenException('无权发送短信'); + const people = await this.collectRecipients(dto); + if (!people.length) throw new BadRequestException('请选择部门、人员,或填写手机号'); + const templateCode = normalizeTemplate(dto.templateCode || OFFICE_TEMPLATE); + const signName = dto.signName?.trim() || this.config().defaultSign; + const rawParam = + typeof dto.templateParam === 'string' + ? (() => { + try { + return JSON.parse(dto.templateParam) as Record; + } catch { + return {} as Record; + } + })() + : dto.templateParam || {}; + const payload: Record = { ...rawParam }; + if (dto.content?.trim()) { + payload.content = dto.content.trim(); + if (!payload.others) payload.others = dto.content.trim(); + if (!payload.project_name) payload.project_name = dto.content.trim(); + } + const meta = await this.templateMeta(templateCode); + const items = people.map((p) => ({ + phone: p.phone, + signName, + templateParam: fillTemplateParams(meta.vars, payload, p.name), + })); + try { + const body = (await this.dispatchSms(templateCode, items)) as { + bizId?: string; + code?: string; + Code?: string; + message?: string; + Message?: string; + }; + const code = body.code || body.Code || 'OK'; + const message = body.message || body.Message || ''; + await this.prisma.smsSendLog.create({ + data: { + phones: items.map((i) => i.phone).join(','), + signName, + templateCode, + templateParam: JSON.stringify(items.map((i) => i.templateParam)), + eventKey: 'MANUAL', + bizId: body.bizId, + code, + message, + createdById: user.id, + }, + }); + if (code !== 'OK') { + throw new BadRequestException(`${code}: ${message || '请检查模板内容和模板参数是否匹配'}`); + } + return { ...body, sent: items.length }; + } catch (e) { + if (e instanceof BadRequestException) throw e; + this.wrap(e); + } + } + + async sendBatch( + user: AuthUser, + dto: { + templateCode: string; + items: { phone: string; signName?: string; templateParam?: Record }[]; + }, + ) { + if (!canSendSms(user.roles, user.permissions)) throw new ForbiddenException('无权发送短信'); + const items = (dto.items || []).filter((i) => i.phone?.trim()); + if (!items.length) throw new BadRequestException('请至少填写一个手机号'); + const defaultSign = this.config().defaultSign; + try { + const req = new $Dysmsapi.SendBatchSmsRequest({ + phoneNumberJson: JSON.stringify(items.map((i) => i.phone.trim())), + signNameJson: JSON.stringify(items.map((i) => i.signName?.trim() || defaultSign)), + templateCode: dto.templateCode.trim(), + templateParamJson: JSON.stringify(items.map((i) => i.templateParam || {})), + }); + const resp = await this.client().sendBatchSmsWithOptions(req, new $dara.RuntimeOptions({})); + const body = this.body(resp); + await this.prisma.smsSendLog.create({ + data: { + phones: items.map((i) => i.phone).join(','), + signName: items.map((i) => i.signName || defaultSign).join(','), + templateCode: dto.templateCode.trim(), + templateParam: JSON.stringify(items.map((i) => i.templateParam || {})), + bizId: (body as { bizId?: string }).bizId, + code: (body as { code?: string }).code || 'OK', + message: (body as { message?: string }).message, + createdById: user.id, + }, + }); + return body; + } catch (e) { + this.wrap(e); + } + } + + async qualifications() { + try { + const req = new $Dysmsapi.QuerySmsQualificationRecordRequest({ pageNo: 1, pageSize: 50, state: 'PASS' }); + const resp = await this.client().querySmsQualificationRecordWithOptions(req, new $dara.RuntimeOptions({})); + return this.body(resp); + } catch (e) { + this.wrap(e); + } + } + + async logs() { + return this.prisma.smsSendLog.findMany({ + orderBy: { createdAt: 'desc' }, + take: 100, + }); + } + + async loginOptions() { + const bind = await this.prisma.smsEventBind.findUnique({ where: { eventKey: 'LOGIN_CODE' } }); + const enabled = bind?.enabled ?? true; + return { smsLogin: this.configured() && enabled }; + } + + async listEvents() { + const binds = await this.prisma.smsEventBind.findMany(); + const map = new Map(binds.map((b) => [b.eventKey, b])); + return { + configured: this.configured(), + defaultSign: this.config().defaultSign, + presets: PRESET_TEMPLATES, + items: SMS_EVENTS.map((spec) => { + const bind = map.get(spec.key); + return { + ...spec, + automatic: AUTOMATED_EVENT_KEYS.has(spec.key), + enabled: AUTOMATED_EVENT_KEYS.has(spec.key) && (bind?.enabled ?? spec.defaultEnabled), + templateCode: bind?.templateCode || spec.defaultTemplate, + signName: bind?.signName || this.config().defaultSign, + vars: Object.keys(spec.sample).join('、'), + }; + }), + }; + } + + async saveEvent(dto: { eventKey: string; enabled?: boolean; templateCode?: string; signName?: string }) { + const spec = smsEventOf(dto.eventKey); + if (!spec) throw new BadRequestException('未知短信步骤'); + const automatic = AUTOMATED_EVENT_KEYS.has(spec.key); + return this.prisma.smsEventBind.upsert({ + where: { eventKey: spec.key }, + create: { + eventKey: spec.key, + enabled: automatic && (dto.enabled ?? spec.defaultEnabled), + templateCode: normalizeTemplate(dto.templateCode?.trim() || spec.defaultTemplate), + signName: dto.signName?.trim() || null, + }, + update: { + enabled: automatic ? dto.enabled : false, + templateCode: dto.templateCode === undefined ? undefined : normalizeTemplate(dto.templateCode.trim()), + signName: dto.signName === undefined ? undefined : dto.signName.trim() || null, + }, + }); + } + + async notifyEvent( + eventKey: string, + payload: { userIds?: string[]; phones?: string[]; params?: Record; actorId?: string }, + ) { + if (!this.configured() || !AUTOMATED_EVENT_KEYS.has(eventKey)) return; + const spec = smsEventOf(eventKey); + if (!spec) return; + const bind = await this.prisma.smsEventBind.findUnique({ where: { eventKey } }); + const enabled = bind?.enabled ?? spec.defaultEnabled; + if (!enabled) return; + const templateCode = normalizeTemplate(bind?.templateCode || spec.defaultTemplate); + const signName = bind?.signName?.trim() || this.config().defaultSign; + let people = await this.recipientsOf(payload.userIds, payload.phones); + if (eventKey === 'NOTICE' && !people.length) { + people = await this.allStaffRecipients(); + } + if (!people.length) return; + const meta = await this.templateMeta(templateCode); + const items = people.map((p) => ({ + phone: p.phone, + signName, + templateParam: fillTemplateParams(meta.vars, payload.params || spec.sample, p.name), + })); + try { + const body = await this.dispatchSms(templateCode, items); + await this.prisma.smsSendLog.create({ + data: { + phones: items.map((i) => i.phone).join(','), + signName, + templateCode, + templateParam: JSON.stringify(items.map((i) => i.templateParam)), + eventKey, + bizId: (body as { bizId?: string }).bizId, + code: (body as { code?: string }).code || 'OK', + message: (body as { message?: string }).message, + createdById: payload.actorId || null, + }, + }); + } catch (e) { + const err = e as { data?: { Message?: string }; message?: string }; + this.log.warn(`sms ${eventKey}: ${err.data?.Message || err.message}`); + await this.prisma.smsSendLog.create({ + data: { + phones: items.map((i) => i.phone).join(','), + signName, + templateCode, + templateParam: JSON.stringify(items.map((i) => i.templateParam)), + eventKey, + code: 'FAIL', + message: err.data?.Message || err.message || '发送失败', + createdById: payload.actorId || null, + }, + }); + if (eventKey === 'LOGIN_CODE') { + throw new BadRequestException(err.data?.Message || err.message || '验证码发送失败'); + } + } + } + + async sendLoginCode(account: string) { + const raw = account.trim(); + if (!raw) throw new BadRequestException('请先填写手机号'); + if (!this.configured()) return { ok: true }; + const opt = await this.loginOptions(); + if (!opt.smsLogin) throw new BadRequestException('未开启登录短信验证码'); + const phone = await this.resolveLoginPhone(raw); + if (!phone) throw new BadRequestException('该账号未绑定手机,无法发送验证码'); + const recent = await this.prisma.smsOtp.findFirst({ + where: { phone, purpose: 'LOGIN', createdAt: { gt: new Date(Date.now() - 60_000) } }, + orderBy: { createdAt: 'desc' }, + }); + if (recent) throw new BadRequestException('验证码已发送,请稍后再试'); + const code = String(randomInt(100000, 999999)); + await this.prisma.smsOtp.create({ + data: { + phone, + codeHash: createHash('sha256').update(code).digest('hex'), + purpose: 'LOGIN', + expiresAt: new Date(Date.now() + 5 * 60_000), + }, + }); + await this.notifyEvent('LOGIN_CODE', { phones: [phone], params: { code } }); + return { ok: true }; + } + + async assertLoginCode(account: string, code?: string) { + const options = await this.loginOptions(); + if (!options.smsLogin) return; + const phone = await this.resolveLoginPhone(account); + if (!phone) throw new BadRequestException('该账号未绑定手机,无法短信登录'); + if (!code?.trim()) throw new BadRequestException('请填写短信验证码'); + const hash = createHash('sha256').update(code.trim()).digest('hex'); + const row = await this.prisma.smsOtp.findFirst({ + where: { phone, purpose: 'LOGIN', usedAt: null, expiresAt: { gt: new Date() } }, + orderBy: { createdAt: 'desc' }, + }); + if (!row || row.codeHash !== hash) throw new BadRequestException('验证码不正确或已过期'); + await this.prisma.smsOtp.update({ where: { id: row.id }, data: { usedAt: new Date() } }); + } + + private async resolveLoginPhone(account: string) { + const raw = account.trim(); + if (!raw) return null; + const user = await this.prisma.user.findFirst({ + where: { tenantId: '1', OR: [{ username: raw }, { employee: { is: { mobile: raw } } }] }, + include: { employee: { select: { mobile: true } } }, + }); + if (!user || user.status !== 'ACTIVE') return null; + const mobile = user.employee?.mobile?.trim() || (/^1\d{10}$/.test(user.username) ? user.username : ''); + return /^1\d{10}$/.test(mobile) ? mobile : null; + } + + private async templateMeta(templateCode: string) { + const hit = this.tplCache.get(templateCode); + if (hit && Date.now() - hit.at < 10 * 60_000) return hit; + try { + const body = (await this.getTemplate(templateCode)) as { + variableAttribute?: string; + VariableAttribute?: string; + templateContent?: string; + TemplateContent?: string; + templateRule?: string; + TemplateRule?: string; + }; + const templateContent = body.templateContent || body.TemplateContent || ''; + const variableAttribute = + body.variableAttribute || body.VariableAttribute || body.templateRule || body.TemplateRule; + const vars = parseTemplateVars(variableAttribute, templateContent); + const row = { at: Date.now(), vars, content: templateContent }; + this.tplCache.set(templateCode, row); + return row; + } catch (e) { + const err = e as { message?: string }; + this.log.warn(`sms template ${templateCode}: ${err.message}`); + const spec = SMS_EVENTS.find((s) => s.defaultTemplate === templateCode); + const vars = spec ? parseTemplateVars(undefined, Object.keys(spec.sample).map((k) => `\${${k}}`).join('')) : {}; + const row = { at: Date.now(), vars, content: '' }; + this.tplCache.set(templateCode, row); + return row; + } + } + + private async dispatchSms( + templateCode: string, + items: { phone: string; signName: string; templateParam: Record }[], + ) { + const same = + items.length === 1 || + items.every((i) => JSON.stringify(i.templateParam) === JSON.stringify(items[0].templateParam) && i.signName === items[0].signName); + if (same) { + const req = new $Dysmsapi.SendSmsRequest({ + phoneNumbers: items.map((i) => i.phone).join(','), + signName: items[0].signName, + templateCode, + templateParam: JSON.stringify(items[0].templateParam), + }); + const resp = await this.client().sendSmsWithOptions(req, new $dara.RuntimeOptions({})); + return this.body(resp); + } + let last: unknown; + for (let i = 0; i < items.length; i += 100) { + const chunk = items.slice(i, i + 100); + const req = new $Dysmsapi.SendBatchSmsRequest({ + phoneNumberJson: JSON.stringify(chunk.map((c) => c.phone)), + signNameJson: JSON.stringify(chunk.map((c) => c.signName)), + templateCode, + templateParamJson: JSON.stringify(chunk.map((c) => c.templateParam)), + }); + const resp = await this.client().sendBatchSmsWithOptions(req, new $dara.RuntimeOptions({})); + last = this.body(resp); + } + return last; + } + + private phoneOf(user: { username: string; employee?: { mobile?: string | null } | null }) { + const m = user.employee?.mobile?.trim() || ''; + if (/^1\d{10}$/.test(m)) return m; + return /^1\d{10}$/.test(user.username) ? user.username : ''; + } + + private parsePhones(raw?: string) { + return (raw || '') + .split(/[,,||;\s]+/) + .map((s) => s.replace(/\D/g, '')) + .filter((s) => /^1\d{10}$/.test(s)); + } + + private async expandDeptIds(ids: string[]) { + if (!ids.length) return []; + const all = await this.prisma.department.findMany({ + where: { tenantId: '1' }, + select: { id: true, parentId: true }, + }); + const children = new Map(); + for (const d of all) { + if (!d.parentId) continue; + const list = children.get(d.parentId) || []; + list.push(d.id); + children.set(d.parentId, list); + } + const out = new Set(ids); + const walk = (id: string) => { + for (const c of children.get(id) || []) { + if (!out.has(c)) { + out.add(c); + walk(c); + } + } + }; + ids.forEach(walk); + return [...out]; + } + + private async collectPhones(dto: { + phoneNumbers?: string; + extraPhones?: string; + departmentIds?: string[]; + userIds?: string[]; + }) { + return (await this.collectRecipients(dto)).map((p) => p.phone); + } + + private async collectRecipients(dto: { + phoneNumbers?: string; + extraPhones?: string; + departmentIds?: string[]; + userIds?: string[]; + }) { + const rows: { phone: string; name: string }[] = []; + const seen = new Set(); + const add = (phone: string, name?: string) => { + if (!/^1\d{10}$/.test(phone) || seen.has(phone)) return; + seen.add(phone); + rows.push({ phone, name: name?.trim() || '同事' }); + }; + for (const p of this.parsePhones(dto.phoneNumbers)) add(p); + for (const p of this.parsePhones(dto.extraPhones)) add(p); + const userIds = [...(dto.userIds || [])]; + if (dto.departmentIds?.length) { + const deptIds = await this.expandDeptIds(dto.departmentIds); + const users = await this.prisma.user.findMany({ + where: { tenantId: '1', status: 'ACTIVE', employee: { departmentId: { in: deptIds } } }, + include: { employee: { select: { mobile: true, name: true, employmentStatus: true } } }, + }); + for (const u of users) { + if (u.employee?.employmentStatus === 'LEFT') continue; + add(this.phoneOf(u), u.displayName || u.employee?.name); + } + } + if (userIds.length) { + const users = await this.prisma.user.findMany({ + where: { id: { in: userIds } }, + include: { employee: { select: { mobile: true, name: true } } }, + }); + for (const u of users) add(this.phoneOf(u), u.displayName || u.employee?.name); + } + return rows; + } + + private async recipientsOf(userIds?: string[], phones?: string[]) { + const rows: { phone: string; name: string }[] = []; + const seen = new Set(); + if (userIds?.length) { + const users = await this.prisma.user.findMany({ + where: { id: { in: userIds } }, + include: { employee: { select: { mobile: true, name: true } } }, + }); + for (const u of users) { + const phone = this.phoneOf(u); + if (!phone || seen.has(phone)) continue; + seen.add(phone); + rows.push({ phone, name: u.displayName || u.employee?.name || '同事' }); + } + } + for (const raw of phones || []) { + const phone = raw.replace(/\s/g, ''); + if (!/^1\d{10}$/.test(phone) || seen.has(phone)) continue; + seen.add(phone); + rows.push({ phone, name: '同事' }); + } + return rows; + } + + private async allStaffRecipients() { + const users = await this.prisma.user.findMany({ + where: { tenantId: '1', status: 'ACTIVE' }, + include: { employee: { select: { mobile: true, name: true, employmentStatus: true } } }, + }); + const rows: { phone: string; name: string }[] = []; + const seen = new Set(); + for (const u of users) { + if (u.employee?.employmentStatus === 'LEFT') continue; + const phone = this.phoneOf(u); + if (!phone || seen.has(phone)) continue; + seen.add(phone); + rows.push({ phone, name: u.displayName || u.employee?.name || '同事' }); + } + return rows; + } +} diff --git a/apps/api/src/system/numbering.ts b/apps/api/src/system/numbering.ts new file mode 100644 index 0000000..1d8de5d --- /dev/null +++ b/apps/api/src/system/numbering.ts @@ -0,0 +1,31 @@ +import { Prisma } from '@prisma/client'; + +type Db = { + numberRule: { + findFirst: (args: { where: { tenantId: string; code: string } }) => Promise<{ prefix: string; padLength: number } | null>; + }; +}; + +export async function nextBizNo( + db: Db, + code: 'BID' | 'HT' | 'PRJ' | 'PAY', + countStartingWith: (start: string) => Promise, +) { + const rule = await db.numberRule.findFirst({ where: { tenantId: '1', code } }); + const prefix = rule?.prefix || code; + const pad = rule?.padLength || 5; + const year = new Date().getFullYear(); + const start = `${prefix}-${year}-`; + const count = await countStartingWith(start); + return `${start}${String(count + 1).padStart(pad, '0')}`; +} + +export function exampleNo(prefix: string, padLength = 5, year = new Date().getFullYear()) { + return `${prefix}-${year}-${String(1).padStart(padLength, '0')}`; +} + +export function patternOf(prefix: string, padLength = 5) { + return `${prefix}-YYYY-${'#'.repeat(padLength)}`; +} + +export type Tx = Prisma.TransactionClient; diff --git a/apps/api/src/system/op-log.ts b/apps/api/src/system/op-log.ts new file mode 100644 index 0000000..485f00b --- /dev/null +++ b/apps/api/src/system/op-log.ts @@ -0,0 +1,41 @@ +import type { PrismaService } from '../prisma/prisma.service'; + +export const LOG_ACTION = { + LOGIN: 'LOGIN', + LOGIN_FAIL: 'LOGIN_FAIL', + APPROVAL: 'APPROVAL', + FORBIDDEN: 'FORBIDDEN', + ACL: 'ACL', +} as const; + +export async function writeOpLog( + prisma: PrismaService, + entry: { + userId?: string | null; + username?: string | null; + action: string; + path: string; + detail?: string | null; + success?: boolean; + requestId?: string | null; + ip?: string | null; + }, +) { + try { + await prisma.operationLog.create({ + data: { + tenantId: '1', + userId: entry.userId || null, + username: entry.username || null, + action: entry.action, + path: entry.path.slice(0, 240), + detail: entry.detail?.slice(0, 500) || null, + success: entry.success !== false, + requestId: entry.requestId || null, + ip: entry.ip || null, + }, + }); + } catch { + /* logging must not break the request */ + } +} diff --git a/apps/api/src/system/ops-workflow.spec.ts b/apps/api/src/system/ops-workflow.spec.ts new file mode 100644 index 0000000..4dfbc1f --- /dev/null +++ b/apps/api/src/system/ops-workflow.spec.ts @@ -0,0 +1,59 @@ +import { + canAccessReport, + canApprovePurchase, + canChangeProject, + canFilePurchase, + canManageSystem, + canPay, + FUNNEL_STAGES, + periodRange, + resourceLoad, +} from './ops-workflow'; +import { reportFlowKind, canWriteWorkReport } from '../office/work-report'; + +describe('ops-workflow', () => { + it('lets directors and finance see reports, only owner/admin manage ACL', () => { + expect(canAccessReport(['tech_director'], [])).toBe(true); + expect(canAccessReport(['employee'], ['office'])).toBe(false); + expect(canAccessReport(['hr'], ['hr'])).toBe(false); + expect(canManageSystem(['admin'])).toBe(true); + expect(canManageSystem(['hr'])).toBe(false); + expect(canPay(['finance'])).toBe(true); + expect(canFilePurchase(['tech_director'])).toBe(true); + expect(canApprovePurchase(['tech_director'])).toBe(false); + expect(canChangeProject(['biz_director'])).toBe(true); + }); + + it('builds the screening-to-award funnel and resource load buckets', () => { + expect(FUNNEL_STAGES.map((s) => s.id)).toEqual([ + 'screening', + 'drafting', + 'execution', + 'won', + 'lost', + 'terminated', + ]); + expect(resourceLoad(0, 0)).toBe('idle'); + expect(resourceLoad(1, 20)).toBe('full'); + expect(resourceLoad(4, 10)).toBe('over'); + const day = periodRange('DAY', new Date('2026-08-29')); + expect(day.period).toBe('2026-08-29'); + }); + + it('routes staff reports to PM with director CC, and PM reports to director', () => { + expect(reportFlowKind({ authorRoles: ['employee'], hasSubordinates: false, title: '工程师' })).toBe( + 'STAFF_TO_PM', + ); + expect(reportFlowKind({ authorRoles: ['employee'], hasSubordinates: true, title: '项目经理' })).toBe( + 'PM_TO_DIRECTOR', + ); + expect(reportFlowKind({ authorRoles: ['tech_director'], hasSubordinates: true, title: '软件总监' })).toBe( + 'DIRECTOR_TO_OWNER', + ); + expect(canWriteWorkReport(['rd_director'])).toBe(false); + expect(canWriteWorkReport(['biz_director'])).toBe(false); + expect(canWriteWorkReport(['owner'])).toBe(false); + expect(canWriteWorkReport(['employee'])).toBe(true); + expect(canWriteWorkReport(['pm'])).toBe(true); + }); +}); diff --git a/apps/api/src/system/ops-workflow.ts b/apps/api/src/system/ops-workflow.ts new file mode 100644 index 0000000..039ca46 --- /dev/null +++ b/apps/api/src/system/ops-workflow.ts @@ -0,0 +1,109 @@ +import { BID_STATUS, STATUS_BUCKET } from '../bidding/bid-workflow'; +import { isTechLead } from '../common/role-groups'; + +export const DATA_SCOPE = { + ALL: 'ALL', + DEPT: 'DEPT', + SELF: 'SELF', +} as const; + +export const PAY_KIND = { + EXPENSE: 'EXPENSE', + BOND: 'BOND', + PURCHASE: 'PURCHASE', +} as const; + +export const PAY_STATUS = { + TODO: 'TODO', + PAID: 'PAID', +} as const; + +export const PURCHASE_STATUS = { + DRAFT: 'DRAFT', + PENDING: 'PENDING', + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', +} as const; + +export const CHANGE_STATUS = { + PENDING: 'PENDING', + APPROVED: 'APPROVED', + REJECTED: 'REJECTED', +} as const; + +export const ASSET_STATUS = { + IN_USE: 'IN_USE', + RETURNED: 'RETURNED', +} as const; + +export const SEAL_STOCK = { + IN_STOCK: 'IN_STOCK', + OUT: 'OUT', +} as const; + +export const FUNNEL_STAGES = [ + { id: 'screening', label: '筛选', statuses: STATUS_BUCKET.screening }, + { id: 'drafting', label: '制标', statuses: STATUS_BUCKET.drafting }, + { id: 'execution', label: '投标执行', statuses: STATUS_BUCKET.execution }, + { id: 'won', label: '中标', statuses: [BID_STATUS.WON] }, + { id: 'lost', label: '未中 / 流废', statuses: [BID_STATUS.LOST, BID_STATUS.FAILED] }, + { id: 'terminated', label: '终止', statuses: [BID_STATUS.TERMINATED] }, +] as const; + +export function canAccessReport(roles: string[], permissions: string[]) { + if (['admin', 'owner', 'finance', 'biz_director'].some((r) => roles.includes(r)) || isTechLead(roles)) { + return true; + } + return permissions.some((p) => p === 'report' || p.startsWith('report:')); +} + +export function canManageSystem(roles: string[]) { + return ['admin', 'owner'].some((r) => roles.includes(r)); +} + +export function canPay(roles: string[]) { + return ['admin', 'owner', 'finance'].some((r) => roles.includes(r)); +} + +export function canFilePurchase(roles: string[]) { + return ['admin', 'owner', 'biz_director'].some((r) => roles.includes(r)) || isTechLead(roles); +} + +export function canApprovePurchase(roles: string[]) { + return ['admin', 'owner', 'finance'].some((r) => roles.includes(r)); +} + +export function canChangeProject(roles: string[]) { + return ['admin', 'owner', 'biz_director'].some((r) => roles.includes(r)) || isTechLead(roles); +} + +export function resourceLoad(openTasks: number, monthHours: number) { + if (openTasks >= 3 || monthHours >= 80) return 'over'; + if (openTasks >= 1 || monthHours >= 20) return 'full'; + return 'idle'; +} + +function ymd(d: Date) { + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; +} + +export function periodRange(periodType: string, date = new Date()) { + const d = new Date(date.getFullYear(), date.getMonth(), date.getDate()); + if (periodType === 'WEEK') { + const day = d.getDay() || 7; + const start = new Date(d); + start.setDate(d.getDate() - day + 1); + const end = new Date(start); + end.setDate(start.getDate() + 7); + return { start, end, period: `${ymd(start)} 周` }; + } + if (periodType === 'MONTH') { + const start = new Date(d.getFullYear(), d.getMonth(), 1); + const end = new Date(d.getFullYear(), d.getMonth() + 1, 1); + return { start, end, period: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}` }; + } + const start = d; + const end = new Date(d); + end.setDate(d.getDate() + 1); + return { start, end, period: ymd(start) }; +} diff --git a/apps/api/src/system/system.controller.ts b/apps/api/src/system/system.controller.ts new file mode 100644 index 0000000..3516051 --- /dev/null +++ b/apps/api/src/system/system.controller.ts @@ -0,0 +1,242 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { PrismaService } from '../prisma/prisma.service'; +import { CurrentUser } from '../common/decorators/current-user.decorator'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { Public } from '../common/decorators/auth.decorators'; +import { PageQueryDto } from '../common/dto/page-query.dto'; +import { SystemService } from './system.service'; + +@ApiTags('system') +@ApiBearerAuth() +@Controller('system') +export class SystemController { + constructor( + private readonly prisma: PrismaService, + private readonly system: SystemService, + ) {} + + @Public() + @Get('app-release') + appRelease() { + return this.system.getAppRelease(); + } + + @Patch('app-release') + updateAppRelease( + @CurrentUser() user: AuthUser, + @Body() + body: { version?: string; build?: number; url?: string; changelog?: string; force?: boolean; windowsUrl?: string }, + ) { + return this.system.updateAppRelease(user, body); + } + + @Get('menus') + async menus(@CurrentUser() user: AuthUser) { + const all = await this.prisma.permission.findMany({ + where: { permType: 'MENU', visible: true }, + orderBy: { sortNo: 'asc' }, + }); + const allowed = user.roles.includes('admin') + ? all + : all.filter((p) => user.permissions.includes(p.code)); + const visible = includeAncestors(all, allowed); + return buildTree(visible); + } + + @Get('dicts') + dicts(@Query('type') type?: string) { + return this.prisma.dictItem.findMany({ + where: { tenantId: '1', ...(type ? { dictType: type } : {}) }, + orderBy: [{ dictType: 'asc' }, { sortNo: 'asc' }], + }); + } + + @Post('dicts') + createDict( + @CurrentUser() user: AuthUser, + @Body() body: { dictType: string; label: string; value: string; sortNo?: number }, + ) { + return this.system.createDict(user, body); + } + + @Patch('dicts/:id') + updateDict( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { dictType?: string; label?: string; value?: string; sortNo?: number }, + ) { + return this.system.updateDict(id, user, body); + } + + @Delete('dicts/:id') + deleteDict(@CurrentUser() user: AuthUser, @Param('id') id: string) { + return this.system.deleteDict(id, user); + } + + @Get('logs') + logs(@CurrentUser() user: AuthUser, @Query() q: PageQueryDto, @Query('action') action?: string) { + this.system.assertManage(user); + return this.system.listLogs(q.page, q.pageSize, action); + } + + @Get('number-rules') + numberRules() { + return this.system.listNumberRules(); + } + + @Patch('number-rules/:id') + updateRule( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { prefix?: string; padLength?: number; name?: string }, + ) { + return this.system.updateNumberRule(id, user, body); + } + + @Get('users') + users(@CurrentUser() user: AuthUser) { + this.system.assertManage(user); + return this.system.listUsers(); + } + + @Post('users') + createUser( + @CurrentUser() user: AuthUser, + @Body() body: { username: string; password: string; displayName: string; roleIds: string[]; employeeId?: string }, + ) { + return this.system.createUser(user, body); + } + + @Post('users/batch') + createUsersBatch( + @CurrentUser() user: AuthUser, + @Body() + body: { + items?: { username: string; displayName?: string; password: string; roleIds?: string[]; roleCodes?: string[] }[]; + text?: string; + defaultPassword?: string; + defaultRoleCodes?: string[]; + }, + ) { + const items = + body.items?.length + ? body.items + : parseUserLines(body.text || '', body.defaultPassword, body.defaultRoleCodes); + return this.system.createUsersBatch(user, { items }); + } + + @Patch('users/:id') + updateUser( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { displayName?: string; status?: string; roleIds?: string[]; password?: string; employeeId?: string | null }, + ) { + return this.system.updateUser(id, user, body); + } + + @Get('roles') + roles(@CurrentUser() user: AuthUser) { + this.system.assertManage(user); + return this.system.listRoles(); + } + + @Patch('roles/:id') + updateRole( + @CurrentUser() user: AuthUser, + @Param('id') id: string, + @Body() body: { name?: string; description?: string; dataScope?: string; permissionCodes?: string[] }, + ) { + return this.system.updateRole(id, user, body); + } + + @Get('permissions') + permissions(@CurrentUser() user: AuthUser) { + this.system.assertManage(user); + return this.system.listPermissions(); + } + + @Get('message-channel') + async messageChannel() { + const port = Number(process.env.IM_TCP_PORT || 8901); + let online = 0; + try { + const httpPort = Number(process.env.IM_HTTP_PORT || 8902); + const res = await fetch(`http://127.0.0.1:${httpPort}/health`); + if (res.ok) { + const json = (await res.json()) as { online?: number }; + online = Number(json.online || 0); + } + } catch { + /* IM 未启动 */ + } + return [ + { + id: 'channel', + inboxEnabled: '已开启', + mobileImSdk: '已启用', + online, + note: `MobileIMSDK TCP ${port}:同事私聊和待办/审批推送已接通${online ? `,当前在线 ${online} 人` : ''}。`, + }, + ]; + } +} + +function parseUserLines(text: string, defaultPassword?: string, defaultRoleCodes?: string[]) { + return text + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + .map((line) => { + const parts = line.split(/[,,\t]+/).map((s) => s.trim()); + const [username, displayName, password, roleCodes] = parts; + return { + username, + displayName: displayName || username, + password: password || defaultPassword || '', + roleCodes: roleCodes + ? roleCodes.split(/[|/;]+/).map((s) => s.trim()).filter(Boolean) + : defaultRoleCodes || [], + }; + }); +} + +function includeAncestors(all: T[], allowed: T[]) { + const byId = new Map(all.map((p) => [p.id, p])); + const visible = new Map(allowed.map((p) => [p.id, p])); + for (const item of allowed) { + let parentId = item.parentId; + while (parentId && !visible.has(parentId)) { + const parent = byId.get(parentId); + if (!parent) break; + visible.set(parent.id, parent); + parentId = parent.parentId; + } + } + return all.filter((p) => visible.has(p.id)); +} + +function buildTree( + items: { + id: string; + parentId: string | null; + name: string; + code: string; + path: string | null; + icon: string | null; + sortNo: number; + }[], +) { + const map = new Map>(); + items.forEach((i) => map.set(i.id, { ...i, children: [] as Record[] })); + const roots: Record[] = []; + items.forEach((i) => { + const node = map.get(i.id)!; + if (i.parentId && map.has(i.parentId)) { + (map.get(i.parentId)!.children as Record[]).push(node); + } else { + roots.push(node); + } + }); + return roots; +} diff --git a/apps/api/src/system/system.service.ts b/apps/api/src/system/system.service.ts new file mode 100644 index 0000000..b3df55a --- /dev/null +++ b/apps/api/src/system/system.service.ts @@ -0,0 +1,366 @@ +import { + BadRequestException, + ForbiddenException, + Injectable, + NotFoundException, +} from '@nestjs/common'; +import * as argon2 from 'argon2'; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { PrismaService } from '../prisma/prisma.service'; +import type { AuthUser } from '../common/decorators/current-user.decorator'; +import { pageMeta } from '../common/dto/page-query.dto'; +import { DATA_SCOPE, canManageSystem } from './ops-workflow'; +import { exampleNo, patternOf } from './numbering'; +import { LOG_ACTION, writeOpLog } from './op-log'; + +@Injectable() +export class SystemService { + constructor(private readonly prisma: PrismaService) {} + + assertManage(user: AuthUser) { + if (!canManageSystem(user.roles)) throw new ForbiddenException('无权维护系统设置'); + } + + async listUsers() { + const rows = await this.prisma.user.findMany({ + where: { tenantId: '1' }, + include: { + employee: { include: { department: true } }, + roles: { include: { role: true } }, + }, + orderBy: { username: 'asc' }, + }); + return { + items: rows.map((u) => ({ + id: u.id, + username: u.username, + displayName: u.displayName, + status: u.status, + employeeId: u.employeeId, + employeeName: u.employee?.name, + departmentName: u.employee?.department?.name, + title: u.employee?.title || null, + roles: u.roles.map((r) => ({ id: r.role.id, code: r.role.code, name: r.role.name, dataScope: r.role.dataScope })), + })), + }; + } + + async updateUser( + id: string, + user: AuthUser, + dto: { displayName?: string; status?: string; roleIds?: string[]; password?: string; employeeId?: string | null }, + ) { + this.assertManage(user); + const row = await this.prisma.user.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('账号不存在'); + if (row.username === 'admin' && dto.status && dto.status !== 'ACTIVE') { + throw new BadRequestException('不能停用内置管理员'); + } + if (dto.roleIds) { + const roles = await this.prisma.role.findMany({ where: { id: { in: dto.roleIds } } }); + if (roles.length !== dto.roleIds.length) throw new BadRequestException('角色无效'); + await this.prisma.userRole.deleteMany({ where: { userId: id } }); + await this.prisma.userRole.createMany({ + data: dto.roleIds.map((roleId) => ({ userId: id, roleId })), + }); + } + const updated = await this.prisma.user.update({ + where: { id }, + data: { + displayName: dto.displayName?.trim() || undefined, + status: dto.status === 'DISABLED' ? 'DISABLED' : dto.status === 'ACTIVE' ? 'ACTIVE' : undefined, + employeeId: dto.employeeId === undefined ? undefined : dto.employeeId, + passwordHash: dto.password ? await argon2.hash(dto.password) : undefined, + }, + }); + await writeOpLog(this.prisma, { + userId: user.id, + username: user.username, + action: LOG_ACTION.ACL, + path: `/system/users/${id}`, + detail: `维护账号 ${updated.username}`, + }); + return { id: updated.id, username: updated.username, status: updated.status }; + } + + async createUser( + user: AuthUser, + dto: { username: string; password: string; displayName: string; roleIds: string[]; employeeId?: string }, + ) { + this.assertManage(user); + const phone = dto.username?.trim(); + if (!phone || !dto.password || dto.password.length < 6) { + throw new BadRequestException('请填写登录手机号和不少于 6 位的密码'); + } + if (!/^1\d{10}$/.test(phone) && phone !== 'admin') { + throw new BadRequestException('登录账号请填 11 位手机号'); + } + const exists = await this.prisma.user.findFirst({ where: { tenantId: '1', username: phone } }); + if (exists) throw new BadRequestException('该手机号已开通账号'); + const created = await this.prisma.user.create({ + data: { + username: phone, + passwordHash: await argon2.hash(dto.password), + displayName: dto.displayName?.trim() || phone, + employeeId: dto.employeeId || null, + roles: dto.roleIds?.length + ? { create: dto.roleIds.map((roleId) => ({ roleId })) } + : undefined, + }, + }); + await writeOpLog(this.prisma, { + userId: user.id, + username: user.username, + action: LOG_ACTION.ACL, + path: '/system/users', + detail: `新建账号 ${created.username}`, + }); + return { id: created.id, username: created.username, displayName: created.displayName, status: created.status }; + } + + async createUsersBatch( + user: AuthUser, + dto: { + items: { username: string; displayName?: string; password: string; roleIds?: string[]; roleCodes?: string[] }[]; + }, + ) { + this.assertManage(user); + const items = dto.items || []; + if (!items.length) throw new BadRequestException('请至少填写一行账号'); + if (items.length > 200) throw new BadRequestException('一次最多 200 个账号'); + const roles = await this.prisma.role.findMany(); + const byCode = new Map(roles.map((r) => [r.code, r])); + const byId = new Map(roles.map((r) => [r.id, r])); + const created: { username: string; displayName: string }[] = []; + const failed: { username: string; reason: string }[] = []; + for (const row of items) { + const username = row.username?.trim(); + try { + if (!username || !row.password || row.password.length < 6) { + throw new Error('用户名或密码不足 6 位'); + } + const roleIds = [ + ...(row.roleIds || []).filter((id) => byId.has(id)), + ...(row.roleCodes || []) + .map((c) => byCode.get(c.trim())?.id) + .filter((id): id is string => Boolean(id)), + ]; + const unique = [...new Set(roleIds)]; + const one = await this.createUser(user, { + username, + password: row.password, + displayName: row.displayName || username, + roleIds: unique, + }); + created.push({ username: one.username, displayName: one.displayName }); + } catch (e) { + failed.push({ username: username || '', reason: e instanceof Error ? e.message : '创建失败' }); + } + } + return { created, failed, createdCount: created.length, failedCount: failed.length }; + } + + async listRoles() { + const rows = await this.prisma.role.findMany({ + include: { permissions: { include: { permission: true } }, users: true }, + orderBy: { code: 'asc' }, + }); + return { + items: rows.map((r) => ({ + id: r.id, + name: r.name, + code: r.code, + description: r.description, + dataScope: r.dataScope, + userCount: r.users.length, + permissionCodes: r.permissions.map((p) => p.permission.code), + aclCustom: r.aclCustom, + })), + }; + } + + async updateRole( + id: string, + user: AuthUser, + dto: { name?: string; description?: string; dataScope?: string; permissionCodes?: string[] }, + ) { + this.assertManage(user); + const row = await this.prisma.role.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('角色不存在'); + if (row.code === 'admin' && dto.permissionCodes) { + throw new BadRequestException('内置管理员角色请保持全部权限'); + } + if (dto.dataScope && !Object.values(DATA_SCOPE).includes(dto.dataScope as (typeof DATA_SCOPE)[keyof typeof DATA_SCOPE])) { + throw new BadRequestException('数据范围须是 ALL / DEPT / SELF'); + } + if (dto.permissionCodes) { + const perms = await this.prisma.permission.findMany({ + where: { code: { in: dto.permissionCodes }, permType: { in: ['MENU', 'ACTION'] } }, + }); + await this.prisma.rolePermission.deleteMany({ where: { roleId: id } }); + await this.prisma.rolePermission.createMany({ + data: perms.map((p) => ({ roleId: id, permissionId: p.id })), + }); + } + const updated = await this.prisma.role.update({ + where: { id }, + data: { + name: dto.name?.trim() || undefined, + description: dto.description, + dataScope: dto.dataScope, + aclCustom: dto.permissionCodes ? true : undefined, + }, + }); + await writeOpLog(this.prisma, { + userId: user.id, + username: user.username, + action: LOG_ACTION.ACL, + path: `/system/roles/${id}`, + detail: `维护角色 ${updated.code} 数据范围 ${updated.dataScope}`, + }); + return updated; + } + + async listPermissions() { + return this.prisma.permission.findMany({ + where: { permType: { in: ['MENU', 'ACTION'] }, visible: true }, + orderBy: { sortNo: 'asc' }, + }); + } + + async listNumberRules() { + const rows = await this.prisma.numberRule.findMany({ orderBy: { code: 'asc' } }); + return rows.map((r) => ({ + ...r, + pattern: patternOf(r.prefix, r.padLength), + example: exampleNo(r.prefix, r.padLength), + })); + } + + async updateNumberRule(id: string, user: AuthUser, dto: { prefix?: string; padLength?: number; name?: string }) { + this.assertManage(user); + const row = await this.prisma.numberRule.findFirst({ where: { id } }); + if (!row) throw new NotFoundException('编号规则不存在'); + if (dto.prefix && !/^[A-Z]{2,6}$/.test(dto.prefix)) throw new BadRequestException('前缀须是 2 到 6 位大写字母'); + if (dto.padLength && (dto.padLength < 3 || dto.padLength > 8)) throw new BadRequestException('流水位数须在 3 到 8'); + const updated = await this.prisma.numberRule.update({ + where: { id }, + data: { + prefix: dto.prefix || undefined, + padLength: dto.padLength, + name: dto.name?.trim() || undefined, + }, + }); + await writeOpLog(this.prisma, { + userId: user.id, + username: user.username, + action: LOG_ACTION.ACL, + path: `/system/number-rules/${id}`, + detail: `编号规则 ${updated.code} → ${updated.prefix}`, + }); + return { ...updated, pattern: patternOf(updated.prefix, updated.padLength), example: exampleNo(updated.prefix, updated.padLength) }; + } + + async listLogs(page: number, pageSize: number, action?: string) { + const where = { tenantId: '1', ...(action ? { action } : {}) }; + const [items, total] = await this.prisma.$transaction([ + this.prisma.operationLog.findMany({ + where, + orderBy: { createdAt: 'desc' }, + skip: (page - 1) * pageSize, + take: pageSize, + }), + this.prisma.operationLog.count({ where }), + ]); + return { items, ...pageMeta(page, pageSize, total) }; + } + + async createDict(user: AuthUser, dto: { dictType: string; label: string; value: string; sortNo?: number }) { + this.assertManage(user); + if (!dto.dictType?.trim() || !dto.label?.trim() || !dto.value?.trim()) { + throw new BadRequestException('请填写字典类型、名称和值'); + } + return this.prisma.dictItem.create({ + data: { + tenantId: '1', + dictType: dto.dictType.trim(), + label: dto.label.trim(), + value: dto.value.trim(), + sortNo: dto.sortNo === undefined || dto.sortNo === null ? 99 : Number(dto.sortNo), + }, + }); + } + + async updateDict( + id: string, + user: AuthUser, + dto: { dictType?: string; label?: string; value?: string; sortNo?: number }, + ) { + this.assertManage(user); + const row = await this.prisma.dictItem.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('字典项不存在'); + return this.prisma.dictItem.update({ + where: { id }, + data: { + dictType: dto.dictType?.trim() || row.dictType, + label: dto.label?.trim() || row.label, + value: dto.value?.trim() || row.value, + sortNo: dto.sortNo === undefined || dto.sortNo === null ? row.sortNo : Number(dto.sortNo), + }, + }); + } + + async deleteDict(id: string, user: AuthUser) { + this.assertManage(user); + const row = await this.prisma.dictItem.findFirst({ where: { id, tenantId: '1' } }); + if (!row) throw new NotFoundException('字典项不存在'); + await this.prisma.dictItem.delete({ where: { id } }); + return { ok: true }; + } + + private appReleasePath() { + return process.env.APP_RELEASE_FILE || join(process.cwd(), 'data/app-release.json'); + } + + defaultAppRelease() { + return { + platform: 'android', + version: process.env.APP_ANDROID_VERSION || '1.1.0', + build: Number(process.env.APP_ANDROID_BUILD || 2), + url: process.env.APP_ANDROID_URL || 'https://oa.fysxkj.com/fengying-oa.apk', + windowsUrl: process.env.APP_WINDOWS_URL || '', + linuxUrl: process.env.APP_LINUX_URL || '', + linuxDebUrl: process.env.APP_LINUX_DEB_URL || '', + changelog: process.env.APP_ANDROID_CHANGELOG || '按微信/企业微信重做手机端:消息、工作台、通讯录、我;支持应用内升级。', + force: process.env.APP_ANDROID_FORCE === '1', + publishedAt: new Date().toISOString(), + }; + } + + getAppRelease() { + const file = this.appReleasePath(); + if (!existsSync(file)) return this.defaultAppRelease(); + try { + return { ...this.defaultAppRelease(), ...JSON.parse(readFileSync(file, 'utf8')) }; + } catch { + return this.defaultAppRelease(); + } + } + + updateAppRelease( + user: AuthUser, + dto: { version?: string; build?: number; url?: string; changelog?: string; force?: boolean; windowsUrl?: string; linuxUrl?: string; linuxDebUrl?: string }, + ) { + this.assertManage(user); + const next = { + ...this.getAppRelease(), + ...Object.fromEntries(Object.entries(dto).filter(([, v]) => v !== undefined)), + publishedAt: new Date().toISOString(), + }; + const file = this.appReleasePath(); + mkdirSync(dirname(file), { recursive: true }); + writeFileSync(file, JSON.stringify(next, null, 2)); + return next; + } +} diff --git a/apps/api/test/app.e2e-spec.ts b/apps/api/test/app.e2e-spec.ts new file mode 100644 index 0000000..ecf4ab8 --- /dev/null +++ b/apps/api/test/app.e2e-spec.ts @@ -0,0 +1,1450 @@ +import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import request from 'supertest'; +import { AppModule } from '../src/app.module'; + +describe('API e2e', () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [AppModule], + }).compile(); + app = moduleRef.createNestApplication(); + app.setGlobalPrefix('api/v1'); + app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true })); + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + it('liveness', async () => { + const res = await request(app.getHttpServer()).get('/api/v1/health/live'); + expect(res.status).toBe(200); + expect(res.body.code).toBe(0); + expect(res.body.data.status).toBe('ok'); + }); + + it('readiness', async () => { + const res = await request(app.getHttpServer()).get('/api/v1/health/ready'); + expect(res.status).toBe(200); + expect(res.body.data.checks.postgres).toBe('up'); + }); + + it('login and menu', async () => { + const login = await request(app.getHttpServer()) + .post('/api/v1/auth/login') + .send({ username: 'admin', password: 'Admin@123456', agreeTerms: true }); + expect(login.status).toBe(201); + expect(login.body.data.accessToken).toBeDefined(); + const token = login.body.data.accessToken; + const menus = await request(app.getHttpServer()) + .get('/api/v1/system/menus') + .set('Authorization', `Bearer ${token}`); + expect(menus.status).toBe(200); + expect(menus.body.data.length).toBeGreaterThan(5); + }); + + it('rejects bad password', async () => { + const res = await request(app.getHttpServer()) + .post('/api/v1/auth/login') + .send({ username: 'admin', password: 'wrong-password', agreeTerms: true }); + expect(res.status).toBe(401); + expect(res.body.code).toBe(401); + }); + + it('rejects login without agreeing to terms', async () => { + const res = await request(app.getHttpServer()) + .post('/api/v1/auth/login') + .send({ username: 'admin', password: 'Admin@123456' }); + expect(res.status).toBe(400); + expect(String(res.body.message || '')).toContain('用户协议'); + }); + + it('serves user agreement and privacy policy', async () => { + const agreement = await request(app.getHttpServer()).get('/api/v1/legal/user-agreement'); + expect(agreement.status).toBe(200); + expect(agreement.body.data.title).toBe('用户协议'); + expect(agreement.body.data.paragraphs.length).toBeGreaterThan(3); + const privacy = await request(app.getHttpServer()).get('/api/v1/legal/privacy'); + expect(privacy.status).toBe(200); + expect(privacy.body.data.title).toBe('隐私政策'); + }); + + async function login(username: string, password: string) { + const res = await request(app.getHttpServer()) + .post('/api/v1/auth/login') + .send({ username, password, agreeTerms: true }); + expect(res.status).toBe(201); + return res.body.data.accessToken as string; + } + + it('runs screening to final review, and hides terminated from employees', async () => { + const owner = await login('owner', 'Demo@123456'); + const directors = await request(app.getHttpServer()) + .get('/api/v1/directors') + .set('Authorization', `Bearer ${owner}`); + expect(directors.status).toBe(200); + const dirsoft = (directors.body.data as { id: string; name: string }[]).find( + (d) => d.name === '吴软件', + ); + const dir3d = (directors.body.data as { id: string; name: string }[]).find( + (d) => d.name === '赵三维', + ); + expect(dirsoft && dir3d).toBeTruthy(); + + const created = await request(app.getHttpServer()) + .post('/api/v1/bid-cases') + .set('Authorization', `Bearer ${owner}`) + .send({ + name: '阶段1主链演示', + externalNo: 'E2E-P1-001', + projectType: '集成', + source: '招标公告', + qualificationNeed: '涉乙', + tenderMethod: '公开招标', + applyMethod: '网上申领', + openMethod: '线下开标', + openPlace: '交易中心', + reviewerIds: [dirsoft!.id, dir3d!.id], + }); + expect(created.status).toBe(201); + const bidId = created.body.data.id as string; + expect(created.body.data.status).toBe('TECH_INITIAL'); + + const softToken = await login('dirsoft', 'Demo@123456'); + const first = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${softToken}`) + .send({ result: 'APPROVED', comment: '软件可做' }); + expect(first.status).toBe(201); + expect(first.body.data.status).toBe('TECH_INITIAL'); + + const d3Token = await login('dir3d', 'Demo@123456'); + const second = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${d3Token}`) + .send({ result: 'APPROVED', comment: '三维可做' }); + expect(second.status).toBe(201); + expect(second.body.data.status).toBe('FETCH'); + + const biz = await login('bizstaff', 'Demo@123456'); + const fetched = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/fetch`) + .set('Authorization', `Bearer ${biz}`) + .send({ nasPath: '\\\\nas\\bids\\E2E-P1-001', openPlace: '交易中心三楼' }); + expect(fetched.status).toBe(201); + expect(fetched.body.data.status).toBe('TECH_FINAL'); + + const final1 = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${softToken}`) + .send({ result: 'APPROVED' }); + expect(final1.body.data.status).toBe('TECH_FINAL'); + const final2 = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${d3Token}`) + .send({ result: 'APPROVED' }); + expect(final2.status).toBe(201); + expect(final2.body.data.status).toBe('DRAFTING'); + + const drafting = await request(app.getHttpServer()) + .get('/api/v1/bid-cases?bucket=drafting') + .set('Authorization', `Bearer ${owner}`); + expect(drafting.body.data.items.some((i: { id: string }) => i.id === bidId)).toBe(true); + + const rejectCreate = await request(app.getHttpServer()) + .post('/api/v1/bid-cases') + .set('Authorization', `Bearer ${owner}`) + .send({ + name: '阶段1驳回演示', + externalNo: 'E2E-P1-002', + projectType: '软件', + source: '招标公告', + qualificationNeed: '无需资质', + tenderMethod: '公开招标', + reviewerIds: [dirsoft!.id], + }); + const rejectId = rejectCreate.body.data.id as string; + const rejected = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${rejectId}/reviews`) + .set('Authorization', `Bearer ${softToken}`) + .send({ result: 'REJECTED', comment: '工期不够' }); + expect(rejected.body.data.status).toBe('TERMINATED'); + + const employee = await login('employee', 'Demo@123456'); + const hidden = await request(app.getHttpServer()) + .get('/api/v1/bid-cases?bucket=terminated') + .set('Authorization', `Bearer ${employee}`); + expect(hidden.status).toBe(403); + + const visible = await request(app.getHttpServer()) + .get('/api/v1/bid-cases?bucket=terminated') + .set('Authorization', `Bearer ${owner}`); + expect(visible.status).toBe(200); + expect(visible.body.data.items.some((i: { id: string }) => i.id === rejectId)).toBe(true); + }, 60000); + + it('runs drafting to open result, and bounces rejected drafts back', async () => { + const owner = await login('owner', 'Demo@123456'); + const bizdir = await login('bizdir', 'Demo@123456'); + const biz = await login('bizstaff', 'Demo@123456'); + const directors = await request(app.getHttpServer()) + .get('/api/v1/directors') + .set('Authorization', `Bearer ${owner}`); + const dirsoft = (directors.body.data as { id: string; name: string }[]).find( + (d) => d.name === '吴软件', + ); + expect(dirsoft).toBeTruthy(); + const staff = await request(app.getHttpServer()) + .get('/api/v1/staff') + .set('Authorization', `Bearer ${owner}`); + const bizUser = (staff.body.data as { id: string; username: string }[]).find( + (s) => s.username === 'bizstaff', + ); + const employee = (staff.body.data as { id: string; username: string }[]).find( + (s) => s.username === 'employee', + ); + expect(bizUser && employee).toBeTruthy(); + + const created = await request(app.getHttpServer()) + .post('/api/v1/bid-cases') + .set('Authorization', `Bearer ${owner}`) + .send({ + name: '阶段2主链演示', + externalNo: 'E2E-P2-001', + projectType: '软件', + source: '招标公告', + qualificationNeed: '无需资质', + tenderMethod: '公开招标', + reviewerIds: [dirsoft!.id], + }); + const bidId = created.body.data.id as string; + const softToken = await login('dirsoft', 'Demo@123456'); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${softToken}`) + .send({ result: 'APPROVED' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/fetch`) + .set('Authorization', `Bearer ${biz}`) + .send({ nasPath: '\\\\nas\\p2' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${softToken}`) + .send({ result: 'APPROVED' }); + + const assigned = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/assign-drafter`) + .set('Authorization', `Bearer ${bizdir}`) + .send({ drafterId: bizUser!.id, review1Id: bizUser!.id }); + expect(assigned.body.data.status).toBe('DRAFTING'); + + const submitted = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/submit-draft`) + .set('Authorization', `Bearer ${biz}`) + .send({ nasPath: '\\\\nas\\p2\\v1', note: '初稿' }); + expect(submitted.body.data.status).toBe('REVIEW1'); + + const bounced = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${biz}`) + .send({ result: 'REJECTED', comment: '封面格式不对' }); + expect(bounced.body.data.status).toBe('DRAFTING'); + + const resubmit = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/submit-draft`) + .set('Authorization', `Bearer ${biz}`) + .send({ nasPath: '\\\\nas\\p2\\v2', note: '已改封面' }); + expect(resubmit.body.data.status).toBe('REVIEW1'); + expect(resubmit.body.data.versions).toHaveLength(2); + + const r1 = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${biz}`) + .send({ result: 'APPROVED' }); + expect(r1.body.data.status).toBe('REVIEW2'); + + const r2 = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${bizdir}`) + .send({ result: 'APPROVED' }); + expect(r2.body.data.status).toBe('PRINT'); + + const printed = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/print`) + .set('Authorization', `Bearer ${biz}`) + .send({ amount: 800, note: '印标店' }); + expect(printed.body.data.status).toBe('LIST'); + expect(printed.body.data.expenses.length).toBeGreaterThan(0); + + const completed = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/complete`) + .set('Authorization', `Bearer ${biz}`) + .send({ + bidderIds: [employee!.id], + bondMethod: '电汇', + bondAmount: 50000, + quoteAmount: '1280000', + }); + expect(completed.body.data.status).toBe('PENDING'); + + const empToken = await login('employee', 'Demo@123456'); + const opened = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/open-result`) + .set('Authorization', `Bearer ${empToken}`) + .send({ result: 'LOST', costDisposition: 'MARKETING', comment: '第二名' }); + expect(opened.status).toBe(201); + expect(opened.body.data.status).toBe('LOST'); + expect(opened.body.data.costDisposition).toBe('MARKETING'); + + const resultList = await request(app.getHttpServer()) + .get('/api/v1/bid-cases?bucket=result') + .set('Authorization', `Bearer ${owner}`); + expect(resultList.body.data.items.some((i: { id: string }) => i.id === bidId)).toBe(true); + }, 60000); + + it('converts a won bid to contract, rejects then approves into a project', async () => { + const owner = await login('owner', 'Demo@123456'); + const bizdir = await login('bizdir', 'Demo@123456'); + const biz = await login('bizstaff', 'Demo@123456'); + const empToken = await login('employee', 'Demo@123456'); + const directors = await request(app.getHttpServer()) + .get('/api/v1/directors') + .set('Authorization', `Bearer ${owner}`); + const dirsoft = (directors.body.data as { id: string; name: string }[]).find( + (d) => d.name === '吴软件', + ); + const staff = await request(app.getHttpServer()) + .get('/api/v1/staff') + .set('Authorization', `Bearer ${owner}`); + const bizUser = (staff.body.data as { id: string; username: string }[]).find( + (s) => s.username === 'bizstaff', + ); + const employee = (staff.body.data as { id: string; username: string }[]).find( + (s) => s.username === 'employee', + ); + expect(dirsoft && bizUser && employee).toBeTruthy(); + + const created = await request(app.getHttpServer()) + .post('/api/v1/bid-cases') + .set('Authorization', `Bearer ${owner}`) + .send({ + name: '阶段3主链演示', + externalNo: 'E2E-P3-001', + projectType: '软件', + source: '招标公告', + qualificationNeed: '无需资质', + tenderMethod: '公开招标', + reviewerIds: [dirsoft!.id], + }); + const bidId = created.body.data.id as string; + const softToken = await login('dirsoft', 'Demo@123456'); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${softToken}`) + .send({ result: 'APPROVED' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/fetch`) + .set('Authorization', `Bearer ${biz}`) + .send({ nasPath: '\\\\nas\\p3' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${softToken}`) + .send({ result: 'APPROVED' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/assign-drafter`) + .set('Authorization', `Bearer ${bizdir}`) + .send({ drafterId: bizUser!.id, review1Id: bizUser!.id }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/submit-draft`) + .set('Authorization', `Bearer ${biz}`) + .send({ nasPath: '\\\\nas\\p3\\v1' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${biz}`) + .send({ result: 'APPROVED' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${bizdir}`) + .send({ result: 'APPROVED' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/print`) + .set('Authorization', `Bearer ${biz}`) + .send({ amount: 800, note: '印标店' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/complete`) + .set('Authorization', `Bearer ${biz}`) + .send({ bidderIds: [employee!.id], bondMethod: '电汇', quoteAmount: '1280000' }); + + const opened = await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/open-result`) + .set('Authorization', `Bearer ${empToken}`) + .send({ result: 'WON', comment: '第一名' }); + expect(opened.status).toBe(201); + expect(opened.body.data.status).toBe('WON'); + expect(opened.body.data.contracts).toHaveLength(1); + expect(opened.body.data.contracts[0].status).toBe('DRAFT'); + expect(opened.body.data.contracts[0].projects).toHaveLength(1); + const contractId = opened.body.data.contracts[0].id as string; + + const asBiz = await request(app.getHttpServer()) + .get(`/api/v1/bid-cases/${bidId}`) + .set('Authorization', `Bearer ${biz}`); + expect(asBiz.body.data.myActions.canConvertToContract).toBe(false); + + const forbidden = await request(app.getHttpServer()) + .post('/api/v1/contracts') + .set('Authorization', `Bearer ${empToken}`) + .send({ bidCaseId: bidId, amount: 1280000 }); + expect(forbidden.status).toBe(403); + + const drafted = await request(app.getHttpServer()) + .post('/api/v1/contracts') + .set('Authorization', `Bearer ${biz}`) + .send({ bidCaseId: bidId, amount: 1280000 }); + expect(drafted.status).toBe(400); + + await request(app.getHttpServer()) + .patch(`/api/v1/contracts/${contractId}`) + .set('Authorization', `Bearer ${biz}`) + .send({ contractNo: `HT-E2E-${bidId.slice(0, 8)}` }); + + const noTerms = await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/submit`) + .set('Authorization', `Bearer ${biz}`); + expect(noTerms.status).toBe(400); + + const miles = await request(app.getHttpServer()) + .put(`/api/v1/contracts/${contractId}/milestones`) + .set('Authorization', `Bearer ${biz}`) + .send({ + items: [ + { name: '预付款', ratio: 30 }, + { name: '验收款', ratio: 70 }, + ], + }); + expect(miles.status).toBe(200); + + const sealed = await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/seal`) + .set('Authorization', `Bearer ${biz}`) + .send({ sealType: '合同章', reason: '签订中标合同' }); + expect(sealed.status).toBe(201); + + const submitted = await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/submit`) + .set('Authorization', `Bearer ${biz}`); + expect(submitted.status).toBe(201); + expect(submitted.body.data.status).toBe('PENDING'); + + const rejected = await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/reviews`) + .set('Authorization', `Bearer ${owner}`) + .send({ result: 'REJECTED', comment: '收款节点要改' }); + expect(rejected.status).toBe(201); + expect(rejected.body.data.status).toBe('DRAFT'); + expect(rejected.body.data.projects).toHaveLength(1); + + await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/submit`) + .set('Authorization', `Bearer ${biz}`); + + const ownerOk = await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/reviews`) + .set('Authorization', `Bearer ${owner}`) + .send({ result: 'APPROVED' }); + expect(ownerOk.body.data.status).toBe('PENDING'); + expect(ownerOk.body.data.projects).toHaveLength(1); + + const dirOk = await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/reviews`) + .set('Authorization', `Bearer ${bizdir}`) + .send({ result: 'APPROVED' }); + expect(dirOk.status).toBe(201); + expect(dirOk.body.data.status).toBe('APPROVED'); + expect(dirOk.body.data.projects).toHaveLength(1); + expect(dirOk.body.data.projects[0].projectNo).toMatch(/^PRJ-\d{4}-\d{5}$/); + expect(Number(dirOk.body.data.projects[0].presalesCost)).toBe(800); + expect(dirOk.body.data.sealRequests[0].status).toBe('APPROVED'); + + const bid = await request(app.getHttpServer()) + .get(`/api/v1/bid-cases/${bidId}`) + .set('Authorization', `Bearer ${owner}`); + expect(bid.body.data.expenses.every((e: { projectId: string | null }) => e.projectId)).toBe(true); + + const projects = await request(app.getHttpServer()) + .get('/api/v1/projects') + .set('Authorization', `Bearer ${owner}`); + expect(projects.body.data.items.some((p: { contractId: string }) => p.contractId === contractId)).toBe( + true, + ); + }, 60000); + + it('requires cost object, offsets loans, and flags overdue bonds', async () => { + const owner = await login('owner', 'Demo@123456'); + const biz = await login('bizstaff', 'Demo@123456'); + const finance = await login('finance', 'Demo@123456'); + const emp = await login('employee', 'Demo@123456'); + const directors = await request(app.getHttpServer()) + .get('/api/v1/directors') + .set('Authorization', `Bearer ${owner}`); + const dirsoft = (directors.body.data as { id: string; name: string }[]).find((d) => d.name === '吴软件'); + const created = await request(app.getHttpServer()) + .post('/api/v1/bid-cases') + .set('Authorization', `Bearer ${owner}`) + .send({ + name: '阶段4差旅借款', + externalNo: 'E2E-P4-001', + projectType: '软件', + source: '招标公告', + qualificationNeed: '无需资质', + tenderMethod: '公开招标', + reviewerIds: [dirsoft!.id], + }); + const bidId = created.body.data.id as string; + + const badCost = await request(app.getHttpServer()) + .post('/api/v1/expenses') + .set('Authorization', `Bearer ${biz}`) + .send({ amount: 800, costType: 'OTHER' }); + expect(badCost.status).toBe(400); + + const noBid = await request(app.getHttpServer()) + .post('/api/v1/expenses') + .set('Authorization', `Bearer ${biz}`) + .send({ amount: 800, costType: 'BID' }); + expect(noBid.status).toBe(400); + + const loan = await request(app.getHttpServer()) + .post('/api/v1/loans') + .set('Authorization', `Bearer ${biz}`) + .send({ costType: 'BID', bidCaseId: bidId, amount: 2000, purpose: '取标差旅' }); + expect(loan.status).toBe(201); + const loanId = loan.body.data.id as string; + await request(app.getHttpServer()) + .post(`/api/v1/loans/${loanId}/submit`) + .set('Authorization', `Bearer ${biz}`); + const loanOk = await request(app.getHttpServer()) + .post(`/api/v1/loans/${loanId}/reviews`) + .set('Authorization', `Bearer ${finance}`) + .send({ result: 'APPROVED' }); + expect(loanOk.body.data.status).toBe('OPEN'); + + const dup = await request(app.getHttpServer()) + .post('/api/v1/expenses') + .set('Authorization', `Bearer ${biz}`) + .send({ costType: 'BID', bidCaseId: bidId, amount: 800 }); + expect(dup.status).toBe(400); + + const exp = await request(app.getHttpServer()) + .post('/api/v1/expenses') + .set('Authorization', `Bearer ${biz}`) + .send({ costType: 'BID', bidCaseId: bidId, amount: 800, loanId }); + expect(exp.status).toBe(201); + const expId = exp.body.data.id as string; + await request(app.getHttpServer()) + .post(`/api/v1/expenses/${expId}/submit`) + .set('Authorization', `Bearer ${biz}`); + const bounced = await request(app.getHttpServer()) + .post(`/api/v1/expenses/${expId}/reviews`) + .set('Authorization', `Bearer ${finance}`) + .send({ result: 'REJECTED', comment: '发票不齐' }); + expect(bounced.body.data.status).toBe('REJECTED'); + const stillOpen = await request(app.getHttpServer()) + .get(`/api/v1/loans/${loanId}`) + .set('Authorization', `Bearer ${biz}`); + expect(stillOpen.body.data.status).toBe('OPEN'); + + const categorized = await request(app.getHttpServer()) + .patch(`/api/v1/expenses/${expId}`) + .set('Authorization', `Bearer ${biz}`) + .send({ costType: 'BID', bidCaseId: bidId, amount: 800, loanId, expenseCategory: '交通' }); + expect(categorized.status).toBe(200); + expect(categorized.body.data.expenseCategory).toBe('交通'); + + const uploaded = await request(app.getHttpServer()) + .post('/api/v1/files') + .set('Authorization', `Bearer ${biz}`) + .attach('file', Buffer.from('%PDF-1.4 invoice'), { + filename: 'fapiao.pdf', + contentType: 'application/pdf', + }) + .field('bizType', 'EXPENSE') + .field('bizId', expId); + expect(uploaded.status).toBe(201); + expect(uploaded.body.data.fileName).toBe('fapiao.pdf'); + + const withFile = await request(app.getHttpServer()) + .get(`/api/v1/expenses/${expId}`) + .set('Authorization', `Bearer ${biz}`); + expect(withFile.body.data.files).toHaveLength(1); + + await request(app.getHttpServer()) + .post(`/api/v1/expenses/${expId}/submit`) + .set('Authorization', `Bearer ${biz}`); + const expOk = await request(app.getHttpServer()) + .post(`/api/v1/expenses/${expId}/reviews`) + .set('Authorization', `Bearer ${finance}`) + .send({ result: 'APPROVED' }); + expect(expOk.body.data.status).toBe('APPROVED'); + const offset = await request(app.getHttpServer()) + .get(`/api/v1/loans/${loanId}`) + .set('Authorization', `Bearer ${biz}`); + expect(offset.body.data.status).toBe('OPEN'); + expect(Number(offset.body.data.remaining)).toBe(1200); + + const rest = await request(app.getHttpServer()) + .post('/api/v1/expenses') + .set('Authorization', `Bearer ${biz}`) + .send({ costType: 'BID', bidCaseId: bidId, amount: 1200, loanId }); + const restId = rest.body.data.id as string; + await request(app.getHttpServer()) + .post(`/api/v1/expenses/${restId}/submit`) + .set('Authorization', `Bearer ${biz}`); + await request(app.getHttpServer()) + .post(`/api/v1/expenses/${restId}/reviews`) + .set('Authorization', `Bearer ${finance}`) + .send({ result: 'APPROVED' }); + const done = await request(app.getHttpServer()) + .get(`/api/v1/loans/${loanId}`) + .set('Authorization', `Bearer ${biz}`); + expect(done.body.data.status).toBe('OFFSET'); + expect(Number(done.body.data.remaining)).toBe(0); + + const noBond = await request(app.getHttpServer()) + .post('/api/v1/bonds') + .set('Authorization', `Bearer ${finance}`) + .send({ bondType: 'TENDER', amount: 50000 }); + expect(noBond.status).toBe(400); + + const empBond = await request(app.getHttpServer()) + .get('/api/v1/bonds') + .set('Authorization', `Bearer ${emp}`); + expect(empBond.status).toBe(403); + + const bond = await request(app.getHttpServer()) + .post('/api/v1/bonds') + .set('Authorization', `Bearer ${finance}`) + .send({ bondType: 'TENDER', amount: 50000, bidCaseId: bidId }); + expect(bond.status).toBe(201); + const yesterday = new Date(Date.now() - 86400000).toISOString(); + await request(app.getHttpServer()) + .post(`/api/v1/bonds/${bond.body.data.id}/pay`) + .set('Authorization', `Bearer ${finance}`) + .send({ dueBackAt: yesterday }); + const overdue = await request(app.getHttpServer()) + .get('/api/v1/bonds?bucket=overdue') + .set('Authorization', `Bearer ${finance}`); + expect(overdue.body.data.items.some((b: { id: string }) => b.id === bond.body.data.id)).toBe(true); + }, 60000); + + it('invoices a contract milestone then records receipt', async () => { + const owner = await login('owner', 'Demo@123456'); + const finance = await login('finance', 'Demo@123456'); + const list = await request(app.getHttpServer()) + .get('/api/v1/contracts') + .set('Authorization', `Bearer ${owner}`); + const contract = (list.body.data.items as { + id: string; + status: string; + milestones: { id: string }[]; + }[]).find((c) => c.status === 'APPROVED' && c.milestones?.length); + expect(contract).toBeTruthy(); + const mileId = contract!.milestones[0].id; + const created = await request(app.getHttpServer()) + .post('/api/v1/invoices') + .set('Authorization', `Bearer ${finance}`) + .send({ contractId: contract!.id, milestoneId: mileId }); + expect(created.status).toBe(201); + expect(created.body.data.status).toBe('TO_ISSUE'); + expect(Number(created.body.data.amount)).toBeGreaterThan(0); + const dup = await request(app.getHttpServer()) + .post('/api/v1/invoices') + .set('Authorization', `Bearer ${finance}`) + .send({ contractId: contract!.id, milestoneId: mileId }); + expect(dup.status).toBe(400); + const issued = await request(app.getHttpServer()) + .post(`/api/v1/invoices/${created.body.data.id}/issue`) + .set('Authorization', `Bearer ${finance}`) + .send({ invoiceNo: 'INV-P4-001' }); + expect(issued.body.data.status).toBe('ISSUED'); + const received = await request(app.getHttpServer()) + .post(`/api/v1/invoices/${created.body.data.id}/receive`) + .set('Authorization', `Bearer ${finance}`); + expect(received.body.data.status).toBe('RECEIVED'); + }, 30000); + + it('breaks down WBS, fills timesheets, blocks over-budget expenses, and invoices on acceptance', async () => { + const owner = await login('owner', 'Demo@123456'); + const bizdir = await login('bizdir', 'Demo@123456'); + const biz = await login('bizstaff', 'Demo@123456'); + const empToken = await login('employee', 'Demo@123456'); + const hr = await login('hr', 'Demo@123456'); + const directors = await request(app.getHttpServer()) + .get('/api/v1/directors') + .set('Authorization', `Bearer ${owner}`); + const dirsoft = (directors.body.data as { id: string; name: string }[]).find((d) => d.name === '吴软件'); + const staff = await request(app.getHttpServer()) + .get('/api/v1/staff') + .set('Authorization', `Bearer ${owner}`); + const bizUser = (staff.body.data as { id: string; username: string }[]).find((s) => s.username === 'bizstaff'); + const employee = (staff.body.data as { id: string; username: string }[]).find((s) => s.username === 'employee'); + expect(dirsoft && bizUser && employee).toBeTruthy(); + + const created = await request(app.getHttpServer()) + .post('/api/v1/bid-cases') + .set('Authorization', `Bearer ${owner}`) + .send({ + name: '阶段5主链演示', + externalNo: 'E2E-P5-001', + projectType: '软件', + source: '招标公告', + qualificationNeed: '无需资质', + tenderMethod: '公开招标', + reviewerIds: [dirsoft!.id], + }); + const bidId = created.body.data.id as string; + const softToken = await login('dirsoft', 'Demo@123456'); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${softToken}`) + .send({ result: 'APPROVED' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/fetch`) + .set('Authorization', `Bearer ${biz}`) + .send({ nasPath: '\\\\nas\\p5' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${softToken}`) + .send({ result: 'APPROVED' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/assign-drafter`) + .set('Authorization', `Bearer ${bizdir}`) + .send({ drafterId: bizUser!.id, review1Id: bizUser!.id }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/submit-draft`) + .set('Authorization', `Bearer ${biz}`) + .send({ nasPath: '\\\\nas\\p5\\v1' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${biz}`) + .send({ result: 'APPROVED' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/reviews`) + .set('Authorization', `Bearer ${bizdir}`) + .send({ result: 'APPROVED' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/print`) + .set('Authorization', `Bearer ${biz}`) + .send({ amount: 600, note: '印标' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/complete`) + .set('Authorization', `Bearer ${biz}`) + .send({ bidderIds: [employee!.id], bondMethod: '电汇', quoteAmount: '880000' }); + await request(app.getHttpServer()) + .post(`/api/v1/bid-cases/${bidId}/open-result`) + .set('Authorization', `Bearer ${empToken}`) + .send({ result: 'WON' }); + const opened = await request(app.getHttpServer()) + .get(`/api/v1/bid-cases/${bidId}`) + .set('Authorization', `Bearer ${biz}`); + const contractId = opened.body.data.contracts[0].id as string; + await request(app.getHttpServer()) + .patch(`/api/v1/contracts/${contractId}`) + .set('Authorization', `Bearer ${biz}`) + .send({ contractNo: `HT-E2E5-${bidId.slice(0, 8)}` }); + await request(app.getHttpServer()) + .put(`/api/v1/contracts/${contractId}/milestones`) + .set('Authorization', `Bearer ${biz}`) + .send({ + items: [ + { name: '预付款', ratio: 30 }, + { name: '验收款', ratio: 70 }, + ], + }); + await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/seal`) + .set('Authorization', `Bearer ${biz}`) + .send({ sealType: '合同章', reason: '阶段5合同' }); + await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/submit`) + .set('Authorization', `Bearer ${biz}`); + await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/reviews`) + .set('Authorization', `Bearer ${owner}`) + .send({ result: 'APPROVED' }); + const approved = await request(app.getHttpServer()) + .post(`/api/v1/contracts/${contractId}/reviews`) + .set('Authorization', `Bearer ${bizdir}`) + .send({ result: 'APPROVED' }); + expect(approved.body.data.projects).toHaveLength(1); + const projectId = approved.body.data.projects[0].id as string; + + const hidden = await request(app.getHttpServer()) + .get('/api/v1/projects') + .set('Authorization', `Bearer ${hr}`); + expect(hidden.status).toBe(403); + + const depts = await request(app.getHttpServer()) + .get('/api/v1/departments') + .set('Authorization', `Bearer ${softToken}`); + const software = (depts.body.data as { id: string; code: string }[]).find((d) => d.code === 'TECH_SOFTWARE'); + const d3 = (depts.body.data as { id: string; code: string }[]).find((d) => d.code === 'TECH_3D'); + expect(software && d3).toBeTruthy(); + + const t1 = await request(app.getHttpServer()) + .post(`/api/v1/projects/${projectId}/tasks`) + .set('Authorization', `Bearer ${softToken}`) + .send({ name: '软件开发', departmentId: software!.id, assigneeId: employee!.id }); + expect(t1.status).toBe(201); + const t2 = await request(app.getHttpServer()) + .post(`/api/v1/projects/${projectId}/tasks`) + .set('Authorization', `Bearer ${softToken}`) + .send({ name: '三维制作', departmentId: d3!.id }); + expect(t2.status).toBe(201); + + const tasks = await request(app.getHttpServer()) + .get('/api/v1/project-tasks') + .set('Authorization', `Bearer ${owner}`); + expect(tasks.body.data.filter((t: { projectId: string }) => t.projectId === projectId)).toHaveLength(2); + + const budgets = await request(app.getHttpServer()) + .put(`/api/v1/projects/${projectId}/budgets`) + .set('Authorization', `Bearer ${owner}`) + .send({ + items: [ + { category: 'LABOR', amount: 1000 }, + { category: 'TRAVEL', amount: 500 }, + { category: 'PURCHASE', amount: 8000 }, + ], + }); + expect(budgets.status).toBe(200); + expect(Number(budgets.body.data.hourRate)).toBe(200); + + const sheet = await request(app.getHttpServer()) + .post('/api/v1/timesheets') + .set('Authorization', `Bearer ${empToken}`) + .send({ + projectId, + taskId: t1.body.data.id, + workDate: '2026-08-29', + hours: 2, + note: '联调', + }); + expect(sheet.status).toBe(201); + expect(Number(sheet.body.data.costAmount)).toBe(400); + + const overLabor = await request(app.getHttpServer()) + .post('/api/v1/timesheets') + .set('Authorization', `Bearer ${empToken}`) + .send({ projectId, workDate: '2026-08-30', hours: 4 }); + expect(overLabor.status).toBe(400); + + const noCat = await request(app.getHttpServer()) + .post('/api/v1/expenses') + .set('Authorization', `Bearer ${biz}`) + .send({ costType: 'PROJECT', projectId, amount: 100 }); + expect(noCat.status).toBe(400); + + const travel = await request(app.getHttpServer()) + .post('/api/v1/expenses') + .set('Authorization', `Bearer ${biz}`) + .send({ costType: 'PROJECT', projectId, amount: 400, budgetCategory: 'TRAVEL', remark: '现场' }); + expect(travel.status).toBe(201); + + const overTravel = await request(app.getHttpServer()) + .post('/api/v1/expenses') + .set('Authorization', `Bearer ${biz}`) + .send({ costType: 'PROJECT', projectId, amount: 200, budgetCategory: 'TRAVEL' }); + expect(overTravel.status).toBe(400); + + const tooSoon = await request(app.getHttpServer()) + .post(`/api/v1/projects/${projectId}/accept`) + .set('Authorization', `Bearer ${owner}`) + .send({ kind: 'CUSTOMER', result: 'APPROVED' }); + expect(tooSoon.status).toBe(400); + + const internal = await request(app.getHttpServer()) + .post(`/api/v1/projects/${projectId}/accept`) + .set('Authorization', `Bearer ${softToken}`) + .send({ kind: 'INTERNAL', result: 'APPROVED' }); + expect(internal.status).toBe(201); + expect(internal.body.data.status).toBe('INTERNAL_ACCEPTED'); + + const customer = await request(app.getHttpServer()) + .post(`/api/v1/projects/${projectId}/accept`) + .set('Authorization', `Bearer ${owner}`) + .send({ kind: 'CUSTOMER', result: 'APPROVED' }); + expect(customer.status).toBe(201); + expect(customer.body.data.status).toBe('ACCEPTED'); + expect(customer.body.data.invoicesCreated).toBe(2); + expect( + customer.body.data.contract.milestones.every((m: { invoice: { status: string } | null }) => m.invoice?.status), + ).toBe(true); + + const empProjects = await request(app.getHttpServer()) + .get('/api/v1/projects') + .set('Authorization', `Bearer ${empToken}`); + expect(empProjects.status).toBe(200); + expect(empProjects.body.data.items.some((p: { id: string }) => p.id === projectId)).toBe(true); + }, 60000); + + it('warns expiring qualifications, overdue borrows and take-out seals, and lists calendar dates', async () => { + const owner = await login('owner', 'Demo@123456'); + const biz = await login('bizstaff', 'Demo@123456'); + const emp = await login('employee', 'Demo@123456'); + const hr = await login('hr', 'Demo@123456'); + + const forbidden = await request(app.getHttpServer()) + .get('/api/v1/qualifications') + .set('Authorization', `Bearer ${hr}`); + expect(forbidden.status).toBe(403); + + const yesterday = new Date(Date.now() - 86400000).toISOString(); + const expired = await request(app.getHttpServer()) + .post('/api/v1/qualifications') + .set('Authorization', `Bearer ${owner}`) + .send({ name: '阶段6过期资质', code: 'QUAL_P6_OLD', expiresAt: yesterday, keeper: '行政' }); + expect(expired.status).toBe(201); + expect(expired.body.data.bucket).toBe('expired'); + + const expiringList = await request(app.getHttpServer()) + .get('/api/v1/qualifications?bucket=expired') + .set('Authorization', `Bearer ${owner}`); + expect(expiringList.body.data.items.some((q: { id: string }) => q.id === expired.body.data.id)).toBe(true); + + const staff = await request(app.getHttpServer()) + .get('/api/v1/staff') + .set('Authorization', `Bearer ${owner}`); + const employee = (staff.body.data as { id: string; username: string }[]).find((s) => s.username === 'employee'); + expect(employee).toBeTruthy(); + + const borrow = await request(app.getHttpServer()) + .post('/api/v1/credential-borrows') + .set('Authorization', `Bearer ${biz}`) + .send({ + itemName: '涉乙证书原件', + borrowerId: employee!.id, + dueAt: yesterday, + }); + expect(borrow.status).toBe(201); + expect(borrow.body.data.bucket).toBe('overdue'); + + const overdueBorrows = await request(app.getHttpServer()) + .get('/api/v1/credential-borrows?bucket=overdue') + .set('Authorization', `Bearer ${owner}`); + expect(overdueBorrows.body.data.items.some((b: { id: string }) => b.id === borrow.body.data.id)).toBe(true); + + const directors = await request(app.getHttpServer()) + .get('/api/v1/directors') + .set('Authorization', `Bearer ${owner}`); + const dirsoft = (directors.body.data as { id: string; name: string }[]).find((d) => d.name === '吴软件'); + const bid = await request(app.getHttpServer()) + .post('/api/v1/bid-cases') + .set('Authorization', `Bearer ${owner}`) + .send({ + name: '阶段6开标日程', + externalNo: 'E2E-P6-001', + projectType: '软件', + source: '招标公告', + qualificationNeed: '无需资质', + tenderMethod: '公开招标', + openAt: new Date(Date.now() + 3 * 86400000).toISOString(), + openPlace: '交易中心', + reviewerIds: [dirsoft!.id], + }); + expect(bid.status).toBe(201); + const bidId = bid.body.data.id as string; + + const noDue = await request(app.getHttpServer()) + .post('/api/v1/seal-requests') + .set('Authorization', `Bearer ${biz}`) + .send({ sealType: '公章', reason: '开标带章', bidCaseId: bidId, takeOut: true }); + expect(noDue.status).toBe(400); + + const seal = await request(app.getHttpServer()) + .post('/api/v1/seal-requests') + .set('Authorization', `Bearer ${biz}`) + .send({ + sealType: '公章', + reason: '开标带章', + bidCaseId: bidId, + takeOut: true, + returnAt: yesterday, + }); + expect(seal.status).toBe(201); + expect(seal.body.data.status).toBe('PENDING'); + const sealId = seal.body.data.id as string; + + const approved = await request(app.getHttpServer()) + .post(`/api/v1/seal-requests/${sealId}/reviews`) + .set('Authorization', `Bearer ${owner}`) + .send({ result: 'APPROVED' }); + expect(approved.status).toBe(201); + expect(approved.body.data.status).toBe('OUT'); + expect(approved.body.data.bucket).toBe('overdue'); + + const overview = await request(app.getHttpServer()) + .get('/api/v1/office/overview') + .set('Authorization', `Bearer ${owner}`); + expect(overview.body.data.expiringQualifications).toBeGreaterThan(0); + expect(overview.body.data.overdueBorrows).toBeGreaterThan(0); + expect(overview.body.data.overdueSeals).toBeGreaterThan(0); + expect(overview.body.data.upcomingOpens).toBeGreaterThan(0); + + const calendar = await request(app.getHttpServer()) + .get('/api/v1/office/calendar') + .set('Authorization', `Bearer ${owner}`); + expect(calendar.body.data.some((i: { kind: string }) => i.kind === 'BORROW' && i.overdue)).toBe(true); + expect(calendar.body.data.some((i: { kind: string }) => i.kind === 'SEAL' && i.overdue)).toBe(true); + expect(calendar.body.data.some((i: { kind: string }) => i.kind === 'BID_OPEN')).toBe(false); + + const returnedBorrow = await request(app.getHttpServer()) + .post(`/api/v1/credential-borrows/${borrow.body.data.id}/return`) + .set('Authorization', `Bearer ${biz}`); + expect(returnedBorrow.body.data.bucket).toBe('returned'); + + const returnedSeal = await request(app.getHttpServer()) + .post(`/api/v1/seal-requests/${sealId}/return`) + .set('Authorization', `Bearer ${biz}`); + expect(returnedSeal.body.data.status).toBe('RETURNED'); + + const empCal = await request(app.getHttpServer()) + .get('/api/v1/office/calendar') + .set('Authorization', `Bearer ${emp}`); + expect(empCal.status).toBe(200); + expect(empCal.body.data.some((i: { kind: string }) => i.kind === 'BID_OPEN')).toBe(false); + }, 30000); + + it('maintains hire-transfer-leave, imports attendance, payroll without housing fund, and lets directors change performance ratio', async () => { + const hr = await login('hr', 'Demo@123456'); + const empToken = await login('employee', 'Demo@123456'); + const soft = await login('dirsoft', 'Demo@123456'); + const film = await login('dirfilm', 'Demo@123456'); + const suffix = String(Date.now()).slice(-8); + const payDate = new Date(2026, 8, 1); + payDate.setMonth(payDate.getMonth() + (Number(suffix) % 30)); + const payMonth = `${payDate.getFullYear()}-${String(payDate.getMonth() + 1).padStart(2, '0')}`; + + const forbiddenDept = await request(app.getHttpServer()) + .post('/api/v1/departments') + .set('Authorization', `Bearer ${empToken}`) + .send({ name: '不可建', code: `NOPE_${suffix}`, deptType: 'TECH' }); + expect(forbiddenDept.status).toBe(403); + + const forbiddenPay = await request(app.getHttpServer()) + .get('/api/v1/payroll?yearMonth=2026-07') + .set('Authorization', `Bearer ${empToken}`); + expect(forbiddenPay.status).toBe(403); + + const depts = await request(app.getHttpServer()) + .get('/api/v1/departments') + .set('Authorization', `Bearer ${hr}`); + expect(Array.isArray(depts.body.data)).toBe(true); + const software = (depts.body.data as { id: string; code: string }[]).find((d) => d.code === 'TECH_SOFTWARE'); + const filmDept = (depts.body.data as { id: string; code: string }[]).find((d) => d.code === 'TECH_FILM'); + expect(software && filmDept).toBeTruthy(); + + const createdDept = await request(app.getHttpServer()) + .post('/api/v1/departments') + .set('Authorization', `Bearer ${hr}`) + .send({ + name: '阶段7演示组', + code: `TECH_P7_${suffix}`, + deptType: 'TECH', + parentId: software!.id, + categoryCode: 'CAT_SOFTWARE', + }); + expect(createdDept.status).toBe(201); + expect(createdDept.body.data.code).toBe(`TECH_P7_${suffix}`); + + const roster = await request(app.getHttpServer()) + .get('/api/v1/employees') + .set('Authorization', `Bearer ${hr}`); + const dirEmp = (roster.body.data.items as { id: string; employeeNo: string }[]).find((e) => e.employeeNo === 'E023'); + expect(dirEmp).toBeTruthy(); + + const hired = await request(app.getHttpServer()) + .post('/api/v1/employees') + .set('Authorization', `Bearer ${hr}`) + .send({ + name: '阶段7实习生', + employeeNo: `E7${suffix}`, + departmentId: software!.id, + title: '实习工程师', + restSchedule: 'SINGLE', + employmentStatus: 'INTERN', + hiredAt: '2026-07-16', + managerId: dirEmp!.id, + baseSalary: 10000, + perfBase: 3000, + socialBase: 10000, + }); + expect(hired.status).toBe(201); + expect(hired.body.data.employmentStatus).toBe('INTERN'); + const employeeId = hired.body.data.id as string; + + const intern = await request(app.getHttpServer()) + .post('/api/v1/employment-events') + .set('Authorization', `Bearer ${hr}`) + .send({ employeeId, kind: 'PROBATION', effectiveAt: '2026-07-20', comment: '转试用' }); + expect(intern.status).toBe(201); + expect(intern.body.data.employmentStatus).toBe('PROBATION'); + + const regular = await request(app.getHttpServer()) + .post('/api/v1/employment-events') + .set('Authorization', `Bearer ${hr}`) + .send({ employeeId, kind: 'REGULARIZE', effectiveAt: '2026-07-25', comment: '转正' }); + expect(regular.status).toBe(201); + expect(regular.body.data.employmentStatus).toBe('REGULAR'); + + const att = await request(app.getHttpServer()) + .post('/api/v1/attendance') + .set('Authorization', `Bearer ${hr}`) + .send({ + employeeId, + yearMonth: payMonth, + workDays: 12, + leaveDays: 1, + overtimeHours: 8, + remark: '加班转调休', + }); + expect(att.status).toBe(201); + expect(att.body.data.overtimeHours).toBe(8); + expect(att.body.data.compLeaveHours).toBe(8); + + const run = await request(app.getHttpServer()) + .post('/api/v1/payroll/run') + .set('Authorization', `Bearer ${hr}`) + .send({ yearMonth: payMonth }); + expect(run.status).toBe(201); + const item = ( + run.body.data.items as { + id: string; + employeeId: string; + housingFund: number; + tax: number; + socialPersonal: number; + leaveDeduct: number; + grossPay: number; + performancePay: number; + overtimeHours: number; + netPay: number; + }[] + ).find((i) => i.employeeId === employeeId); + expect(item).toBeTruthy(); + expect(item!.housingFund).toBe(0); + expect(item!.socialPersonal).toBeGreaterThan(0); + expect(item!.tax).toBeGreaterThan(0); + expect(item!.leaveDeduct).toBeGreaterThan(0); + expect(item!.overtimeHours).toBe(8); + expect(item!.grossPay).toBeGreaterThan(item!.netPay); + const beforePerf = item!.performancePay; + const beforeGross = item!.grossPay; + + const otherDept = await request(app.getHttpServer()) + .patch(`/api/v1/payroll-items/${item!.id}`) + .set('Authorization', `Bearer ${film}`) + .send({ performanceRatio: 50 }); + expect(otherDept.status).toBe(403); + + const adjusted = await request(app.getHttpServer()) + .patch(`/api/v1/payroll-items/${item!.id}`) + .set('Authorization', `Bearer ${soft}`) + .send({ performanceRatio: 80 }); + expect(adjusted.status).toBe(200); + expect(adjusted.body.data.performanceRatio).toBe(80); + expect(adjusted.body.data.performancePay).toBeLessThan(beforePerf); + expect(adjusted.body.data.grossPay).toBeLessThan(beforeGross); + expect(adjusted.body.data.housingFund).toBe(0); + + const transferred = await request(app.getHttpServer()) + .post('/api/v1/employment-events') + .set('Authorization', `Bearer ${hr}`) + .send({ + employeeId, + kind: 'TRANSFER', + toDepartmentId: filmDept!.id, + effectiveAt: '2026-07-28', + comment: '支援摄制', + }); + expect(transferred.status).toBe(201); + expect(transferred.body.data.departmentId).toBe(filmDept!.id); + + const left = await request(app.getHttpServer()) + .post('/api/v1/employment-events') + .set('Authorization', `Bearer ${hr}`) + .send({ employeeId, kind: 'LEAVE', effectiveAt: '2026-07-31', comment: '交接完成' }); + expect(left.status).toBe(201); + expect(left.body.data.employmentStatus).toBe('LEFT'); + + const confirmed = await request(app.getHttpServer()) + .post(`/api/v1/payroll/${run.body.data.run.id}/confirm`) + .set('Authorization', `Bearer ${hr}`); + expect(confirmed.status).toBe(201); + expect(confirmed.body.data.status).toBe('CONFIRMED'); + + const reopened = await request(app.getHttpServer()) + .patch(`/api/v1/payroll-items/${item!.id}`) + .set('Authorization', `Bearer ${soft}`) + .send({ performanceRatio: 60 }); + expect(reopened.status).toBe(200); + expect(reopened.body.data.performanceRatio).toBe(60); + const after = await request(app.getHttpServer()) + .get('/api/v1/payroll?yearMonth=2026-07') + .set('Authorization', `Bearer ${hr}`); + expect(after.status).toBe(200); + expect(after.body.data.run.status).toBe('DRAFT'); + }, 30000); + + it('shows funnel and spend, writes logs, and fills remaining ledgers', async () => { + const fail = await request(app.getHttpServer()) + .post('/api/v1/auth/login') + .send({ username: 'admin', password: 'wrong-password', agreeTerms: true }); + expect(fail.status).toBe(401); + + const employee = await login('employee', 'Demo@123456'); + const denied = await request(app.getHttpServer()) + .get('/api/v1/reports/bid-conversion') + .set('Authorization', `Bearer ${employee}`); + expect(denied.status).toBe(403); + + const report = await request(app.getHttpServer()) + .post('/api/v1/office/reports') + .set('Authorization', `Bearer ${employee}`) + .send({ periodType: 'DAY', date: '2026-08-29', content: '完成接口联调,明天对方案。', submit: true }); + expect(report.status).toBe(201); + expect(report.body.data.periodType).toBe('DAY'); + expect(report.body.data.content).toContain('接口联调'); + expect(report.body.data.status).toBe('SENT'); + expect(report.body.data.toUser.displayName).toBe('软件项目经理'); + expect(report.body.data.ccUser.displayName).toBe('吴软件'); + + const viewed = await request(app.getHttpServer()) + .get(`/api/v1/office/reports/${report.body.data.id}`) + .set('Authorization', `Bearer ${employee}`); + expect(viewed.status).toBe(200); + expect(viewed.body.data.content).toContain('明天对方案'); + + const owner = await login('owner', 'Demo@123456'); + const funnel = await request(app.getHttpServer()) + .get('/api/v1/reports/bid-conversion') + .set('Authorization', `Bearer ${owner}`); + expect(funnel.status).toBe(200); + expect(funnel.body.data[0].status).toBe('筛选'); + expect(funnel.body.data.some((r: { id: string; presalesSpend: number }) => r.id === 'won')).toBe(true); + expect(typeof funnel.body.data[0].presalesSpend).toBe('number'); + + const profit = await request(app.getHttpServer()) + .get('/api/v1/reports/project-profit') + .set('Authorization', `Bearer ${owner}`); + expect(profit.status).toBe(200); + + const depts = await request(app.getHttpServer()) + .get('/api/v1/departments') + .set('Authorization', `Bearer ${owner}`); + expect(Array.isArray(depts.body.data)).toBe(true); + + const admin = await login('admin', 'Admin@123456'); + const partyName = `E2E招标人-${Date.now()}`; + const party = await request(app.getHttpServer()) + .post('/api/v1/parties') + .set('Authorization', `Bearer ${admin}`) + .send({ + name: partyName, + creditNo: '91320000MA00E2E01X', + level: 'A', + contacts: [{ name: '李工', phone: '13900001111', title: '招标办' }], + }); + expect(party.status).toBe(201); + expect(party.body.data.creditNo).toBe('91320000MA00E2E01X'); + expect(party.body.data.contacts[0].name).toBe('李工'); + const partyId = party.body.data.id as string; + const partyPatched = await request(app.getHttpServer()) + .patch(`/api/v1/parties/${partyId}`) + .set('Authorization', `Bearer ${admin}`) + .send({ name: partyName, creditNo: '91320000MA00E2E02X', level: 'B', contacts: party.body.data.contacts }); + expect(partyPatched.body.data.creditNo).toBe('91320000MA00E2E02X'); + const partyView = await request(app.getHttpServer()) + .get(`/api/v1/parties/${partyId}`) + .set('Authorization', `Bearer ${admin}`); + expect(partyView.status).toBe(200); + expect(partyView.body.data.contacts).toHaveLength(1); + + const logs = await request(app.getHttpServer()) + .get('/api/v1/system/logs?pageSize=100') + .set('Authorization', `Bearer ${admin}`); + expect(logs.status).toBe(200); + const actions = (logs.body.data.items as { action: string }[]).map((i) => i.action); + expect(actions).toEqual(expect.arrayContaining(['LOGIN', 'LOGIN_FAIL', 'FORBIDDEN'])); + + const roles = await request(app.getHttpServer()) + .get('/api/v1/system/roles') + .set('Authorization', `Bearer ${admin}`); + const employeeRole = (roles.body.data.items as { id: string; code: string }[]).find((r) => r.code === 'employee'); + expect(employeeRole).toBeTruthy(); + const scoped = await request(app.getHttpServer()) + .patch(`/api/v1/system/roles/${employeeRole!.id}`) + .set('Authorization', `Bearer ${admin}`) + .send({ dataScope: 'DEPT' }); + expect(scoped.status).toBe(200); + expect(scoped.body.data.dataScope).toBe('DEPT'); + + const rules = await request(app.getHttpServer()) + .get('/api/v1/system/number-rules') + .set('Authorization', `Bearer ${admin}`); + expect(rules.status).toBe(200); + expect((rules.body.data as { code: string }[]).map((r) => r.code).sort()).toEqual(['BID', 'HT', 'PRJ']); + + const board = await request(app.getHttpServer()) + .get('/api/v1/resource-board') + .set('Authorization', `Bearer ${owner}`); + expect(board.status).toBe(200); + expect(board.body.data.items.length).toBeGreaterThan(0); + expect(board.body.data.items[0].load).toMatch(/idle|full|over/); + + const seal = await request(app.getHttpServer()) + .post('/api/v1/seals') + .set('Authorization', `Bearer ${admin}`) + .send({ name: '项目章', keeper: '行政' }); + expect(seal.status).toBe(201); + expect(seal.body.data.name).toBe('项目章'); + + const projects = await request(app.getHttpServer()) + .get('/api/v1/projects?pageSize=50') + .set('Authorization', `Bearer ${owner}`); + const projectId = (projects.body.data.items as { id: string }[])[0]?.id; + expect(projectId).toBeTruthy(); + + const purchase = await request(app.getHttpServer()) + .post('/api/v1/purchase-requests') + .set('Authorization', `Bearer ${owner}`) + .send({ title: '渲染节点硬盘', amount: 80, projectId }); + expect(purchase.status).toBe(201); + expect(purchase.body.data.status).toBe('PENDING'); + + const change = await request(app.getHttpServer()) + .post('/api/v1/project-changes') + .set('Authorization', `Bearer ${owner}`) + .send({ projectId, summary: '增加交底次数', amountDelta: 0 }); + expect(change.status).toBe(201); + expect(change.body.data.status).toBe('PENDING'); + + const camera = await request(app.getHttpServer()) + .post('/api/v1/registered-assets') + .set('Authorization', `Bearer ${owner}`) + .send({ kind: 'FIXED', name: `摄像机 ${Date.now()}`, code: `CAM-${Date.now()}` }); + expect(camera.status).toBe(201); + const asset = await request(app.getHttpServer()) + .post('/api/v1/assets') + .set('Authorization', `Bearer ${owner}`) + .send({ registeredAssetId: camera.body.data.id, projectId }); + expect(asset.status).toBe(201); + expect(asset.body.data.status).toBe('IN_USE'); + expect(asset.body.data.registeredAssetId || asset.body.data.registeredAsset?.id).toBe(camera.body.data.id); + + const finance = await login('finance', 'Demo@123456'); + const payment = await request(app.getHttpServer()) + .post('/api/v1/payments') + .set('Authorization', `Bearer ${finance}`) + .send({ kind: 'EXPENSE', amount: 88, remark: '阶段8演示打款' }); + expect(payment.status).toBe(201); + expect(payment.body.data.status).toBe('TODO'); + const paid = await request(app.getHttpServer()) + .post(`/api/v1/payments/${payment.body.data.id}/pay`) + .set('Authorization', `Bearer ${finance}`); + expect(paid.status).toBe(201); + expect(paid.body.data.status).toBe('PAID'); + }, 60000); + + it('occupies registry assets and office applies land on purchase occupancy calendar', async () => { + const owner = await login('owner', 'Demo@123456'); + const employee = await login('employee', 'Demo@123456'); + const stamp = Date.now(); + + const nameless = await request(app.getHttpServer()) + .post('/api/v1/assets') + .set('Authorization', `Bearer ${owner}`) + .send({ assetName: '手填器材' }); + expect(nameless.status).toBe(400); + + const laptop = await request(app.getHttpServer()) + .post('/api/v1/registered-assets') + .set('Authorization', `Bearer ${owner}`) + .send({ kind: 'DIGITAL', name: `渲染账号 ${stamp}`, code: `DIG-${stamp}` }); + expect(laptop.status).toBe(201); + + const useApply = await request(app.getHttpServer()) + .post('/api/v1/office/applies') + .set('Authorization', `Bearer ${employee}`) + .send({ + kind: 'ASSET_USE', + assetKind: 'DIGITAL', + registeredAssetId: laptop.body.data.id, + reason: '项目渲染', + endAt: '2026-09-15', + }); + expect(useApply.status).toBe(201); + expect(useApply.body.data.status).toBe('PENDING'); + + const useOk = await request(app.getHttpServer()) + .post(`/api/v1/office/applies/${useApply.body.data.id}/decide`) + .set('Authorization', `Bearer ${owner}`) + .send({ result: 'APPROVED' }); + expect(useOk.status).toBe(201); + expect(useOk.body.data.status).toBe('APPROVED'); + expect(useOk.body.data.occupancy?.status).toBe('IN_USE'); + + const buy = await request(app.getHttpServer()) + .post('/api/v1/office/applies') + .set('Authorization', `Bearer ${employee}`) + .send({ + kind: 'ASSET_BUY', + assetKind: 'FIXED', + title: '摄影灯套装', + amount: 1280, + reason: '补充灯光', + }); + expect(buy.status).toBe(201); + const buyOk = await request(app.getHttpServer()) + .post(`/api/v1/office/applies/${buy.body.data.id}/decide`) + .set('Authorization', `Bearer ${owner}`) + .send({ result: 'APPROVED' }); + expect(buyOk.status).toBe(201); + expect(buyOk.body.data.purchase?.status).toBe('APPROVED'); + + const car = await request(app.getHttpServer()) + .post('/api/v1/registered-assets') + .set('Authorization', `Bearer ${owner}`) + .send({ kind: 'FIXED', name: `商务车 ${stamp}`, code: `CAR-${stamp}` }); + expect(car.status).toBe(201); + const vehicle = await request(app.getHttpServer()) + .post('/api/v1/office/applies') + .set('Authorization', `Bearer ${employee}`) + .send({ + kind: 'VEHICLE', + registeredAssetId: car.body.data.id, + destination: '南京政务中心', + startAt: '2026-09-01', + endAt: '2026-09-02', + reason: '送标', + }); + expect(vehicle.status).toBe(201); + const vehicleOk = await request(app.getHttpServer()) + .post(`/api/v1/office/applies/${vehicle.body.data.id}/decide`) + .set('Authorization', `Bearer ${owner}`) + .send({ result: 'APPROVED' }); + expect(vehicleOk.status).toBe(201); + expect(vehicleOk.body.data.occupancy?.status).toBe('IN_USE'); + + const cal = await request(app.getHttpServer()) + .get('/api/v1/office/calendar') + .set('Authorization', `Bearer ${employee}`); + expect(cal.status).toBe(200); + const kinds = (cal.body.data as { kind: string; title: string }[]).map((i) => i.kind); + expect(kinds).toEqual(expect.arrayContaining(['VEHICLE'])); + }, 30000); +}); diff --git a/apps/api/test/jest-e2e.json b/apps/api/test/jest-e2e.json new file mode 100644 index 0000000..e9d912f --- /dev/null +++ b/apps/api/test/jest-e2e.json @@ -0,0 +1,9 @@ +{ + "moduleFileExtensions": ["js", "json", "ts"], + "rootDir": ".", + "testEnvironment": "node", + "testRegex": ".e2e-spec.ts$", + "transform": { + "^.+\\.(t|j)s$": "ts-jest" + } +} diff --git a/apps/api/tsconfig.build.json b/apps/api/tsconfig.build.json new file mode 100644 index 0000000..2c8f61f --- /dev/null +++ b/apps/api/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist", "**/*spec.ts", "prisma"] +} diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..4c9da5d --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "esModuleInterop": true, + "isolatedModules": true, + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2023", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": true, + "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "strictBindCallApply": true, + "noFallthroughCasesInSwitch": true + } +} diff --git a/apps/harmony/.gitignore b/apps/harmony/.gitignore new file mode 100644 index 0000000..bc632d9 --- /dev/null +++ b/apps/harmony/.gitignore @@ -0,0 +1,10 @@ +/node_modules +/.hvigor +**/build +**/.preview +**/.cxx +local.properties +oh_modules +.idea +*.hap +*.app diff --git a/apps/harmony/AppScope/app.json5 b/apps/harmony/AppScope/app.json5 new file mode 100644 index 0000000..66a44d6 --- /dev/null +++ b/apps/harmony/AppScope/app.json5 @@ -0,0 +1,10 @@ +{ + "app": { + "bundleName": "com.fengying.oa.mobile", + "vendor": "fengying", + "versionCode": 301000, + "versionName": "3.0.1", + "icon": "$media:app_icon", + "label": "$string:app_name" + } +} diff --git a/apps/harmony/AppScope/resources/base/element/string.json b/apps/harmony/AppScope/resources/base/element/string.json new file mode 100644 index 0000000..e9faa3b --- /dev/null +++ b/apps/harmony/AppScope/resources/base/element/string.json @@ -0,0 +1,5 @@ +{ + "string": [ + { "name": "app_name", "value": "风影智慧办公" } + ] +} diff --git a/apps/harmony/AppScope/resources/base/media/app_icon.png b/apps/harmony/AppScope/resources/base/media/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cea1702e1bf7dd98fdc85773c0647d134b597a0c GIT binary patch literal 4151 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 { + hilog.warn(0x0000, TAG, 'notify perm %{public}s', JSON.stringify(err)); + }); + pushService.getToken().then((token: string) => { + if (token) { + AppStorage.setOrCreate('pushToken', token); + hilog.info(0x0000, TAG, 'push token ok'); + } + }).catch((err: BusinessError) => { + hilog.error(0x0000, TAG, 'getToken %{public}d %{public}s', err.code, err.message); + }); + } + + onNewWant(want: Want): void { + if (want?.uri) { + AppStorage.setOrCreate('launchUri', want.uri); + } + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + windowStage.loadContent('pages/Index').then(() => { + this.askUserPerms(); + }).catch((err: BusinessError) => { + hilog.error(0x0000, TAG, 'loadContent %{public}s', JSON.stringify(err)); + }); + } + + private askUserPerms(): void { + const ctx = this.context as common.UIAbilityContext; + abilityAccessCtrl.createAtManager() + .requestPermissionsFromUser(ctx, PERMS) + .catch((err: BusinessError) => { + hilog.warn(0x0000, TAG, 'user perm %{public}s', JSON.stringify(err)); + }); + } +} diff --git a/apps/harmony/entry/src/main/ets/pages/Index.ets b/apps/harmony/entry/src/main/ets/pages/Index.ets new file mode 100644 index 0000000..2165550 --- /dev/null +++ b/apps/harmony/entry/src/main/ets/pages/Index.ets @@ -0,0 +1,65 @@ +import { webview } from '@kit.ArkWeb'; +import { hilog } from '@kit.PerformanceAnalysisKit'; + +const OA_URL = 'https://oa.fysxkj.com'; +const TAG = 'FengyingOA'; + +@Entry +@Component +struct Index { + controller: webview.WebviewController = new webview.WebviewController(); + @StorageLink('pushToken') pushToken: string = ''; + @StorageLink('launchUri') launchUri: string = ''; + + build() { + Column() { + Web({ src: OA_URL, controller: this.controller }) + .width('100%') + .layoutWeight(1) + .javaScriptAccess(true) + .domStorageAccess(true) + .fileAccess(true) + .geolocationAccess(true) + .mediaPlayGestureAccess(true) + .onGeolocationShow((event) => { + event.geolocation.invoke(event.origin, true, false); + }) + .onPermissionRequest((event) => { + event.request.grant(event.request.getAccessibleResource()); + }) + .onPageEnd(() => { + this.injectNative(); + }) + } + .width('100%') + .height('100%') + } + + private injectNative(): void { + const token = this.pushToken.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + const deep = this.launchUri.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + const js = ` + (function(){ + window.__fyPushToken='${token}'; + window.__fyPushVendor='huawei'; + window.__fyDeepLink='${deep}'; + function once(){ + var t=sessionStorage.getItem('accessToken'); + if(!t || !window.__fyPushToken) return; + if(window.__fyPushSent===t+window.__fyPushToken) return; + window.__fyPushSent=t+window.__fyPushToken; + fetch('/api/v1/push/devices',{ + method:'POST', + headers:{'Content-Type':'application/json','Authorization':'Bearer '+t}, + body:JSON.stringify({vendor:'huawei',regId:window.__fyPushToken,brand:'HarmonyOS',model:'phone'}) + }).catch(function(){}); + } + once(); + setInterval(once, 4000); + })(); + `; + this.controller.runJavaScript(js).catch(() => { + hilog.warn(0x0000, TAG, 'inject js fail'); + }); + } +} diff --git a/apps/harmony/entry/src/main/ets/pushability/PushMessageAbility.ets b/apps/harmony/entry/src/main/ets/pushability/PushMessageAbility.ets new file mode 100644 index 0000000..4cce4f0 --- /dev/null +++ b/apps/harmony/entry/src/main/ets/pushability/PushMessageAbility.ets @@ -0,0 +1,13 @@ +import { PushExtensionAbility, pushCommon } from '@kit.PushKit'; +import { hilog } from '@kit.PerformanceAnalysisKit'; + +const TAG = 'FengyingOA'; + +export default class PushMessageAbility extends PushExtensionAbility { + onReceiveMessage(message: pushCommon.PushPayload): void { + hilog.info(0x0000, TAG, 'push data %{public}s', JSON.stringify(message)); + } + + onDestroy(): void { + } +} diff --git a/apps/harmony/entry/src/main/module.json5 b/apps/harmony/entry/src/main/module.json5 new file mode 100644 index 0000000..44df847 --- /dev/null +++ b/apps/harmony/entry/src/main/module.json5 @@ -0,0 +1,87 @@ +{ + "module": { + "name": "entry", + "type": "entry", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + "deviceTypes": ["phone", "tablet"], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:app_icon", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:app_icon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": ["entity.system.home"], + "actions": ["ohos.want.action.home"] + }, + { + "entities": ["entity.system.browsable"], + "actions": ["ohos.want.action.viewData"], + "uris": [ + { "scheme": "fengyingoa", "host": "chat" }, + { "scheme": "fengyingoa", "host": "qr" } + ] + } + ] + } + ], + "extensionAbilities": [ + { + "name": "PushMessageAbility", + "srcEntry": "./ets/pushability/PushMessageAbility.ets", + "type": "push", + "exported": false, + "description": "华为推送" + } + ], + "requestPermissions": [ + { "name": "ohos.permission.INTERNET" }, + { "name": "ohos.permission.GET_NETWORK_INFO" }, + { + "name": "ohos.permission.CAMERA", + "reason": "$string:perm_camera", + "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } + }, + { + "name": "ohos.permission.MICROPHONE", + "reason": "$string:perm_mic", + "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } + }, + { + "name": "ohos.permission.APPROXIMATELY_LOCATION", + "reason": "$string:perm_location", + "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } + }, + { + "name": "ohos.permission.LOCATION", + "reason": "$string:perm_location", + "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } + }, + { + "name": "ohos.permission.READ_IMAGEVIDEO", + "reason": "$string:perm_media", + "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } + }, + { + "name": "ohos.permission.WRITE_IMAGEVIDEO", + "reason": "$string:perm_media", + "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } + } + ], + "metadata": [ + { + "name": "client_id", + "value": "6917615149085387132" + } + ] + } +} diff --git a/apps/harmony/entry/src/main/resources/base/element/color.json b/apps/harmony/entry/src/main/resources/base/element/color.json new file mode 100644 index 0000000..9b173cf --- /dev/null +++ b/apps/harmony/entry/src/main/resources/base/element/color.json @@ -0,0 +1,5 @@ +{ + "color": [ + { "name": "start_window_background", "value": "#5B4FE3" } + ] +} diff --git a/apps/harmony/entry/src/main/resources/base/element/string.json b/apps/harmony/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000..f56bd75 --- /dev/null +++ b/apps/harmony/entry/src/main/resources/base/element/string.json @@ -0,0 +1,11 @@ +{ + "string": [ + { "name": "module_desc", "value": "风影智慧办公鸿蒙客户端" }, + { "name": "EntryAbility_desc", "value": "风影智慧办公" }, + { "name": "EntryAbility_label", "value": "风影智慧办公" }, + { "name": "perm_camera", "value": "用于扫码登录、拍照上传附件" }, + { "name": "perm_mic", "value": "用于即时通讯语音消息" }, + { "name": "perm_location", "value": "用于出差打卡、地图选点" }, + { "name": "perm_media", "value": "用于选择和保存图片、文件" } + ] +} diff --git a/apps/harmony/entry/src/main/resources/base/media/app_icon.png b/apps/harmony/entry/src/main/resources/base/media/app_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cea1702e1bf7dd98fdc85773c0647d134b597a0c GIT binary patch literal 4151 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" + } +} diff --git a/apps/im-server/server.js b/apps/im-server/server.js new file mode 100644 index 0000000..2177e9e --- /dev/null +++ b/apps/im-server/server.js @@ -0,0 +1,324 @@ +#!/usr/bin/env node +/** + * MobileIMSDK TCP 服务端(官方协议): + * 4 字节大端长度 + UTF-8 JSON Protocal。 + * 登录 token 走 OA GET /auth/me。 + * 同一账号允许多端同时在线(手机 + 网页),重连不再把本机踢掉。 + */ +const net = require('net'); +const http = require('http'); +const crypto = require('crypto'); + +const TCP_PORT = Number(process.env.IM_TCP_PORT || 8901); +const HTTP_PORT = Number(process.env.IM_HTTP_PORT || 8902); +const OA_API = (process.env.OA_API_URL || 'http://127.0.0.1:3010/api/v1').replace(/\/$/, ''); +const INTERNAL = process.env.IM_INTERNAL_TOKEN || ''; +const MAX_BODY = 6 * 1024; + +const C = { + LOGIN: 0, + KEEP_ALIVE: 1, + COMMON_DATA: 2, + LOGOUT: 3, + RECIVED: 4, + ECHO: 5, +}; +const S = { + LOGIN: 50, + KEEP_ALIVE: 51, + ERROR: 52, + ECHO: 53, + KICKOUT: 54, +}; + +/** @type {Map>} */ +const online = new Map(); +/** @type {WeakMap} */ +const sessions = new WeakMap(); + +function frame(obj) { + const body = Buffer.from(JSON.stringify(obj), 'utf8'); + const header = Buffer.alloc(4); + header.writeUInt32BE(body.length, 0); + return Buffer.concat([header, body]); +} + +function send(socket, obj) { + if (!socket || socket.destroyed) return false; + try { + socket.write(frame(obj)); + return true; + } catch { + return false; + } +} + +function addOnline(userId, socket) { + let set = online.get(userId); + if (!set) { + set = new Set(); + online.set(userId, set); + } + set.add(socket); +} + +function removeSocket(userId, socket) { + const set = online.get(userId); + if (!set) return; + set.delete(socket); + if (set.size === 0) online.delete(userId); +} + +function sendToUser(userId, obj) { + const set = online.get(userId); + if (!set) return false; + let ok = false; + for (const s of set) ok = send(s, obj) || ok; + return ok; +} + +function protocal(type, dataContent, from, to, extra = {}) { + return { + bridge: false, + type, + dataContent, + from: from || '0', + to: to || '0', + fp: extra.fp || null, + QoS: Boolean(extra.QoS), + typeu: extra.typeu ?? -1, + sm: extra.sm ?? -1, + }; +} + +async function verifyLogin(userId, token) { + if (!userId || !token) return false; + try { + const res = await fetch(`${OA_API}/auth/me`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) return false; + const json = await res.json(); + const me = json.data || json; + return me && me.id === userId && me.status !== 'LEFT'; + } catch (e) { + console.warn('verify login', e.message); + return false; + } +} + +async function ingest(fromUserId, toUserId, body, fingerprint, extra = {}) { + if (!INTERNAL) return; + try { + await fetch(`${OA_API}/im/internal/ingest`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'x-im-token': INTERNAL }, + body: JSON.stringify({ + fromUserId, + toUserId, + kind: extra.kind || 'chat', + body, + fingerprint, + conversationId: extra.conversationId, + contentType: extra.contentType, + meta: extra.meta, + }), + }); + } catch (e) { + console.warn('ingest', e.message); + } +} + +async function membersOf(conversationId) { + if (!INTERNAL || !conversationId) return []; + try { + const res = await fetch(`${OA_API}/im/internal/members/${conversationId}`, { + headers: { 'x-im-token': INTERNAL }, + }); + if (!res.ok) return []; + const json = await res.json(); + const data = json.data || json; + return data.userIds || []; + } catch { + return []; + } +} + +async function handle(socket, p) { + const type = Number(p.type); + if (type === C.LOGIN) { + let info = {}; + try { + info = JSON.parse(p.dataContent || '{}'); + } catch { + info = {}; + } + const userId = info.loginUserId || p.from; + const ok = await verifyLogin(userId, info.loginToken); + if (!ok) { + send(socket, protocal(S.LOGIN, JSON.stringify({ code: 1, firstLoginTime: -1 }), '0', '-1')); + socket.end(); + return; + } + const firstLoginTime = Number(info.firstLoginTime) > 0 ? Number(info.firstLoginTime) : Date.now(); + const sess = sessions.get(socket) || { buf: Buffer.alloc(0) }; + sess.userId = userId; + sessions.set(socket, sess); + addOnline(userId, socket); + send(socket, protocal(S.LOGIN, JSON.stringify({ code: 0, firstLoginTime }), '0', userId)); + console.log(`login ok ${userId} sockets=${online.get(userId)?.size || 0} users=${online.size}`); + return; + } + + const sess = sessions.get(socket); + if (!sess?.userId) { + socket.end(); + return; + } + const userId = sess.userId; + + if (type === C.KEEP_ALIVE) { + send(socket, protocal(S.KEEP_ALIVE, '{}', '0', userId)); + return; + } + if (type === C.LOGOUT) { + removeSocket(userId, socket); + socket.end(); + return; + } + if (type === C.ECHO) { + send(socket, protocal(S.ECHO, p.dataContent || '', '0', userId)); + return; + } + if (type === C.RECIVED) { + return; + } + if (type === C.COMMON_DATA) { + const fp = p.fp || crypto.randomUUID(); + let parsed = {}; + try { + parsed = JSON.parse(p.dataContent || '{}'); + } catch { + parsed = {}; + } + const convId = parsed.conversationId || ''; + const typeu = Number(p.typeu ?? -1); + const payload = protocal(C.COMMON_DATA, p.dataContent, userId, p.to, { + QoS: p.QoS, + fp, + typeu: p.typeu, + sm: Date.now(), + }); + let targets = []; + if (typeu === 2 && convId) { + targets = (await membersOf(convId)).filter((id) => id !== userId); + } else if (p.to) { + targets = [p.to]; + } + for (const to of targets) { + sendToUser(to, { ...payload, to }); + } + if (p.QoS) { + send(socket, protocal(C.RECIVED, fp, p.to || '0', userId)); + } + if (typeu === 3 || parsed.kind === 'signal') return; + const text = String(parsed.text || parsed.title || p.dataContent || ''); + await ingest(userId, p.to, text, fp, { + kind: parsed.kind || 'chat', + conversationId: convId, + contentType: parsed.contentType, + meta: parsed.meta, + }); + } +} + +const tcp = net.createServer((socket) => { + sessions.set(socket, { buf: Buffer.alloc(0) }); + socket.on('data', (chunk) => { + const sess = sessions.get(socket); + if (!sess) return; + sess.buf = Buffer.concat([sess.buf, chunk]); + while (sess.buf.length >= 4) { + const len = sess.buf.readUInt32BE(0); + if (len <= 0 || len > MAX_BODY) { + socket.end(); + return; + } + if (sess.buf.length < 4 + len) break; + const body = sess.buf.subarray(4, 4 + len).toString('utf8'); + sess.buf = sess.buf.subarray(4 + len); + try { + void handle(socket, JSON.parse(body)); + } catch (e) { + console.warn('bad packet', e.message); + } + } + }); + socket.on('close', () => { + const sess = sessions.get(socket); + if (sess?.userId) removeSocket(sess.userId, socket); + }); + socket.on('error', () => {}); + socket.setTimeout(90000, () => socket.end()); +}); + +const httpServer = http.createServer((req, res) => { + const url = new URL(req.url || '/', 'http://127.0.0.1'); + if (req.method === 'GET' && url.pathname === '/health') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, online: online.size, tcp: TCP_PORT })); + return; + } + if (req.method === 'GET' && url.pathname === '/online') { + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ count: online.size, users: [...online.keys()] })); + return; + } + if (req.method === 'POST' && url.pathname === '/push') { + const token = req.headers['x-im-token']; + if (!INTERNAL || token !== INTERNAL) { + res.writeHead(401); + res.end('forbidden'); + return; + } + const chunks = []; + req.on('data', (c) => chunks.push(c)); + req.on('end', () => { + try { + const body = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}'); + const to = body.toUserId; + let parsed = {}; + try { + parsed = JSON.parse(body.dataContent || '{}'); + } catch { + parsed = {}; + } + const seq = Number(parsed.seq ?? body.seq ?? 0); + const delivered = sendToUser( + to, + protocal(C.COMMON_DATA, body.dataContent || '', '0', to, { + QoS: true, + fp: parsed.fingerprint || crypto.randomUUID(), + typeu: 1, + sm: seq > 0 ? seq : Date.now(), + }), + ); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, delivered })); + } catch (e) { + res.writeHead(400); + res.end(e.message); + } + }); + return; + } + res.writeHead(404); + res.end(); +}); + +tcp.listen(TCP_PORT, '0.0.0.0', () => { + console.log(`MobileIMSDK TCP ${TCP_PORT}`); +}); +httpServer.listen(HTTP_PORT, '127.0.0.1', () => { + console.log(`IM HTTP push ${HTTP_PORT}`); +}); diff --git a/apps/native/.gitignore b/apps/native/.gitignore new file mode 100644 index 0000000..81f47c7 --- /dev/null +++ b/apps/native/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/dist/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/apps/native/.metadata b/apps/native/.metadata new file mode 100644 index 0000000..ec911c6 --- /dev/null +++ b/apps/native/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "d8a9f9a52e5af486f80d932e838ee93861ffd863" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + - platform: android + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + - platform: linux + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + - platform: windows + create_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + base_revision: d8a9f9a52e5af486f80d932e838ee93861ffd863 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/apps/native/README.md b/apps/native/README.md new file mode 100644 index 0000000..2ee05b9 --- /dev/null +++ b/apps/native/README.md @@ -0,0 +1,55 @@ +# 风影办公 · 原生客户端 + +一套 Flutter 代码,Android APK + Windows exe。界面按微信 / 企业微信手机端重做,不嵌 OA 网页。 + +底栏:消息 · 工作台 · 通讯录 · 我。 + +- 消息:会话列表、未读红点、全部/未读/单聊/群聊筛选,待办和待审批钉在列表顶部 +- 工作台:应用宫格 + 待处理数字 + 新版本横幅 +- 通讯录:组织架构、按姓氏排序、点人发消息 +- 我:检查更新(OTA)、连接设置、退出 + +默认连接: + +- REST:`https://oa.fysxkj.com/api/v1` +- IM:`47.96.23.244:8901`(MobileIMSDK TCP) + +当前版本:`1.1.0`(build `2`)。 + +## Android + +```bash +flutter pub get +flutter build apk --release +``` + +产物:`build/app/outputs/flutter-apk/app-release.apk` + +发布到服务器后,用户打开 App 会自动检查升级;也可在「我 → 检查更新」手动检查。 + +```bash +# 把 APK 放到下载地址,并登记版本 +mkdir -p /data/oa-releases +cp build/app/outputs/flutter-apk/app-release.apk /data/oa-releases/fengying-oa.apk +# 管理员登录后: +# PATCH /api/v1/system/app-release +# { "version": "1.1.0", "build": 2, "url": "https://oa.fysxkj.com/fengying-oa.apk", "changelog": "..." } +``` + +公开接口 `GET /api/v1/system/app-release` 返回最新包信息。客户端比较 `build`,更大则提示下载并安装。 + +## Windows + +在 Windows 安装 Flutter 与 Visual Studio(Desktop development with C++)后: + +```bat +build-windows.bat +``` + +exe 在 `build\windows\x64\runner\Release\fengying_oa.exe`。 + +本 Linux 构建机不能交叉编译 Windows 桌面包。 + +## 登录前 init + +Android `MainActivity` 通过 `com.fysxkj.oa/im` 暴露 `init` / `release`,通过 `com.fysxkj.oa/ota` 安装下载好的 APK。 diff --git a/apps/native/WINDOWS_BUILD.md b/apps/native/WINDOWS_BUILD.md new file mode 100644 index 0000000..5adaa9e --- /dev/null +++ b/apps/native/WINDOWS_BUILD.md @@ -0,0 +1,9 @@ +# Windows 桌面端打包 + +打包机 `10.1.1.202`,SSH **端口 22022**,用户 `administrator`。 + +1. 安装工具链(只需一次):`installer\bootstrap-windows.ps1` +2. 在 `C:\fengying-oa-native` 执行 `.\build-windows.ps1` + +产物必须是可安装的 **`build\fengying-oa-setup.exe`**(Inno Setup),不要只发 zip。 +上传到 `https://oa.fysxkj.com/fengying-oa-setup.exe`,并写入 `apps/api/data/app-release.json` 的 `windowsUrl`。 diff --git a/apps/native/analysis_options.yaml b/apps/native/analysis_options.yaml new file mode 100644 index 0000000..c16dc22 --- /dev/null +++ b/apps/native/analysis_options.yaml @@ -0,0 +1,34 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +analyzer: + exclude: + - build/** + - android/** + - windows/** + - linux/** +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/apps/native/android/.gitignore b/apps/native/android/.gitignore new file mode 100644 index 0000000..55afd91 --- /dev/null +++ b/apps/native/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/apps/native/android/app/agconnect-services.json b/apps/native/android/app/agconnect-services.json new file mode 100644 index 0000000..4b57ff6 --- /dev/null +++ b/apps/native/android/app/agconnect-services.json @@ -0,0 +1 @@ +{"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.dbankcloud.cn","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-dre.hispace.hicloud.com","DE_back":"connect-dre.hispace.hicloud.com"},"client":{"cp_id":"10086000847744494","product_id":"101653523864901451","client_id":"2029248924690199744","client_secret":"2540EB569E83BF5271BC689A63FB0540832E4849A9608BB814D463EFA6754FA4","project_id":"101653523864901451","app_id":"118825899","api_key":"DgEDALOHkJa4k9Q93H74q1jSF7G3GWcDUQIqrJBqwJFC3Bcyrg+TEK1K5FdhJHh++SBQQ/QxRTdQXOO+XOq/8r5qdZ6rGi7ISmKJlw==","package_name":"com.fengying.oa.mobile"},"oauth_client":{"client_id":"118825899","client_type":1},"app_info":{"app_id":"118825899","package_name":"com.fengying.oa.mobile"},"configuration_version":"3.0","appInfos":[{"package_name":"com.fengying.oa.mobile","client":{"app_id":"118825899"},"oauth_client":{"client_id":"118825899","client_type":1},"app_info":{"app_id":"118825899","package_name":"com.fengying.oa.mobile"}}]} diff --git a/apps/native/android/app/build.gradle b/apps/native/android/app/build.gradle new file mode 100644 index 0000000..766b43c --- /dev/null +++ b/apps/native/android/app/build.gradle @@ -0,0 +1,68 @@ +plugins { + id "com.android.application" + id "kotlin-android" + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id "dev.flutter.flutter-gradle-plugin" +} + +def keystoreProperties = new Properties() +def keystorePropertiesFile = rootProject.file("key.properties") +if (keystorePropertiesFile.exists()) { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} + +android { + namespace = "com.fysxkj.fengying_oa" + compileSdk = flutter.compileSdkVersion + // LiveKit/WebRTC and the current Android plugins require NDK 27+. + // 28.2 is installed in the build environment and is backwards compatible. + ndkVersion = "28.2.13676358" + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17 + } + + defaultConfig { + applicationId = "com.fengying.oa.mobile" + minSdkVersion = 23 + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + signingConfigs { + release { + keyAlias = keystoreProperties["keyAlias"] + keyPassword = keystoreProperties["keyPassword"] + storeFile = keystoreProperties["storeFile"] ? file(keystoreProperties["storeFile"]) : null + storePassword = keystoreProperties["storePassword"] + } + } + + buildTypes { + release { + signingConfig = signingConfigs.release + proguardFiles getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" + } + } +} + +flutter { + source = "../.." +} + +dependencies { + implementation "vivo:vpush:4.1.5.0" + implementation "com.heytap.msp:push:3.0.0" + implementation files("libs/push-internal-5.0.5.aar") + implementation "com.google.code.gson:gson:2.10.1" + implementation "com.huawei.agconnect:agconnect-core:1.9.0.300" + implementation "com.huawei.hms:push:6.13.0.300" + implementation "com.alphacephei:vosk-android:0.3.75@aar" + implementation "net.java.dev.jna:jna:5.13.0@aar" +} diff --git a/apps/native/android/app/libs/push-internal-5.0.5.aar b/apps/native/android/app/libs/push-internal-5.0.5.aar new file mode 100644 index 0000000000000000000000000000000000000000..9cbf7bba7fdd03e90cab483d082d84d79fb04f82 GIT binary patch literal 100389 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 z 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 fCoY5$zO9VpC$#`?RyL4P96 + + + diff --git a/apps/native/android/app/src/main/AndroidManifest.xml b/apps/native/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..e40447e --- /dev/null +++ b/apps/native/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,176 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/native/android/app/src/main/assets/agconnect-services.json b/apps/native/android/app/src/main/assets/agconnect-services.json new file mode 100644 index 0000000..4b57ff6 --- /dev/null +++ b/apps/native/android/app/src/main/assets/agconnect-services.json @@ -0,0 +1 @@ +{"agcgw_all":{"SG":"connect-dra.dbankcloud.cn","SG_back":"connect-dra.hispace.hicloud.com","CN":"connect-drcn.dbankcloud.cn","CN_back":"connect-drcn.hispace.dbankcloud.cn","RU":"connect-drru.hispace.dbankcloud.ru","RU_back":"connect-drru.hispace.dbankcloud.cn","DE":"connect-dre.dbankcloud.cn","DE_back":"connect-dre.hispace.hicloud.com"},"websocketgw_all":{"SG":"connect-ws-dra.hispace.dbankcloud.cn","SG_back":"connect-ws-dra.hispace.dbankcloud.com","CN":"connect-ws-drcn.dbankcloud.cn","CN_back":"connect-ws-drcn.hispace.dbankcloud.com","RU":"connect-ws-drru.hispace.dbankcloud.ru","RU_back":"connect-ws-drru.hispace.dbankcloud.cn","DE":"connect-dre.hispace.hicloud.com","DE_back":"connect-dre.hispace.hicloud.com"},"client":{"cp_id":"10086000847744494","product_id":"101653523864901451","client_id":"2029248924690199744","client_secret":"2540EB569E83BF5271BC689A63FB0540832E4849A9608BB814D463EFA6754FA4","project_id":"101653523864901451","app_id":"118825899","api_key":"DgEDALOHkJa4k9Q93H74q1jSF7G3GWcDUQIqrJBqwJFC3Bcyrg+TEK1K5FdhJHh++SBQQ/QxRTdQXOO+XOq/8r5qdZ6rGi7ISmKJlw==","package_name":"com.fengying.oa.mobile"},"oauth_client":{"client_id":"118825899","client_type":1},"app_info":{"app_id":"118825899","package_name":"com.fengying.oa.mobile"},"configuration_version":"3.0","appInfos":[{"package_name":"com.fengying.oa.mobile","client":{"app_id":"118825899"},"oauth_client":{"client_id":"118825899","client_type":1},"app_info":{"app_id":"118825899","package_name":"com.fengying.oa.mobile"}}]} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/Coord.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/Coord.kt new file mode 100644 index 0000000..c0b83f1 --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/Coord.kt @@ -0,0 +1,43 @@ +package com.fysxkj.fengying_oa + +import kotlin.math.PI +import kotlin.math.sin +import kotlin.math.sqrt + +/** 国内地图(腾讯/高德)用 GCJ-02。卫星 GPS 是 WGS-84,不转换会偏三四百米。 */ +object Coord { + private const val A = 6378245.0 + private const val EE = 0.00669342162296594323 + + fun outOfChina(lat: Double, lng: Double): Boolean { + return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271 + } + + fun wgs84ToGcj02(lat: Double, lng: Double): Pair { + if (outOfChina(lat, lng)) return lat to lng + var dLat = transformLat(lng - 105.0, lat - 35.0) + var dLng = transformLng(lng - 105.0, lat - 35.0) + val radLat = lat / 180.0 * PI + var magic = 1 - EE * sin(radLat) * sin(radLat) + val sqrtMagic = sqrt(magic) + dLat = dLat * 180.0 / ((A * (1 - EE)) / (magic * sqrtMagic) * PI) + dLng = dLng * 180.0 / (A / sqrtMagic * Math.cos(radLat) * PI) + return (lat + dLat) to (lng + dLng) + } + + private fun transformLat(x: Double, y: Double): Double { + var ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(Math.abs(x)) + ret += (20.0 * sin(6.0 * x * PI) + 20.0 * sin(2.0 * x * PI)) * 2.0 / 3.0 + ret += (20.0 * sin(y * PI) + 40.0 * sin(y / 3.0 * PI)) * 2.0 / 3.0 + ret += (160.0 * sin(y / 12.0 * PI) + 320 * sin(y * PI / 30.0)) * 2.0 / 3.0 + return ret + } + + private fun transformLng(x: Double, y: Double): Double { + var ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(Math.abs(x)) + ret += (20.0 * sin(6.0 * x * PI) + 20.0 * sin(2.0 * x * PI)) * 2.0 / 3.0 + ret += (20.0 * sin(x * PI) + 40.0 * sin(x / 3.0 * PI)) * 2.0 / 3.0 + ret += (150.0 * sin(x / 12.0 * PI) + 300.0 * sin(x / 30.0 * PI)) * 2.0 / 3.0 + return ret + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/FengyingApp.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/FengyingApp.kt new file mode 100644 index 0000000..01afdcd --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/FengyingApp.kt @@ -0,0 +1,104 @@ +package com.fysxkj.fengying_oa + +import android.app.Application +import android.util.Log +import com.heytap.msp.push.HeytapPushManager +import com.vivo.push.IPushActionListener +import com.vivo.push.PushClient +import com.vivo.push.PushConfig +import com.vivo.push.listener.IPushQueryActionListener +import com.vivo.push.util.VivoPushException + +class FengyingApp : Application() { + override fun onCreate() { + super.onCreate() + OemAdapt.ensureChannels(this) + OppoPushCallback.appCtx = applicationContext + initVivo() + initOppo() + initMeizu() + initHuawei() + } + + private fun initVivo() { + try { + val config = PushConfig.Builder().agreePrivacyStatement(true).build() + PushClient.getInstance(this).initialize(config) + PushClient.getInstance(applicationContext).turnOnPush( + object : IPushActionListener { + override fun onStateChanged(state: Int) { + if (state != 0) { + Log.w(TAG, "vivo turnOnPush $state") + return + } + PushClient.getInstance(applicationContext).getRegId( + object : IPushQueryActionListener { + override fun onSuccess(regid: String?) { + if (!regid.isNullOrBlank()) PushStore.saveReg(this@FengyingApp, regid, "vivo") + } + + override fun onFail(errorCode: Int?) { + Log.w(TAG, "vivo getRegId $errorCode") + } + }, + ) + } + }, + ) + } catch (e: VivoPushException) { + Log.e(TAG, "vivo initialize", e) + } catch (e: Throwable) { + Log.e(TAG, "vivo initialize", e) + } + } + + private fun initOppo() { + try { + HeytapPushManager.init(this, false) + if (!HeytapPushManager.isSupportPush(this)) return + HeytapPushManager.register(this, OPPO_APP_KEY, OPPO_APP_SECRET, OppoPushCallback()) + } catch (e: Throwable) { + Log.e(TAG, "oppo initialize", e) + } + } + + private fun initMeizu() { + if (PushStore.detectVendor() != "meizu") return + try { + com.meizu.cloud.pushsdk.PushManager.register(this, MEIZU_APP_ID, MEIZU_APP_KEY) + } catch (e: Throwable) { + Log.e(TAG, "meizu initialize", e) + } + } + + private fun initHuawei() { + if (!PushStore.isHuaweiFamily()) return + Thread { + try { + runCatching { + if (com.huawei.agconnect.AGConnectInstance.getInstance() == null) { + com.huawei.agconnect.AGConnectInstance.initialize(this) + } + } + val token = com.huawei.hms.aaid.HmsInstanceId.getInstance(this).getToken(HUAWEI_APP_ID, "HCM") + if (!token.isNullOrBlank()) { + PushStore.saveReg(this, token, "huawei") + Log.i(TAG, "huawei token ok") + } else { + Log.w(TAG, "huawei token empty") + } + } catch (e: Throwable) { + Log.e(TAG, "huawei initialize", e) + } + }.start() + } + + companion object { + private const val TAG = "FengyingPush" + private const val OPPO_APP_KEY = "e23832b52fbb46e18de6eef5affda2e9" + private const val OPPO_APP_SECRET = "e9cf0bf35052484ebcaf15d56576468b" + private const val MEIZU_APP_ID = "157643" + private const val MEIZU_APP_KEY = "4263a8d18de442b6a17203866ce1546d" + private const val HUAWEI_APP_ID = "118825899" + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/HuaweiPushService.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/HuaweiPushService.kt new file mode 100644 index 0000000..5bd8e16 --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/HuaweiPushService.kt @@ -0,0 +1,32 @@ +package com.fysxkj.fengying_oa + +import android.os.Bundle +import android.util.Log +import com.huawei.hms.push.HmsMessageService +import com.huawei.hms.push.RemoteMessage + +class HuaweiPushService : HmsMessageService() { + override fun onNewToken(token: String?) { + saveToken(token) + } + + override fun onNewToken(token: String?, bundle: Bundle?) { + saveToken(token) + } + + override fun onMessageReceived(message: RemoteMessage?) { + val data = message?.dataOfMap ?: return + if (data.isEmpty()) return + Log.i(TAG, "huawei data ${data.keys}") + } + + private fun saveToken(token: String?) { + if (token.isNullOrBlank()) return + PushStore.saveReg(this, token, "huawei") + Log.i(TAG, "huawei token ok") + } + + companion object { + private const val TAG = "FengyingPush" + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/MainActivity.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/MainActivity.kt new file mode 100644 index 0000000..4567855 --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/MainActivity.kt @@ -0,0 +1,926 @@ +package com.fysxkj.fengying_oa + +import android.Manifest +import android.app.NotificationManager +import android.app.PendingIntent +import android.content.ClipData +import android.content.ContentValues +import android.content.Intent +import android.content.pm.PackageManager +import android.database.ContentObserver +import android.location.LocationManager +import android.media.MediaPlayer +import android.media.AudioAttributes +import android.media.MediaRecorder +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.provider.MediaStore +import android.provider.OpenableColumns +import android.provider.Settings +import androidx.core.app.NotificationCompat +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodChannel +import java.io.File +import java.io.FileOutputStream + +class MainActivity : FlutterActivity() { + private val imChannel = "com.fysxkj.oa/im" + private val otaChannel = "com.fysxkj.oa/ota" + private val deviceEvents = "com.fysxkj.oa/device" + private val pushEvents = "com.fysxkj.oa/push" + private var initialized = false + private var screenshotSink: EventChannel.EventSink? = null + private var pushSink: EventChannel.EventSink? = null + private var pendingPush: Map? = null + private var watching = false + private var lastShotAt = 0L + private val mainHandler = Handler(Looper.getMainLooper()) + private var mediaObserver: ContentObserver? = null + private var captureCallback: (() -> Unit)? = null + + private var recorder: MediaRecorder? = null + private var recordFile: File? = null + private var player: MediaPlayer? = null + private var callTone: MediaPlayer? = null + private var pickResult: MethodChannel.Result? = null + private var locResult: MethodChannel.Result? = null + private var voiceStartResult: MethodChannel.Result? = null + private var locListener: android.location.LocationListener? = null + private var locTimeout: Runnable? = null + private var callMediaResult: MethodChannel.Result? = null + private var bestGps: android.location.Location? = null + private var bestNet: android.location.Location? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + prepareIncomingCallWindow(intent) + stashPush(intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + prepareIncomingCallWindow(intent) + stashPush(intent) + emitPush(intent) + } + + override fun onPostResume() { + super.onPostResume() + preferMaxRefresh() + } + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, imChannel) + .setMethodCallHandler { call, result -> + when (call.method) { + "init" -> { + initialized = true + result.success( + mapOf( + "ok" to true, + "initialized" to true, + "engine" to "dart-tcp", + ), + ) + } + "release" -> { + initialized = false + result.success(mapOf("ok" to true)) + } + "engine" -> result.success(if (initialized) "dart-tcp" else "idle") + else -> result.notImplemented() + } + } + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, otaChannel) + .setMethodCallHandler { call, result -> + when (call.method) { + "installApk" -> { + val path = call.argument("path") + if (path.isNullOrBlank()) { + result.error("bad_path", "缺少安装包路径", null) + return@setMethodCallHandler + } + try { + installApk(path) + result.success(true) + } catch (e: Exception) { + result.error("install_failed", e.message, null) + } + } + "preferMaxRefresh" -> { + preferMaxRefresh() + result.success(true) + } + "startScreenshotWatch" -> { + startScreenshotWatch() + result.success(true) + } + "stopScreenshotWatch" -> { + stopScreenshotWatch() + result.success(true) + } + "startVoice" -> startVoice(result) + "stopVoice" -> stopVoice(result, cancel = false) + "cancelVoice" -> stopVoice(result, cancel = true) + "playVoice" -> { + val path = call.argument("path") + if (path.isNullOrBlank()) { + result.error("bad_path", "缺少音频路径", null) + return@setMethodCallHandler + } + playVoice(path, result) + } + "stopPlay" -> { + stopPlay() + result.success(true) + } + "startCallTone" -> { + startCallTone(call.argument("incoming") == true) + result.success(true) + } + "stopCallTone" -> { + stopCallTone() + result.success(true) + } + "cancelCallNotification" -> { + cancelCallNotification() + result.success(true) + } + "playMessageSent" -> { + playBundledSound(R.raw.fengying_send) + result.success(true) + } + "showOnlineNotification" -> { + val title = call.argument("title") ?: "风影办公" + val content = call.argument("content") ?: "您有一条新消息" + @Suppress("UNCHECKED_CAST") + val extras = (call.argument>("extras") ?: emptyMap()).mapValues { "${it.value}" } + showOnlineNotification(title, content, extras) + result.success(true) + } + "speechToText" -> { + val path = call.argument("path") + if (path.isNullOrBlank()) { + result.success(null) + return@setMethodCallHandler + } + Thread { + val text = + try { + VoiceAsr.recognize(applicationContext, path) + } catch (_: Exception) { + null + } + mainHandler.post { result.success(text) } + }.start() + } + "pickMedia" -> pickContent(result, "image/*", arrayOf("image/gif", "image/png", "image/jpeg", "image/webp")) + "pickFile" -> pickContent(result, "*/*", null) + "getLocation" -> getLocation(result) + "openFile" -> { + val path = call.argument("path") + val mime = call.argument("mime") + if (path.isNullOrBlank()) { + result.error("bad_path", "缺少文件路径", null) + return@setMethodCallHandler + } + try { + openFile(path, mime) + result.success(true) + } catch (e: Exception) { + result.error("open_failed", e.message, null) + } + } + "saveImage" -> { + val path = call.argument("path") + if (path.isNullOrBlank()) { + result.error("bad_path", "缺少图片路径", null) + return@setMethodCallHandler + } + try { + saveImage(path) + result.success(true) + } catch (e: Exception) { + result.error("save_failed", e.message, null) + } + } + "openUrl" -> { + val url = call.argument("url") + if (url.isNullOrBlank()) { + result.error("bad_url", "缺少链接", null) + return@setMethodCallHandler + } + try { + openUrl(url) + result.success(true) + } catch (e: Exception) { + result.error("open_failed", e.message, null) + } + } + "getPushToken" -> result.success(PushStore.token(this)) + "consumeLaunchPush" -> { + val hit = pendingPush + pendingPush = null + result.success(hit) + } + "requestPushPermission", "requestFirstLoginPermissions" -> { + requestNotifyPermission() + result.success(true) + } + "requestCallMedia" -> requestCallMedia(result) + "oemStatus" -> { + val map = OemAdapt.status().toMutableMap() + map["batteryIgnored"] = OemAdapt.batteryIgnored(this) + map["prompted"] = PushStore.oemPrompted(this) + result.success(map) + } + "openOemKeepAlive" -> { + OemAdapt.openKeepAlive(this) + result.success(true) + } + "requestBattery" -> { + OemAdapt.requestBattery(this) + result.success(true) + } + "markOemPrompted" -> { + PushStore.markOemPrompted(this) + result.success(true) + } + else -> result.notImplemented() + } + } + EventChannel(flutterEngine.dartExecutor.binaryMessenger, deviceEvents) + .setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + screenshotSink = events + } + + override fun onCancel(arguments: Any?) { + screenshotSink = null + } + }, + ) + EventChannel(flutterEngine.dartExecutor.binaryMessenger, pushEvents) + .setStreamHandler( + object : EventChannel.StreamHandler { + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + pushSink = events + pendingPush?.let { events?.success(it) } + } + + override fun onCancel(arguments: Any?) { + pushSink = null + } + }, + ) + preferMaxRefresh() + } + + private fun requestNotifyPermission() { + if (Build.VERSION.SDK_INT < 33) return + if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == + PackageManager.PERMISSION_GRANTED + ) { + return + } + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), REQ_NOTIFY) + } + + private fun requestCallMedia(result: MethodChannel.Result) { + val need = mutableListOf() + if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { + need.add(Manifest.permission.RECORD_AUDIO) + } + if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { + need.add(Manifest.permission.CAMERA) + } + if (need.isEmpty()) { + result.success(true) + return + } + callMediaResult = result + ActivityCompat.requestPermissions(this, need.toTypedArray(), REQ_CALL) + } + + private fun stashPush(intent: Intent?) { + val extras = PushStore.extrasOf(intent) + if (extras.isNotEmpty()) pendingPush = extras + } + + private fun emitPush(intent: Intent?) { + val extras = PushStore.extrasOf(intent) + if (extras.isEmpty()) return + mainHandler.post { pushSink?.success(extras) } + } + + private fun preferMaxRefresh() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return + try { + val display = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) display else windowManager.defaultDisplay + val modes = display?.supportedModes ?: return + val best = modes.maxByOrNull { it.refreshRate } ?: return + val params = window.attributes + if (params.preferredDisplayModeId != best.modeId) { + params.preferredDisplayModeId = best.modeId + window.attributes = params + } + } catch (_: Exception) { + } + } + + private fun emitScreenshot() { + val now = System.currentTimeMillis() + if (now - lastShotAt < 1500) return + lastShotAt = now + mainHandler.post { screenshotSink?.success("screenshot") } + } + + private fun startScreenshotWatch() { + if (watching) return + watching = true + requestMediaRead() + if (Build.VERSION.SDK_INT >= 34) { + val cb = android.app.Activity.ScreenCaptureCallback { emitScreenshot() } + captureCallback = { unregisterScreenCaptureCallback(cb) } + registerScreenCaptureCallback(mainExecutor, cb) + } + val observer = + object : ContentObserver(mainHandler) { + override fun onChange(selfChange: Boolean, uri: Uri?) { + super.onChange(selfChange, uri) + mainHandler.post { inspectScreenshot(uri) } + } + } + mediaObserver = observer + try { + contentResolver.registerContentObserver(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true, observer) + } catch (_: Exception) { + } + } + + private fun requestMediaRead() { + val perm = + if (Build.VERSION.SDK_INT >= 33) Manifest.permission.READ_MEDIA_IMAGES + else Manifest.permission.READ_EXTERNAL_STORAGE + if (ContextCompat.checkSelfPermission(this, perm) == PackageManager.PERMISSION_GRANTED) return + ActivityCompat.requestPermissions(this, arrayOf(perm), REQ_MEDIA) + } + + private fun inspectScreenshot(uri: Uri?) { + val uriText = uri?.toString()?.lowercase() ?: "" + if (uriText.contains("screenshot") || uriText.contains("screen_shot") || uriText.contains("截屏")) { + emitScreenshot() + return + } + try { + val projection = + arrayOf( + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.RELATIVE_PATH, + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.DATE_ADDED, + ) + val target = uri ?: MediaStore.Images.Media.EXTERNAL_CONTENT_URI + contentResolver.query(target, projection, null, null, "${MediaStore.Images.Media.DATE_ADDED} DESC")?.use { c -> + if (!c.moveToFirst()) return + val added = if (c.columnCount > 3) c.getLong(3) else 0L + if (added > 0 && System.currentTimeMillis() / 1000 - added > 8) return + val blob = "${c.getString(0) ?: ""} ${c.getString(1) ?: ""} ${if (c.columnCount > 2) c.getString(2) ?: "" else ""}".lowercase() + if ( + blob.contains("screenshot") || + blob.contains("screen_shot") || + blob.contains("截屏") || + blob.contains("截圖") || + blob.contains("screen capture") + ) { + emitScreenshot() + } + } + } catch (_: Exception) { + } + } + + private fun stopScreenshotWatch() { + watching = false + if (Build.VERSION.SDK_INT >= 34) { + try { + captureCallback?.invoke() + } catch (_: Exception) { + } + captureCallback = null + } + mediaObserver?.let { + try { + contentResolver.unregisterContentObserver(it) + } catch (_: Exception) { + } + } + mediaObserver = null + } + + /** Keep a call visible over the lock screen when the user enters through an OEM push. */ + private fun prepareIncomingCallWindow(intent: Intent?) { + val extras = PushStore.extrasOf(intent) + if (extras["kind"] != "call" || extras["action"] in setOf("end", "reject", "timeout")) return + try { + setShowWhenLocked(true) + setTurnScreenOn(true) + window.addFlags(android.view.WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } catch (_: Exception) { + } + } + + private fun startCallTone(incoming: Boolean) { + if (callTone?.isPlaying == true) return + stopCallTone() + try { + val res = if (incoming) R.raw.fengying_incoming else R.raw.fengying_dial + callTone = MediaPlayer.create(this, res)?.apply { + isLooping = true + setAudioAttributes( + AudioAttributes.Builder().setUsage( + if (incoming) AudioAttributes.USAGE_NOTIFICATION_RINGTONE else AudioAttributes.USAGE_VOICE_COMMUNICATION_SIGNALLING, + ).setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION).build(), + ) + start() + } + } catch (_: Exception) { + stopCallTone() + } + } + + private fun stopCallTone() { + try { callTone?.stop() } catch (_: Exception) {} + try { callTone?.release() } catch (_: Exception) {} + callTone = null + } + + private fun playBundledSound(res: Int) { + try { + MediaPlayer.create(this, res)?.apply { + setOnCompletionListener { it.release() } + start() + } + } catch (_: Exception) { + } + } + + private fun notificationIntent(extras: Map, requestCode: Int): PendingIntent { + val i = Intent(this, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + extras.forEach { (k, v) -> putExtra(k, v) } + data = Uri.parse("fengyingoa://chat/open?$requestCode") + } + return PendingIntent.getActivity( + this, + requestCode, + i, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE, + ) + } + + private fun showOnlineNotification(title: String, content: String, extras: Map) { + val isCall = extras["kind"] == "call" && extras["action"] !in setOf("end", "reject", "timeout") + val id = if (isCall) CALL_NOTICE_ID else (System.currentTimeMillis() % 100000).toInt() + val intent = notificationIntent(extras, id) + val b = NotificationCompat.Builder(this, if (isCall) PushStore.CHANNEL_CALL else PushStore.CHANNEL_IM) + .setSmallIcon(android.R.drawable.sym_action_chat) + .setContentTitle(title) + .setContentText(content) + .setAutoCancel(!isCall) + .setContentIntent(intent) + .setPriority(if (isCall) NotificationCompat.PRIORITY_MAX else NotificationCompat.PRIORITY_HIGH) + .setCategory(if (isCall) NotificationCompat.CATEGORY_CALL else NotificationCompat.CATEGORY_MESSAGE) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + if (isCall) { + b.setOngoing(true).setFullScreenIntent(intent, true) + } + val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + nm.notify(id, b.build()) + } + + private fun cancelCallNotification() { + val nm = getSystemService(NOTIFICATION_SERVICE) as NotificationManager + nm.cancel(CALL_NOTICE_ID) + } + + private fun hasMic(): Boolean { + return ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == + PackageManager.PERMISSION_GRANTED + } + + private fun startVoice(result: MethodChannel.Result) { + if (!hasMic()) { + voiceStartResult = result + ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.RECORD_AUDIO), REQ_MIC) + return + } + beginRecord(result) + } + + private fun beginRecord(result: MethodChannel.Result) { + try { + stopPlay() + recorder?.release() + val file = File(cacheDir, "voice_${System.currentTimeMillis()}.m4a") + recordFile = file + val rec = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + MediaRecorder(this) + } else { + @Suppress("DEPRECATION") + MediaRecorder() + } + rec.setAudioSource(MediaRecorder.AudioSource.MIC) + rec.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) + rec.setAudioEncoder(MediaRecorder.AudioEncoder.AAC) + rec.setAudioSamplingRate(16000) + rec.setAudioEncodingBitRate(64000) + rec.setAudioChannels(1) + rec.setOutputFile(file.absolutePath) + rec.prepare() + rec.start() + recorder = rec + result.success(true) + } catch (e: Exception) { + recorder?.release() + recorder = null + recordFile?.delete() + recordFile = null + result.error("record_failed", e.message ?: "无法开始录音", null) + } + } + + private fun stopVoice(result: MethodChannel.Result, cancel: Boolean) { + val file = recordFile + try { + recorder?.stop() + } catch (_: Exception) { + } + try { + recorder?.release() + } catch (_: Exception) { + } + recorder = null + recordFile = null + if (cancel || file == null || !file.exists() || file.length() < 200) { + file?.delete() + result.success(null) + return + } + result.success(mapOf("path" to file.absolutePath, "name" to file.name)) + } + + private fun playVoice(path: String, result: MethodChannel.Result) { + try { + stopPlay() + val p = MediaPlayer() + p.setDataSource(path) + p.setOnCompletionListener { + stopPlay() + } + p.prepare() + p.start() + player = p + result.success(true) + } catch (e: Exception) { + stopPlay() + result.error("play_failed", e.message ?: "无法播放", null) + } + } + + private fun stopPlay() { + try { + player?.stop() + } catch (_: Exception) { + } + try { + player?.release() + } catch (_: Exception) { + } + player = null + } + + private fun pickContent(result: MethodChannel.Result, type: String, extras: Array?) { + pickResult = result + val intent = + Intent(Intent.ACTION_GET_CONTENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + this.type = type + if (extras != null) putExtra(Intent.EXTRA_MIME_TYPES, extras) + } + startActivityForResult(Intent.createChooser(intent, "选择文件"), REQ_PICK) + } + + private fun hasLoc(): Boolean { + return ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == + PackageManager.PERMISSION_GRANTED || + ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) == + PackageManager.PERMISSION_GRANTED + } + + private fun getLocation(result: MethodChannel.Result) { + if (!hasLoc()) { + locResult = result + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION), + REQ_LOC, + ) + return + } + emitLocation(result) + } + + @Suppress("MissingPermission") + private fun emitLocation(result: MethodChannel.Result) { + try { + val lm = getSystemService(LOCATION_SERVICE) as LocationManager + stopLocUpdates(lm) + locResult = result + bestGps = null + bestNet = null + val listener = + android.location.LocationListener { loc -> + if (loc.provider == LocationManager.GPS_PROVIDER) { + bestGps = loc + if (loc.accuracy <= 50f) finishLoc(lm, loc) + } else { + val cur = bestNet + if (cur == null || loc.accuracy < cur.accuracy) bestNet = loc + } + } + locListener = listener + if (lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) { + lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0L, 0f, listener, Looper.getMainLooper()) + } + if (lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { + lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 0L, 0f, listener, Looper.getMainLooper()) + } + val timeout = + Runnable { + val gps = bestGps + val net = bestNet + val last = bestLastLocation(lm) + val chosen = + when { + gps != null -> gps + net != null -> net + last != null -> last + else -> null + } + if (chosen != null) finishLoc(lm, chosen) + else { + stopLocUpdates(lm) + val cb = locResult + locResult = null + cb?.error("no_location", "没有拿到定位,请到开阔处打开系统定位后再试", null) + } + } + locTimeout = timeout + mainHandler.postDelayed(timeout, 16000) + } catch (e: Exception) { + result.error("no_location", e.message ?: "定位失败", null) + } + } + + @Suppress("MissingPermission") + private fun bestLastLocation(lm: LocationManager): android.location.Location? { + val providers = listOf(LocationManager.GPS_PROVIDER, LocationManager.NETWORK_PROVIDER, LocationManager.PASSIVE_PROVIDER) + var best: android.location.Location? = null + for (p in providers) { + val loc = + try { + lm.getLastKnownLocation(p) + } catch (_: Exception) { + null + } ?: continue + if (best == null) { + best = loc + continue + } + val newer = loc.time - best.time > 15_000 + val moreAccurate = loc.accuracy + 8f < best.accuracy + if (newer || moreAccurate) best = loc + } + return best + } + + @Suppress("MissingPermission") + private fun finishLoc(lm: LocationManager, loc: android.location.Location) { + stopLocUpdates(lm) + val cb = locResult + locResult = null + cb?.success(locMap(loc)) + } + + @Suppress("MissingPermission") + private fun stopLocUpdates(lm: LocationManager) { + locTimeout?.let { mainHandler.removeCallbacks(it) } + locTimeout = null + locListener?.let { + try { + lm.removeUpdates(it) + } catch (_: Exception) { + } + } + locListener = null + } + + private fun locMap(loc: android.location.Location): Map { + var lat = loc.latitude + var lng = loc.longitude + val p = loc.provider ?: "" + if (p != LocationManager.NETWORK_PROVIDER) { + val gcj = Coord.wgs84ToGcj02(lat, lng) + lat = gcj.first + lng = gcj.second + } + return mapOf( + "lat" to lat, + "lng" to lng, + "accuracy" to loc.accuracy.toDouble(), + ) + } + + private fun openFile(path: String, mime: String?) { + val file = File(path) + if (!file.exists()) throw IllegalStateException("文件不存在") + val uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file) + val mimeType = if (mime.isNullOrBlank()) "*/*" else mime + val view = + Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, mimeType) + clipData = ClipData.newRawUri("", uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + val matches = packageManager.queryIntentActivities(view, PackageManager.MATCH_DEFAULT_ONLY) + for (ri in matches) { + grantUriPermission(ri.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + if (matches.isNotEmpty()) { + startActivity(Intent.createChooser(view, "打开文件").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + return + } + val send = + Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, uri) + setType(mimeType) + clipData = ClipData.newRawUri("", uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + val sendMatches = packageManager.queryIntentActivities(send, PackageManager.MATCH_DEFAULT_ONLY) + for (ri in sendMatches) { + grantUriPermission(ri.activityInfo.packageName, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + if (sendMatches.isEmpty()) { + throw IllegalStateException("手机上没有能打开此文件的应用,请先安装 WPS 或 Office") + } + startActivity(Intent.createChooser(send, "打开文件").addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)) + } + + private fun saveImage(path: String) { + val file = File(path) + if (!file.exists()) throw IllegalStateException("图片不存在") + val values = + ContentValues().apply { + put(MediaStore.Images.Media.DISPLAY_NAME, "fy_${System.currentTimeMillis()}.jpg") + put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") + } + val uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) + ?: throw IllegalStateException("无法写入相册") + contentResolver.openOutputStream(uri)?.use { out -> + file.inputStream().use { it.copyTo(out) } + } ?: throw IllegalStateException("无法写入相册") + } + + private fun openUrl(url: String) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + startActivity(intent) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + if (requestCode != REQ_PICK) return + val cb = pickResult + pickResult = null + if (cb == null) return + if (resultCode != RESULT_OK || data?.data == null) { + cb.success(null) + return + } + try { + cb.success(copyUri(data.data!!)) + } catch (e: Exception) { + cb.error("pick_failed", e.message, null) + } + } + + private fun copyUri(uri: Uri): Map { + var name = "sticker.gif" + contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)?.use { c -> + if (c.moveToFirst()) { + val i = c.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (i >= 0) name = c.getString(i) ?: name + } + } + val mime = contentResolver.getType(uri) ?: "" + val safe = name.replace(Regex("[^a-zA-Z0-9._-]"), "_") + val out = File(cacheDir, "pick_${System.currentTimeMillis()}_$safe") + contentResolver.openInputStream(uri)?.use { input -> + FileOutputStream(out).use { input.copyTo(it) } + } ?: throw IllegalStateException("无法读取所选文件") + return mapOf("path" to out.absolutePath, "name" to name, "mime" to mime) + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == REQ_MIC) { + val cb = voiceStartResult + voiceStartResult = null + if (cb == null) return + if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + beginRecord(cb) + } else { + cb.error("need_permission", "请允许麦克风权限后再按住说话", null) + } + return + } + if (requestCode == REQ_CALL) { + val cb = callMediaResult + callMediaResult = null + if (cb == null) return + cb.success(grantResults.isNotEmpty() && grantResults.all { it == PackageManager.PERMISSION_GRANTED }) + return + } + if (requestCode == REQ_LOC) { + val cb = locResult + locResult = null + if (cb == null) return + if (grantResults.any { it == PackageManager.PERMISSION_GRANTED }) { + emitLocation(cb) + } else { + cb.error("need_permission", "请允许位置权限后再发送位置", null) + } + } + } + + override fun onDestroy() { + stopScreenshotWatch() + try { + recorder?.release() + } catch (_: Exception) { + } + stopPlay() + stopCallTone() + super.onDestroy() + } + + private fun installApk(path: String) { + val file = File(path) + if (!file.exists()) throw IllegalStateException("安装包不存在") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && + !packageManager.canRequestPackageInstalls() + ) { + startActivity( + Intent( + Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, + Uri.parse("package:$packageName"), + ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + ) + } + val uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", file) + val intent = + Intent(Intent.ACTION_VIEW).apply { + setDataAndType(uri, "application/vnd.android.package-archive") + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + startActivity(intent) + } + + companion object { + private const val REQ_MIC = 91 + private const val REQ_PICK = 88 + private const val REQ_LOC = 93 + private const val REQ_NOTIFY = 94 + private const val REQ_CALL = 95 + private const val REQ_MEDIA = 96 + private const val CALL_NOTICE_ID = 73101 + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/MeizuPushReceiver.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/MeizuPushReceiver.kt new file mode 100644 index 0000000..fb5e15b --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/MeizuPushReceiver.kt @@ -0,0 +1,71 @@ +package com.fysxkj.fengying_oa + +import android.content.Context +import android.content.Intent +import android.util.Log +import com.meizu.cloud.pushsdk.MzPushMessageReceiver +import com.meizu.cloud.pushsdk.handler.MzPushMessage +import com.meizu.cloud.pushsdk.platform.message.PushSwitchStatus +import com.meizu.cloud.pushsdk.platform.message.RegisterStatus +import com.meizu.cloud.pushsdk.platform.message.SubAliasStatus +import com.meizu.cloud.pushsdk.platform.message.SubTagsStatus +import com.meizu.cloud.pushsdk.platform.message.UnRegisterStatus +import org.json.JSONObject + +class MeizuPushReceiver : MzPushMessageReceiver() { + override fun onRegisterStatus(context: Context?, registerStatus: RegisterStatus?) { + val pushId = registerStatus?.pushId + if (context == null || pushId.isNullOrBlank()) { + Log.w(TAG, "meizu register ${registerStatus?.code} ${registerStatus?.message}") + return + } + PushStore.saveReg(context, pushId, "meizu") + Log.i(TAG, "meizu register ok") + } + + override fun onUnRegisterStatus(context: Context?, unRegisterStatus: UnRegisterStatus?) {} + + override fun onPushStatus(context: Context?, pushSwitchStatus: PushSwitchStatus?) {} + + override fun onSubTagsStatus(context: Context?, subTagsStatus: SubTagsStatus?) {} + + override fun onSubAliasStatus(context: Context?, subAliasStatus: SubAliasStatus?) {} + + override fun onMessage(context: Context?, message: String?, platformExtra: String?) {} + + override fun onNotificationClicked(context: Context?, mzPushMessage: MzPushMessage?) { + val ctx = context ?: return + val intent = + Intent(ctx, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP) + putExtras(parseExtras(mzPushMessage?.selfDefineContentString)) + } + runCatching { ctx.startActivity(intent) } + .onFailure { Log.w(TAG, "meizu click", it) } + } + + override fun onNotificationArrived(context: Context?, mzPushMessage: MzPushMessage?) {} + + override fun onNotificationDeleted(context: Context?, mzPushMessage: MzPushMessage?) {} + + companion object { + private const val TAG = "FengyingPush" + + private fun parseExtras(raw: String?): android.os.Bundle { + val extras = android.os.Bundle() + if (raw.isNullOrBlank()) return extras + runCatching { + val obj = JSONObject(raw) + val src = if (obj.has("parameters")) obj.getJSONObject("parameters") else obj + for (key in listOf( + "conversationId", "kind", "fromName", "peerId", "title", "bizType", "bizId", + "callId", "callKind", "action", "fromUserId", + )) { + val v = src.optString(key) + if (v.isNotBlank()) extras.putString(key, v) + } + } + return extras + } + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OemAdapt.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OemAdapt.kt new file mode 100644 index 0000000..913871a --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OemAdapt.kt @@ -0,0 +1,183 @@ +package com.fysxkj.fengying_oa + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.media.AudioAttributes +import android.net.Uri +import android.os.Build +import android.os.PowerManager +import android.provider.Settings +import android.util.Log + +object OemAdapt { + private const val TAG = "FengyingOem" + + fun ensureChannels(ctx: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return + val nm = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val im = + NotificationChannel(PushStore.CHANNEL_IM, "聊天消息", NotificationManager.IMPORTANCE_HIGH).apply { + description = "同事发来的即时消息和通话" + enableVibration(true) + enableLights(true) + setShowBadge(true) + val sound = Uri.parse("android.resource://${ctx.packageName}/raw/fengying_notification") + setSound(sound, AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build()) + } + val call = + NotificationChannel(PushStore.CHANNEL_CALL, "音视频来电", NotificationManager.IMPORTANCE_HIGH).apply { + description = "语音和视频来电提醒" + enableVibration(true) + enableLights(true) + setShowBadge(true) + val sound = Uri.parse("android.resource://${ctx.packageName}/raw/fengying_incoming") + setSound(sound, AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE).build()) + lockscreenVisibility = android.app.Notification.VISIBILITY_PUBLIC + } + val todo = + NotificationChannel(PushStore.CHANNEL_TODO, "待办与审批", NotificationManager.IMPORTANCE_DEFAULT).apply { + description = "审批、待办和办公提醒" + setShowBadge(true) + } + nm.createNotificationChannel(im) + nm.createNotificationChannel(call) + nm.createNotificationChannel(todo) + } + + fun batteryIgnored(ctx: Context): Boolean { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true + val pm = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager + return pm.isIgnoringBatteryOptimizations(ctx.packageName) + } + + fun requestBattery(ctx: Context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return + if (batteryIgnored(ctx)) return + try { + val intent = + Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:${ctx.packageName}") + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ctx.startActivity(intent) + } catch (e: Exception) { + Log.w(TAG, "battery intent", e) + openAppDetails(ctx) + } + } + + fun status(): Map { + val vendor = PushStore.detectVendor() + val (title, hint) = + when (vendor) { + "oppo" -> + "后台运行与通知" to "请允许自启动、允许后台运行,并把电池优化设为不优化,否则 ColorOS 会杀掉办公消息。" + "vivo" -> + "后台运行与通知" to "请在 i 管家 / 权限管理里允许自启动、允许后台高耗电,并关闭省电限制,OriginOS 才会及时弹出消息。" + "meizu" -> + "后台运行与通知" to "请在手机管家里允许自启动、允许后台运行,并关闭电池优化,Flyme 才会及时弹出消息。" + "huawei" -> + "后台运行与通知" to "请允许自启动、允许关联启动,并把电池优化设为不允许,否则鸿蒙/EMUI 会杀掉办公消息。" + else -> + "后台运行与通知" to "请允许通知,并把本应用加入电池优化白名单,以便及时收到消息。" + } + return mapOf( + "vendor" to vendor, + "title" to title, + "hint" to hint, + ) + } + + fun openKeepAlive(ctx: Context) { + val vendor = PushStore.detectVendor() + val intents = + when (vendor) { + "oppo" -> oppoIntents(ctx) + "vivo" -> vivoIntents(ctx) + "meizu" -> meizuIntents(ctx) + "huawei" -> huaweiIntents() + else -> emptyList() + } + for (intent in intents) { + if (launch(ctx, intent)) return + } + openAppDetails(ctx) + } + + private fun oppoIntents(ctx: Context): List { + val pkg = ctx.packageName + return listOf( + component("com.oplus.safecenter", "com.oplus.safecenter.startupapp.view.StartupAppListActivity"), + component("com.coloros.safecenter", "com.coloros.safecenter.startupapp.StartupAppListActivity"), + component("com.coloros.safecenter", "com.coloros.safecenter.permission.startup.StartupAppListActivity"), + component("com.oppo.safe", "com.oppo.safe.permission.startup.StartupAppListActivity"), + component("com.coloros.oppoguardelf", "com.coloros.powermanager.fuelgaue.PowerConsumptionActivity"), + Intent().setClassName("com.coloros.safecenter", "com.coloros.privacypermissionsentry.PermissionTopActivity"), + Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS), + ).onEach { it.putExtra("packageName", pkg) } + } + + private fun vivoIntents(ctx: Context): List { + val pkg = ctx.packageName + return listOf( + component("com.vivo.permissionmanager", "com.vivo.permissionmanager.activity.BgStartUpManagerActivity"), + component("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.BgStartUpManager"), + component("com.iqoo.secure", "com.iqoo.secure.ui.phoneoptimize.AddWhiteListActivity"), + component("com.vivo.abe", "com.vivo.applicationbehaviorengine.ui.ExcessivePowerManagerActivity"), + Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS), + ).onEach { it.putExtra("packageName", pkg) } + } + + private fun huaweiIntents(): List { + return listOf( + component("com.huawei.systemmanager", "com.huawei.systemmanager.startupmgr.ui.StartupNormalAppListActivity"), + component("com.huawei.systemmanager", "com.huawei.systemmanager.appcontrol.activity.StartupAppControlActivity"), + component("com.huawei.systemmanager", "com.huawei.systemmanager.optimize.process.ProtectActivity"), + component("com.huawei.systemmanager", "com.huawei.systemmanager.power.ui.HwPowerManagerActivity"), + Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS), + ) + } + + private fun meizuIntents(ctx: Context): List { + val pkg = ctx.packageName + return listOf( + Intent("com.meizu.safe.security.SHOW_APPSEC").apply { + setClassName("com.meizu.safe", "com.meizu.safe.security.AppSecActivity") + putExtra("packageName", pkg) + }, + component("com.meizu.safe", "com.meizu.safe.permission.PermissionMainActivity"), + component("com.meizu.safe", "com.meizu.safe.powerui.PowerAppPermissionActivity"), + Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS), + ) + } + + private fun component(pkg: String, cls: String) = + Intent().setComponent(ComponentName(pkg, cls)).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + private fun openAppDetails(ctx: Context) { + try { + ctx.startActivity( + Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { + data = Uri.parse("package:${ctx.packageName}") + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + }, + ) + } catch (e: Exception) { + Log.w(TAG, "details", e) + } + } + + private fun launch(ctx: Context, intent: Intent): Boolean { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return try { + if (intent.resolveActivity(ctx.packageManager) == null) return false + ctx.startActivity(intent) + true + } catch (_: Exception) { + false + } + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoCompatPushService.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoCompatPushService.kt new file mode 100644 index 0000000..9f48497 --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoCompatPushService.kt @@ -0,0 +1,11 @@ +package com.fysxkj.fengying_oa + +import android.content.Context +import com.heytap.msp.push.mode.DataMessage +import com.heytap.msp.push.service.CompatibleDataMessageCallbackService + +class OppoCompatPushService : CompatibleDataMessageCallbackService() { + override fun processMessage(context: Context?, message: DataMessage?) { + super.processMessage(context, message) + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoDataMessageService.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoDataMessageService.kt new file mode 100644 index 0000000..084a988 --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoDataMessageService.kt @@ -0,0 +1,11 @@ +package com.fysxkj.fengying_oa + +import android.content.Context +import com.heytap.msp.push.mode.DataMessage +import com.heytap.msp.push.service.DataMessageCallbackService + +class OppoDataMessageService : DataMessageCallbackService() { + override fun processMessage(context: Context?, message: DataMessage?) { + super.processMessage(context, message) + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoPushCallback.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoPushCallback.kt new file mode 100644 index 0000000..0573dcf --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/OppoPushCallback.kt @@ -0,0 +1,34 @@ +package com.fysxkj.fengying_oa + +import android.content.Context +import android.util.Log +import com.heytap.msp.push.callback.ICallBackResultService + +class OppoPushCallback : ICallBackResultService { + override fun onRegister(responseCode: Int, registerID: String?) { + if (responseCode == 0 && !registerID.isNullOrBlank()) { + val ctx = appCtx ?: return + PushStore.saveReg(ctx, registerID, "oppo") + Log.i(TAG, "oppo register ok") + } else { + Log.w(TAG, "oppo register $responseCode") + } + } + + override fun onUnRegister(responseCode: Int) {} + + override fun onSetPushTime(responseCode: Int, pushTime: String?) {} + + override fun onGetPushStatus(responseCode: Int, status: Int) {} + + override fun onGetNotificationStatus(responseCode: Int, status: Int) {} + + override fun onError(errorCode: Int, message: String?) { + Log.w(TAG, "oppo error $errorCode $message") + } + + companion object { + private const val TAG = "FengyingPush" + @Volatile var appCtx: Context? = null + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/PushStore.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/PushStore.kt new file mode 100644 index 0000000..18a2bce --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/PushStore.kt @@ -0,0 +1,89 @@ +package com.fysxkj.fengying_oa + +import android.content.Context +import android.content.Intent +import android.os.Build + +object PushStore { + const val PREFS = "fy_push" + const val KEY_REG = "regId" + const val KEY_VENDOR = "vendor" + // New id makes the bundled custom sound take effect even on phones that + // already created the legacy fy_im notification channel. + const val CHANNEL_IM = "fy_im_custom" + const val CHANNEL_CALL = "fy_call_v2" + const val CHANNEL_TODO = "fy_todo" + const val KEY_OEM_PROMPT = "oem_prompted" + + fun saveReg(ctx: Context, id: String, vendor: String = detectVendor()) { + ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .edit() + .putString(KEY_REG, id) + .putString(KEY_VENDOR, vendor) + .apply() + } + + fun token(ctx: Context): Map { + val p = ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + val reg = p.getString(KEY_REG, "") ?: "" + val vendor = p.getString(KEY_VENDOR, detectVendor()) ?: detectVendor() + return mapOf( + "vendor" to vendor, + "regId" to reg, + "brand" to Build.BRAND, + "model" to Build.MODEL, + ) + } + + fun detectVendor(): String { + val m = "${Build.MANUFACTURER} ${Build.BRAND} ${Build.FINGERPRINT}".lowercase() + return when { + m.contains("vivo") || m.contains("iqoo") -> "vivo" + m.contains("oppo") || m.contains("realme") || m.contains("oneplus") || m.contains("oplus") -> "oppo" + m.contains("meizu") -> "meizu" + isHuaweiFamily(m) -> "huawei" + m.contains("xiaomi") || m.contains("redmi") || m.contains("blackshark") -> "xiaomi" + else -> "android" + } + } + + fun isHuaweiFamily(raw: String = "${Build.MANUFACTURER} ${Build.BRAND} ${Build.FINGERPRINT}".lowercase()): Boolean { + return listOf("huawei", "honor", "harmony", "hmos", "ohos", "harmonyos").any { raw.contains(it) } + } + + fun extrasOf(intent: Intent?): Map { + val out = linkedMapOf() + val data = intent?.data + if (data != null && data.scheme == "fengyingoa") { + when (data.host) { + "qr" -> { + val ticket = data.getQueryParameter("ticket") + if (!ticket.isNullOrBlank()) { + out["kind"] = "qr-login" + out["ticket"] = ticket + } + } + "chat" -> { + val cid = data.getQueryParameter("conversationId") + if (!cid.isNullOrBlank()) out["conversationId"] = cid + } + } + } + val b = intent?.extras ?: return out + for (key in listOf( + "conversationId", "kind", "fromName", "peerId", "title", "bizType", "bizId", "ticket", + "callId", "callKind", "action", "fromUserId", + )) { + val v = runCatching { b.getString(key) }.getOrNull() + if (!v.isNullOrBlank() && !out.containsKey(key)) out[key] = v + } + return out + } + + fun oemPrompted(ctx: Context) = + ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE).getBoolean(KEY_OEM_PROMPT, false) + + fun markOemPrompted(ctx: Context) { + ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putBoolean(KEY_OEM_PROMPT, true).apply() + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/VivoPushReceiver.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/VivoPushReceiver.kt new file mode 100644 index 0000000..1a3d1d2 --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/VivoPushReceiver.kt @@ -0,0 +1,11 @@ +package com.fysxkj.fengying_oa + +import android.content.Context +import com.vivo.push.sdk.OpenClientPushMessageReceiver + +class VivoPushReceiver : OpenClientPushMessageReceiver() { + override fun onReceiveRegId(context: Context?, regId: String?) { + if (context == null || regId.isNullOrBlank()) return + PushStore.saveReg(context, regId, "vivo") + } +} diff --git a/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/VoiceAsr.kt b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/VoiceAsr.kt new file mode 100644 index 0000000..de23b3d --- /dev/null +++ b/apps/native/android/app/src/main/kotlin/com/fysxkj/fengying_oa/VoiceAsr.kt @@ -0,0 +1,222 @@ +package com.fysxkj.fengying_oa + +import android.content.Context +import android.media.MediaCodec +import android.media.MediaExtractor +import android.media.MediaFormat +import org.json.JSONObject +import org.vosk.LibVosk +import org.vosk.LogLevel +import org.vosk.Model +import org.vosk.Recognizer +import java.io.BufferedInputStream +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.net.HttpURLConnection +import java.net.URL +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.util.zip.ZipInputStream + +/** 手机端离线语音转文字(Vosk,首次使用自动下载中文小模型到应用目录)。 */ +object VoiceAsr { + private const val MODEL_DIR = "vosk-model-small-cn-0.22" + private const val MODEL_URL = + "https://alphacephei.com/vosk/models/vosk-model-small-cn-0.22.zip" + + @Volatile + private var model: Model? = null + + @Synchronized + fun recognize(context: Context, audioPath: String): String? { + LibVosk.setLogLevel(LogLevel.WARN) + val m = model ?: loadModel(context.applicationContext).also { model = it } + val pcm = decodeToPcm16Mono16k(audioPath) ?: return null + if (pcm.isEmpty()) return null + val recognizer = Recognizer(m, 16000f) + try { + recognizer.acceptWaveForm(pcm, pcm.size) + val json = JSONObject(recognizer.finalResult) + val text = json.optString("text", "").trim() + return text.ifEmpty { null } + } finally { + recognizer.close() + } + } + + private fun loadModel(context: Context): Model { + val dir = File(context.filesDir, MODEL_DIR) + if (!File(dir, "am/final.mdl").exists()) { + downloadAndUnzip(dir) + } + return Model(dir.absolutePath) + } + + private fun downloadAndUnzip(targetDir: File) { + if (targetDir.exists()) { + targetDir.deleteRecursively() + } + targetDir.mkdirs() + val zipFile = File(targetDir.parentFile, "$MODEL_DIR.zip") + val conn = (URL(MODEL_URL).openConnection() as HttpURLConnection).apply { + connectTimeout = 30_000 + readTimeout = 120_000 + requestMethod = "GET" + } + conn.inputStream.use { input -> + FileOutputStream(zipFile).use { output -> input.copyTo(output) } + } + conn.disconnect() + unzip(zipFile, targetDir.parentFile!!) + zipFile.delete() + val nested = File(targetDir.parentFile, MODEL_DIR) + if (!nested.exists()) { + targetDir.parentFile?.listFiles()?.firstOrNull { + it.isDirectory && File(it, "am/final.mdl").exists() + }?.renameTo(nested) + } + } + + private fun unzip(zipFile: File, destDir: File) { + ZipInputStream(BufferedInputStream(zipFile.inputStream())).use { zis -> + var entry = zis.nextEntry + while (entry != null) { + val outFile = File(destDir, entry.name) + if (entry.isDirectory) { + outFile.mkdirs() + } else { + outFile.parentFile?.mkdirs() + FileOutputStream(outFile).use { fos -> zis.copyTo(fos) } + } + zis.closeEntry() + entry = zis.nextEntry + } + } + } + + private fun decodeToPcm16Mono16k(path: String): ByteArray? { + val extractor = MediaExtractor() + try { + extractor.setDataSource(path) + var track = -1 + var format: MediaFormat? = null + for (i in 0 until extractor.trackCount) { + val f = extractor.getTrackFormat(i) + val mime = f.getString(MediaFormat.KEY_MIME) ?: continue + if (mime.startsWith("audio/")) { + track = i + format = f + break + } + } + if (track < 0 || format == null) return null + extractor.selectTrack(track) + val mime = format.getString(MediaFormat.KEY_MIME) ?: return null + val codec = MediaCodec.createDecoderByType(mime) + codec.configure(format, null, null, 0) + codec.start() + val pcmOut = ByteArrayOutputStream() + var sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE) + var channels = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT) + val bufferInfo = MediaCodec.BufferInfo() + var inputDone = false + while (true) { + if (!inputDone) { + val inIndex = codec.dequeueInputBuffer(10_000) + if (inIndex >= 0) { + val buf = codec.getInputBuffer(inIndex) ?: continue + val size = extractor.readSampleData(buf, 0) + if (size < 0) { + codec.queueInputBuffer( + inIndex, + 0, + 0, + 0, + MediaCodec.BUFFER_FLAG_END_OF_STREAM, + ) + inputDone = true + } else { + codec.queueInputBuffer(inIndex, 0, size, extractor.sampleTime, 0) + extractor.advance() + } + } + } + val outIndex = codec.dequeueOutputBuffer(bufferInfo, 10_000) + if (outIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + val outFormat = codec.outputFormat + sampleRate = outFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE) + channels = outFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) + } else if (outIndex >= 0) { + val buf = codec.getOutputBuffer(outIndex) ?: continue + if (bufferInfo.size > 0) { + val chunk = ByteArray(bufferInfo.size) + buf.position(bufferInfo.offset) + buf.limit(bufferInfo.offset + bufferInfo.size) + buf.get(chunk) + pcmOut.write(chunk) + } + codec.releaseOutputBuffer(outIndex, false) + if (bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0) break + } + } + codec.stop() + codec.release() + var samples = pcmOut.toByteArray() + if (samples.isEmpty()) return null + var shorts = bytesToShorts(samples) + if (channels > 1) { + shorts = downmix(shorts, channels) + } + if (sampleRate != 16000) { + shorts = resample(shorts, sampleRate, 16000) + } + return shortsToBytes(shorts) + } catch (_: Exception) { + return null + } finally { + try { + extractor.release() + } catch (_: Exception) { + } + } + } + + private fun bytesToShorts(bytes: ByteArray): ShortArray { + val bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN) + val count = bytes.size / 2 + val out = ShortArray(count) + for (i in 0 until count) out[i] = bb.short + return out + } + + private fun shortsToBytes(samples: ShortArray): ByteArray { + val bb = ByteBuffer.allocate(samples.size * 2).order(ByteOrder.LITTLE_ENDIAN) + for (s in samples) bb.putShort(s) + return bb.array() + } + + private fun downmix(samples: ShortArray, channels: Int): ShortArray { + if (channels <= 1) return samples + val frames = samples.size / channels + val out = ShortArray(frames) + for (i in 0 until frames) { + var sum = 0 + for (c in 0 until channels) sum += samples[i * channels + c] + out[i] = (sum / channels).toShort() + } + return out + } + + private fun resample(samples: ShortArray, fromRate: Int, toRate: Int): ShortArray { + if (fromRate == toRate) return samples + val ratio = fromRate.toDouble() / toRate.toDouble() + val outLen = (samples.size / ratio).toInt().coerceAtLeast(1) + val out = ShortArray(outLen) + for (i in 0 until outLen) { + val src = (i * ratio).toInt().coerceIn(0, samples.size - 1) + out[i] = samples[src] + } + return out + } +} diff --git a/apps/native/android/app/src/main/res/drawable-v21/launch_background.xml b/apps/native/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/apps/native/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/apps/native/android/app/src/main/res/drawable/launch_background.xml b/apps/native/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/apps/native/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/apps/native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/apps/native/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..3defb0bf54ff9fbcb1f55c78f5bbbfb68aa75ba9 GIT binary patch literal 5613 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%0ALK@OFUR5cg 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 literal 0 HcmV?d00001 diff --git a/apps/native/android/app/src/main/res/raw/fengying_incoming.mp3 b/apps/native/android/app/src/main/res/raw/fengying_incoming.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..7d509f0652d8a75f6c76b8cc96104c49507c166e GIT binary patch literal 200398 zcmeF)cTiMYn>PFg0TED8GDr{*5KsXDm7Ft@L~;%iC5q%If}kV`l5lzy?>#zU%&u_q*P2GR|QO)eZ{i8n|{r?z|uv6z!U}zYa*yr#r z5E7G9P|?saTw%G!%5n2H55J&@n1u8l1tk?#4J{pg!~15I4{Yrnom||#efK$04hTZ`&60PNTZ;^8PU zoc=KRfeN^W>$`yp^KOZ!I^BmNDp-yYAc3YAL4NsK^qq9-acxfY#8Vg4lu+4eIr62x zU|rz2tLdb{B-7M|fyc;$GJuVXF@qymJjQ03Y|t3MME%Og=Lc+Je3}9UbH)0ZsMwb& ztUB{LOA>Ar7>rHw16cs$wI{GT;t9&Lo#*cIPmqw?uK8Aq1sUmH6~9%YN%KR&ut`uG zI|W_ri%%*R&RwywMoI05VY06@!|a;Gg2YO$X|hCN-%a~YL6{oY{GQ+{(GT6LGMAs4 zDj4Sww2=~CC2A8H-S^YrP(1qE@N4Qj_kp$0hR=LAdXlJOQ_xr@OPHgu?FR3&v=P$j z;G}jdNnItl8!jV8NEh}%{b*r~pM5#V=cyQ4#!EzlUDaD2U!ss^z$T7p7F_BVE^)oC z?i&6omX}h7@S2Y87~S*4=JL^;dEPch?%WOa{&vSlarYmJxE*w>eNebYS&>J}dH7tW zaO)l)g{Dr+w`bBdNm`w?OYRwcSQ!zlfckw@x<$N!KF^E#4cFs+8kuTVM5_%rXgS24 zqe><+bI%GBTP4ia>1W-E@QOz-X&o=w&MYg<7o|mcigq;!n*-m#^t6Kaz^p<@C$Vz^ zU()U=y@vwW3Qo^y&9Yxz&DSg+vJvI87Lne?mT&rpYVCOlXiBLe@%utp5eTi**kz;MZ-EkvoV{PgYNpF0`)m^ zmvY={oD$Q3wcxTT?v~A=mQIef4eJ9wPM|1arADRai~;HJLNfi!vl!YER$F%}Zpkx$ zpg5bT4$feb1O@f|h~XD6BX{+ixB}HN6T5d0)V|%=Pp0ErUB8VhqR?>LJtM<&c~)BC zKrh_H!ld`kyj>OX)((NF=HQIrxq38mX#+pB>dkrb2swgzo+`Vk#OLM-Vv6)1%VFp7 zTErGAslDF6zWV7>d}i?GHTypLj3m|@l1@6aPm^VVHNl2mvb8%kI!>LYL+*q3M(N#} zc;_sq9i)FA_ND~Ix2cNB;eEiulu!_a>0Mm@q8LFDAMR0|5{vOFSP*rPYwWDC&i2{_ zBWs_eQBkdIW>Ux24h4@Y`P|XIX&fpC4$jdn2yASCmClmJjJ&OJ>Nb^*k=^(iUcLU( z^GQa8!54r&(?x`4ldkp*r9V?oBP?D7JFq1(qW6(~Zsmh0kx9?hjLWrAk<7yAlBc+b zFP^!3;U!vjc8=c@Q)8ox>UN%CX9s#!JQdYPsBlN*_-`mWSod+~R#mh4`k#AFK}yD* zptSmKB@L`}i{N`lb&cVjS0eFFU1N_(Zp?giVI{a2(*5wYV^LK`a2t*8+NO%u6X56< zOl7a)^4BLoZ9Iywk7oG|1d3P)ZB49-9zVk z9-TVJ4;<%^lsmP?!>P}7=F?*u>ZgO26NUmItFnPVm1w$y^E@;^>7B{nZ&|bSk0Iqd z8fpW04m_{8ceR0MG0BUo@HtO9f7nO=+_!km+IgAuPOB0{qaRtmP3(lt=8P~U%FvCr zo?=)7$5|(NL0?d#*@or@vn1XpA=$^V^=f{0iwS~g(04Y|(7o!mVQ|41|= zD0TJ($N5Pjyjt(!CWE#%{!F8(+NJ@Ez=Wc2PjrdOlkR}b$?u%_;uU`*prf*|Wi5;0 z{+i#iq=d7htH-dr%GHGVzByoN-Wpuf)k<{18|k;!Uw8%PczM-aqAob^?fqEk@)>O7 z3*Zi!B8wwB+KEAuWTgIL13NLI1{C7TOhjpcPUwE<`}m@? zlDFL_K4o1$++bzE#WBtqUazi=UUH7=Td+=f^4^USOJLastMCoG=H)Xf!)YfDC_Tg0gw_VkcPX z4v7+fg^p94tV?hIfNFvX+g?|YjzOgZ+erH9$G2xS*Pi#(O!r3*ySRH+W*uF-w%kGm zoiYH}gPEtAH)8}v8fIt}6-Qx}%2_RqF5z9*)U^C^TMAX5uW~}p|Gm=-9%I#4;5e(K zRj9wlVJqj;ciUKgnS$DTnlOJv?dD!R`|HIz6aO1GG9yfvFQUf}3YI8DVm%J;yWA^K zYEvx|nq;9X)}5T0O+`^$C);F_W$rL0MLNG{*KU14?z5BWUeVzFCS2H}3>;^Q2%D_l zLq?vOr}okL_cr2$=V{$NsAdb~r*bt3my1&>D_dhd3TNFUaDw;V24wF(bdwW3_%whw z#z+>6b$9!`plYnWPBL4xDcNH>`{A$JeQ#3kOwUD#DVHZN4qLz;soL4U2Cv)<*_GBN zouhl3U~HEt4gI$Dr_`%-7!j&;>1FNT+|9CXwVP)I78r}aYV3c-W?OnVUP!`Haj8*HmnMPBk$J-< zvXP$B>vn#Lmu&N8lh>NVEL8E}4Aw6Sb(H9Q=+{yh!hFSWDMZN~m6%p!7mcyICz!7D zW>6iLJyluExr_nPM;YhC3*FDFx~_G758pH0Wq5c?q_pL7vR3$LsE!qmBa6Gf&}$7Y zsZJL{%Q`*@<1l`VF5h~EYP%*DsuyQ&&`4b&)}&~&HZ~nGD47{xmy*_~3kGMfeerxp z&Ebcx2;u_PZ0ZjL;kls(gv@NY*)n-nX$4e|D2wiL7Z&v0^$Ozp)K}O(^w|>t_D5=u zcF_mI4t46A3l(D+ZT(5iGcp!qR(4f*7eY8Xo zU}?R6ERtI$%B|Rsv}0Ppe=u=GQTOVLH5anniU*po7k@A=*iSboqEogCpGhhiY}(YI};yfGiCa?KGhWK z1M~aZdR-Kj++Q7S$aw`=802v~gMwf(Hr&Jo@xa5SnB7f_y(w<-d*pIT?OM4~Y@F^` z^=$O9=8E5YO8lBW7));70ms=O3S6w$acY#!wo|g-$W*ryvsxIL&?AzI+kDNV0|2cu zM^D=j-Q5FEc>us?T1$r&xK*CAAil(_QEY2+>mJJ!!fxu9FH=Rn#rl))Q8dPDIhnC$wb4gA1RTe#wO+?i^wS;6wZ`_lgq8h^fc$57b*Jj+8!!Ar@VYi} zI}&fhE)&^~*B`y6klc}( zo#6d^9}o30@Yy^c2*$d#EP{i9)amodiov2op&7XD=nBG zJ%ej?fgE{V&r%2O3gFOfr*DJ9*oe8}vln{r6IP*10*u*0^kAhc%rbEak2f@;l1oIjt6C3;TFets|th+Nq(0@@~3x_Sg2OSs#?PNNE9=~=#9v@G)w1y!s?JCoOi) zHO6c$4q_0f^|hC_bM#nT$>ncnvz%!PdK#=X>^U(Oi)9xnV(1vkiQ?<#n8ze6kk#wQ zdQFHdtPpkZ9!ryAj(W)Hxuu@yS5DyEILXyn(? z@D$&!DFHe8Yg`U@It&V;HLAKB`vwwj-ju?W=VUG}=Tz98z1`m~aL=_wv!nb`L`Cg^ z6`)J(v;+|MrLbxnX)-Lst}=z)w*J<=)P35pQMHw%;GNTTPE;59Y#r@;ujn4D)OFZz zmy!qHY#PM930AsA?A&woic7_a&_V6Iq3Wf_8HxdRT+Sb^$S6nUUg?;j7Z8oPy6Pt_ z{W#^;@HH+brMs`AueUWdVyqUIM@Zj`)sHNUqFDS^Fzj{xylHgPuu{r+oU+spr}mgN z{(X`g?$wb?iQqWvq%sDD{BCz&$+xAAj(p0%_a)NVbsm-6eKh(mlqs^@j$L}@>*ZSa zqH>gxpBk%a14_Ob%CmgdpRblJ4Xcy|G~|ZM1I-fxO}PUKq~F6kYu=n9l&2wi=l^nw zRUcZq0+OYR{X%G>_Mi*idYL{D-&7_tn5kS4o17_Ks7V5pJ3tG0)a* zXSMQ2?0veaQz99p-wfjHuFW*MRA6o{Y<6!|E3%C36EEx5kEZJWH}?b+mWbkYLAgizm|JcVmq zsuwXn3ck=Ib+#;2b>=oD_OlQiV}F7^F@&|9s_yLJe;=pjVqQ5zFwqmn;l0QDB)uu} zP0cvulvGW?8f(KLky3b#j*FOJWfi4aTmA;2$sqDQQ=`j#1mD1!EF@^5lMGr&gmW;A$CAq3p%; zFsldv4w!*QaRAVK0bLLkKYNTyCTcphCnqP2rLe5n;AIp?F1e7o`3x};&8}3k8r=mJ zt!yrbL@8uk>No z6vkY&vz?n`QYW$edC@Oa!5s;_i{Trp8J??leTN5|;?_s2>l03`n=e1z3%i%ymuoE3 z9QZUmvcaXaLz{D@s*@#QI@XL(T7%JN_s4r#x5O3M4Z~IrJq`qWFPiN8nD$F>yXvgr z>>eArz;H-eOirR^T--{*)>qyLeAI4kQA^h@smTIhjx*xIa*{SqWv)YzgjCq#Cl0URf%|5U@)B?=Ez9zV z)1;dV5+1TMr{3@^>aTe^v6A%6T(F{l{%}|!_Fjj-#dSj5^n}gjMwI<-LwA#Nz{+bk za1Ay|c*trg>l?H_xKk@A_$wXMzpBcA@0%;^6;l!(&!wcXbmiJ!0YJ=z_Dm6gvsH ztClH5q`?!`wAP7Xl`ZLZFE`By?PtcVxu^6h2`{3*muyIHI)BXGRuiuCy!~1#zFJAG zA(69XigvImR?s-~OBJ9NU@gV&SBa*65>q zO3Gl+@~wg2j#4AxsI>C=^Ht-~A+FP})lk$-U-AE-sjW~Vsg?;#Qm*X}%;C%K*s!hBTM2kVW~Y>sX;PF? zyIWAZ=23fa129>6Cil$N?+G4(AlAfB7{v{WdZ%YEwS6=C9k*(gE={+|7PCo#GdQ5S zFjX+(K}c7WaGz&%y}7(*Ytrp3@n@NyoGE@bGp$TZtFKyLE%<>^oq>LrQO>UTOXdoLW0qg6c%Pv!dnGJQ?O%9C3M%=+o+ah?cCa$Cx?TG$ik*$h;f8C!SF`fH zmu8De`pY`^bHPWFRMr(RuIuz?ew|ep<8(3--6E-zd|=IdbRYw!718#=eJ64~Wt#iFK+b_Ks{x7y7`cWvOrZlsNG1rtc7$ z%Bln1+K*4GVM(UB5f+A?(!|o!u76Pp-os=1^f-#V8;o$P=ztG-bXz{BsW-U%0I*mEANU zPp)iWZMWL-ey+fS>M$<3kRlvlTf>KyBAbEXsf`5rben6CPzL+uFviV=xplrQC1>Lr z>kF!ZqHJ?(UuT6&-;UiO_Kv9ibq5n&0pGQ?QGYyxW2}rLQ>CHSG{v^qMCqUC)~`)p zeK${qpy}<5OYviIl;RzGyyyEHe76IuDECMK;LcF((LGb4%&2-z^_p6-xmi7>5utS` z>|WIBxhKAI;+fa8>Mt6FCV=B?lg%3x)EwQx=~@yMheqvG0v@~8@w0&hZyD)p*!Ghz z@UE_k;jbxQu;tIK3f{h*@l8}rg{i$D)@3)h;LtKP@08#3yXvH03hOZYT`Ksja&b=u*z)bPh;vzkJ7U#n$;rcgfx;WT=%Po4Qs@n=ycX}Q3bshV?IWRcE8O#u`43+3O zsfx}HVV!EPesM(NA%=naN9kb&f%3z=n87u z%q1^>Fs`80ksxRGZ9_Jen=cR8F-xsVZ>zoY^X<|u(7Zj<`1AsWX~FCME~PxOP~U>w za|TjmgcV;?%PRfj+zf3!tmK`B^aO{A_UdX3GD-=Snl3TZGc-xyW!D>nS9^o-=#{FD zE1H&TdG?h@Nm3j3bA@X@G+1gMdzzxO^vkjXsd9paK8mcFEwC@@e7~VKI^-wVB~Y+d z0>hW|a|m2)DoRQT>S>8#>-#qGu{P5df2)(MuP9O^^d3_pK-+zp#bO2=XOGlSr(Q>| zP15zl0oK*i`TK_Ys6FEbTZ}h9+ILoA$dX8RG z$V*Cbgi!Ocgyc^b$fBDXc`N3B8m%l01JbK3URx1-cA2}Wo&RoS_JjY9P30wj+{A0~ z86h#zk=EALXT$)2`!rh^5JJK;acd>4IOz$xerV%DsK+U*!$8<>gEn~OrpUs2O8A{Z z9!D(Qo0Q7AxX)x|w-Ehdx29B6V_?hen+94Sr4HWXm%edCqTCln?Q&S05*;!Z)LTN}rfRq9YzADAD{HCI$Gde1GCH@B(oF=|m; zD`ZKif&`qO#sOCt<&gvhsM3UeKzW4o{|YU zGnMF=IqoUFL7ThY%$_(?`Z19uWgb+~;;(_`{uZN<^;pc;SJMmfbAIg|Qs-x(E@(1kNrXtsx#~ssSb@Mr2CQbj)UR~C!6FmBu3gc${>z^|5&ne~8KTlVE z))nM^`l6j!@sh(wy4@t@^~=|KlZIC#4RLq&IG_7m#jIz`6^?J|xn%*a=n_?$N&2P< zHXSQr8?AK3y~d=3YL1HQj`*CILE@dQV^S(Z-->a$2RaL>Waz^ATs9_Vv*O$2a9jDO z%Cku3mpU|3Q_;Gz_%bhC$#6W2QItDZ-D)VHO1{yH3G;@X=kJ9*6FfVamQ#-=7{FfN zG$=SjjE!k#?X<+sMv;^c&R~*Qz4g~?mp4w#HduJqSdwgrSd1+VN$(y$Bpafx_nJ|hn5wjG^%)DrR(JQ7ji~48|#vH zl4N)TqdP@oRCz}n>ezn9ziRsik{}A2UU}tDkDLuqzSv7RJhgJ zFM$oZjkiie{c~G;)HON2I{xkKxtwJEfXAc8uTUkQa9mWY-xH0_A8tEyrt{G^PT}p0 z>ViY}iAyTm+fG*&DSuUZ^!}&3Mn-zZJvYDeFs47()hfQLC^RDdHRnxFVb47jbs4nY z=Wq8i**YwAifO{W9_&b)h~IoB?;gH_$WhVol9Bd^-ci1Odsr+9Gq`lmo`i5J&}u`{j9 zA=KUa&r(QIh243ZaTLT$Fij3zJtqwY56)|?M&|HW4yzdRSs$HMtPOjF`mIMl@obb7 zONo2=(a<6$I_s>F3oQ0qw4&W~r}CXbaRR5AC{f5Iyk6!aN$}-)(t&pY6C%(1T_SCWG0N3@#$5DBfkPIEy z8rQ0$&y3Ud4vW->Suc4}>*2AzKX+5~+NWnS1E1R&)^gHa4&|p@IeI=atU0@`n8tOE zewEbvrhTUWfH1aT1VdKt1C4#3)XfKx_1;;47S-XolfhSSSraePkfZQv{tb@(6bS{N7ZfZa(Dm)?~5|A8%3I{oGy=y z#g?2NDm01UR~cXPFDMQ;FXb0=o(%JOI2l9lce5z_b3Jwo=IC-=1NKC{596n_UyBw34@Rvmj&9Wbtk%}a zxyaj`EsVovf|yhIILTn8lNV*A3v~?p7)38(RTwMN4y=s54;;=(cjfJEq;tI)uA0Q? z+cr<7Q2&|l`|i7}mf83b6ZP1qa$5U^DH$2LGoPG^K0i+C`&oXE`StMd;pj%BY18WP zNZ#wi(dWu4kNrox#}ZO49fo%2YU|)ITExn;Ai>|WDYH2jl zs}|~&@)<>RkxQR~+?n>+No#=vPhi6Bp|td20MY0i=8unpjyKY&F0JN-_R(G)Ywk~y z;$2{RO7EGJaDO~2?dDd~IJnkZB&WDCb(~}*!z78?v;eDa?G9B5>0Bc11+mg-70&g? zOiGciyb1eDd(ErPj|UZbwmJ`Q>edEXciQ-j#9mcsl3;QE@$?x%xSs3VsJINr$yxC{ zCM}_P*9ce>=WMPW-5IebV5O^M^Y=F^$!ZpLbPc!{>b*Q0vVc7Wh&-?3J*G|o@WpQL}xjw20>0h(loAK`x za;gLNt+y|WJc)jqe&%PtAlAK8(JVdh#A{XfbyYsCWJqs5TGAB>$2oeBuF$YYfqEfT z%Xw6m;ON8Ln`Bjl7?);6QPTS!xy+0-x5bFttMHxc$(wclY;joeE_v60!S&t~KcX#% z#9i^5>jPHiFakyi8P%^SMitx%a;YeOsHc3qV8JOGD@Ax=*u;uaDLIMbaf;)UiV;I@ zSm~CwcV~5{5(f#w15dKik42;|%i%&qg)n-FK&sUY)aWJR?_HMhm^ zyOW2a?$bkl6~n0YMPP0Fx>&Vw&n{+*KD!qx-W-=^*5;j_AFIwcb_b1~U)uL1v)Fyp z?Tw*T=I5(Dy()LvlkaeA7aV7mh_tneO8yp17N~t5Qz}qP_hRqIc2{KJvvP^{)sjKF zRdtSVDFz^1%HvK?(np@d!~K|j%W!p`JqE40@2d_2T5XCJEDPNa)jmfvgiFx@^jmwt z=ttf}`O8&ubURo1zEtyp{%l=3XvhbM>q6UM(UG%@{fXB^vE(xl;}9`v`aZYdbRJov z-MuW}YP0sI!@ z&>5jO!ef%AZnTW#*MY;czM>6*uM&N>{hs)BV)kaC>E|DHZEug^hkRKzMd@mD*;^n66^O4J*^z@jNd{s=f8=z<6h~FKnb$Cf24jXdP6H-uP9>7M&LVnUKB2 z%Kj0OIr=Dz&Vt?!hzURmnb_Cr!N%>b*3x{)OHr#2?@IQ9gAHg<6Ub+S+D z432(E5V7r3VnaM_9HFNqwZtM9uB8!ZPY|(*eQChc(#BDAWMg~OL|sV|Z-B{oE%m3t z`26@1p7I3oN)XALhG!bs|4El!I=dFCmZ3Vr7@aTv*d(BnyLup{@n2c{Kp+v^0UbL&>mvx&-}E4+9?Su~GXK>znvk%E9LF zxDS}l?~E_q$SMT{Ezf0rH^)7nZpHbZ;o1L+HTw%M18!QmMKt=V6A**{kf&d(5N&A>`GA!G)JX9%=rf8k}nDuwtN0<9T1 z&MJh=;P4C))(jlyCxp!4@C*^w4E$tn5kh8gc=mtAnt^Mu1R*mxJVS&v19$H%gv{XZ z>}0LkuNgqd3=Ypu)|&mggCS%Fhi4~i&A_X@2q7~#JVS&v11p_|kQp4FA<&xrg_nVq z9{dMCL!31OE1iUp862LStTp>}2SdmV4$lx_&A>`mAY=xIX9%=rf8k}|40a)YhIniC z7hVRg^&Z5}5NOT7N;e^728U-SYt4S06c93l!!v|fv!fMVhmaW@o*}}Tfh#%-Au~8U zL!dSL3oip#bO+*R2()IuDus|49G;!5H3Mg`2q7~#JVT&0`wK4v`OE>t&;G~O41E1I z10gdwJo{gpGK0gjleK2QDus|49G)S< znt_$hLC6da&ra5wft9X9$P5n8{>RqrFT4z_bO+*Rh__~c;bmZ@2M|9)pfv+4UHT7X z_76Nmpf&poF9T<=4e>L?TeH9LGH|W;Aby5GYX*+93?VZ(JVS&v16On&LS}GycCywC zoWVMT%;50sWUU#v)*Ga7WCn+4h_GhhIP(xPgTpgKSThirEkMW&4$qL*?7v~netnb$ zAu~8U`@dq%Kx8%#Au~8U`^TDr$ZQ)zW^j1+|7U9k-X^OMGDBdl8F=vggpk>vA!jFK z&Hg!f;K&Stxn|%vvk)@-Gvw@8tr5IL;=7%n+Ds2CnElgv|a7IYWds z`wK4vD?Nnx*)ivu{cg>`aaJK@cFehEzgshKoOKA9Au!kM*If@Gvp+-5PRN>pGnj*r z*`Fb2CuGgQwO)jf*`Fb2$7;>Mapoap_Gif1v05{5oH+=Y{TXt0tkw)1XBt9ge}^E=q!ZH{tP)gA!`QCU}*&ycedvS#26HXvknV!39&^D=O)4E2HJ63B3jxz}%vp+-5j@6ohpUh1|$n4LMv;TE#2JWgE2$}sEa)tXL{`!nS1{~2p0DU#M1Ro~IjI)2cpXTrG3{K6!6-8#89cMpHa7B?BXa9CT&EMS_oSK0vip)4W znSPq!S|c;g{`G#E;JP6*&i?Iwn#hc^6X>T2z8*zpoSjTRP4GNGW}F>oKTUA=A~VkZ z?S7ibj588s#@X@p(*(~qWX9Ra^wR{_8kuo+GW|5cr_3hA&yX2s$JbBucXtM-X5hX? zW}F>oKTUA=A~VjA8E41bPxI(LMP{5GXFpBwKAnY-88YKcM-7>Ab`1SA!RIM5(wB{&yX2s$JbBucXtM-X5d;QGtQ2)pC))xAT!R6v!5n1 zxf^kraA#!388YLnKYG~3-Lo?5sH<~ZZlQus8363T%u~&qF@houGqj3|qp(Wltd>TX z@UClWT7J1Lg{se2IU(nd%s3N&)!6@z-N&+F;_;;h_IXM_Gi>~uQ+d3F%RL&mUum~C z2&{bO8c_Zw*{vtrBWx&v&9?M#ypV*Y;!>lYE=>ZLBlCt!WFtN0QNQz}?`|P8&X5^r z+In3SmfT+*{{?FXJ^_&#XXKmRrS#*f$70REyAhdjcJ1O96uI_x_UappH{~wP8Y}M5L9b6;{5qkEYWjX_Va^LK;+7P5zscN($ypISi;>7b{bU> zOV9G82*~_1zY~ys^IP08Q(<3 zRG8WeVqJD~3l1$)^G^9azpGCArLYdO-=%`jDi`OJ8I^b0b0M7MEBs0={_^*4kN)0I z6FjSs8E22Fr=nu?BYN^Oxb2=z=kQ7CsBb~;IRhy&!iuk{WtIMMZicoVR`O0mdV<45 zdv!Gi8Q|_kW}I0Oe0G_;sh$6BW%h&rj!oqyf84}t@fjg8(UI2H)n~*2fcrFC7!X3j zGjVGrtT^cjx_)TmLa4_ntHVIpZi6;5;|vX%aTd=w+?<+=v!iFzN#PiemvEd(BnT3Z}_=(C^I+B!KG zd7HC^ao9`{a|$0P`S*UB;94Uy&LoN2v;eDa?G9B5>0Bc11+mg-70&g?OiGciyb1eD zd(ErPj|UZbwmJ`Q>edEXciQ-j#9mcsl3;QE@$?x%xSs3VsJINr$yxC{CM}_P*9ce> zCoYP z0sI!@&>5jO!ef%AZnTW#*MY;czM>6*uM&N>{hs)BV)kX=KamL<{%s4}4oc+7*4Bk!?d?UOJwbzQYQC#y(TGy%Ie8^c=N;veuaN@ZWoD!rN(rd*=XZu_^i5 z`rFKC+-_dX16eec0c6HmHWfv2ootgymbt^26zTk)UAy%GxzA3fdqsoyn{Z)^vfq1a z!rN(rE4l-<*d$yoPN}SHjrAy;b(6peMrNFakA~`4;W)Cm>kGZs;F9WeA+)UHlQ0hB z$LR8{SE#mYVxf9*<_3+_6=F?_Hh4SDf8Lm!@2ENa&=oPjE+wr|7kom!HR0_v!E1`l zIQtje8N8h)xNgXdvtw~*@OGNuiXK8OHdPPSI+-dtRkhX!J&k589$351m1?S|>WK#S za5ni#Kew9z6udg^E`&r${J5o2K5qYC=vNB6R= z5*SpB1RAqf@>`$Z0>_z!8fg}~?Uomy-uS)I=5h7b1n=!RsF9}7B>~24A^P7NZ6dI@ zCcK>{I0IzH88YMSnA{n>ohEqy>_aU!VU&h4Jj;q7W4+~NziyQyGtT}ccLr~#39jfC z)MB%#8Hb#bs_9sJYr@-Ug5zvMEjBN`a-uLBj9m}GKVS$_(yn$Lqqa>8#w7aW$^FPq ztwvM8)iR($*^B34R?!gwV+J0@0YLKwbU{@7>@g~tsOi+6oSZP0!m?t6mr)$KIC>YG-=&X_Yvc5^b`) z(uY}77<1Lmc5aSIoy79zMZZu5cO>vGhHt25c&^&@9Ug3oTOWO1*==(FV$f0!rN*7suXImnF|bul*QyE zYR1K_6l{IvjW`B(25+Ycj)TlNL$Eu8x6=gI8kup1%s5j@8&8PkZ?bDKbNAF zU^PZTfse9H&L!*RVubuGkP)&NcPUdk3IGmugE50|2irOY1U0r=IS>bEFZ6i1t7CLN zDhM&fN-I|(2G6Q#sFCJh8f_x5w<%sZm?SbJ;2+i8N=6q#{`%s4}4oc)XL4Bk!? zeA4bfEj9+ASNmg~Pa*Wx$PZ2PkN9XHRm=Y&nR zsh*nJeJrf{I#}ytR-JsRf_|7m-p5kMsi@)&N(wzr2Km^!{<4Am0lT&-csot7A=d%a zViTEhhRito@3=E~J5BID-GEwbs%&pNU0I|=L~l*-d5X+9LuQ;ElRJaA(*$?bHq>I1 z&h=)vY7(Pw+dP#*{b#=KyYI4EX5&XpkQrzHf;$6ObRKG?`4@zSz}}khcADS}kQryl zjI;lyJ3F?1nty4biOe`dW}NBFK24S#av!`mO7GUhJ7+oVApP^OHzg>(O;t<|?*kU5 zgn}SU@8a?o#R!V{aF6PgSd3S}f~bRBV`q(Zw$~;YS^Ff7ifUyulRCC`D0o!K=Kx@* zai|BfIf4yn6km=aXOGg4!UB8F^dd)NLAJ@CBgHbP=K1q^mtc>5t4f z<0^d4lg=OZ(LeVsUbA*yCcV?DMA7I+mTwc_BV&r>1${w{W*gd0*lf-SQ=$yrXzMA4 zH8+?g@jeO3K8~$d^Rrv5j34?P&70VDABINvAv4ZiF^8UEnDxT6+mrU<9~vEt4Y z_2B8u)9;HEwp$aP``L-h@;GK0QEx0M+PDuWb-O#nz&8y0$c!^&#+jIMdGg}01?-Wk zo&D?I`)PtZWS#6vYm<&spDDb#<~MhF}a>tZt*N(GxBR~r$_pE71-A&cIP*lGxhnV#rDr0!Wun}Q{YX2e1w1ZTiC)eNif6LV1OsH8?&l&TiX1-9-#Z-7_;=^pFOdr>$nqqxm zejk}})&f2OXGmowb0!Gg9JUQ zqE3$=1M@U8<7~maZw{!{1#;wdJxd+9D}Y0{oxTkYVnJAK z0kN;r+SS>V%g>2KbQ-&X_vt1HTT_#VbC_W6p_xuQ?~+NZ{Bq1N{Wvn??7y~V;JGsm zAu~8UJ6UV?&y|BW(nMyQ9fLIkR}`6XhRisV-gN$$y{#r(=Xv|JRD89PT0>tK_8PHh>HPglSIbX%jJ%U};f<{@Ef0kpJHP!m+c~x~+r`33A+6Cz?^UD>1})zj z`0Xe)5{^nMpFdwkK33-DSVGcm!hjoz5Arf_zwMLGW!C68b;?feaaK9ZPRy!~xGQh- zZ-ux{zg9z0GkwMXgQm7ZiKJR4EJ?YxKQMsj>|jY1RBwInJIjoPUMJa(<)X9Ee|GSb(u?I&O0U0p?HoH3gn zR-XzYqSEi@e;d$L+Wj-D=u323aX|02XM9@&HY8!8QN`r)cN_DpqHt%&cb-OWwU7LB zQZO5&+;eUVaCN>C67nIyP$JV7=iDPpWX74E&Gly-u58z0?T8s4?DgSdKfASa|16;+ zsVeob)q`Cul)fS?FHT-pA2Z5Zr70o!Of%x)x6i*hyBKC~B4?=ajeb>%BZQilB_w~k zKoGUIH$qTASY69963>oj`ap>Z#7pN)E% zr({CTOeH#Ij(bXP(B`f;vnS4!eoSOZnFm$0_-o+TQ-;)eq=w&35t(sDhs-#;vG3;K z;N)cFX8MV5VuF@--;w9scFSmpmH$X~)q@I{6^-)!l}8p_vc|^X-kBoDzmPMc*;tn> zVyDa3gm-9JK}w^Fm%NiC!y6dgDH@}S%s6xHET+(Ik#?uB>?lZxJeQ*}l;5HmdDU)4 zl>gQ~#e>7wKLu(_ytpomooQVTq3+gymO_#$?9SVaqaa>_X>#D|IcYEmo_q_0`mHq{ zPK}ZW=e1TNbNDNVRgC$pwZ)JbXCLa*x_1;;47S-XolfhSSSraePkfZQv{tb@(6bS{ zN7ZfZa(Dm)?~5|A8%3I{oGy=y#Rhlp4B6mFp_@~Zq~!Eap-BY4%J`apX<3a5GUM#a z`-ImAvel!5edUd}^oF7=y2>i6rO`yMTBujbXB5#zE`17eXWC;YtpyG|feE*V($a?k zM5A+OMW>S22uQUj){+uZvY1_v~V} z=(BsF;>~esW^LZ-`LXJJV|UQ#`K5hNGK<|e-QE~lWq!We)2niqJ^2o|c4dLu=P{)M zwIDA8+4vz5X=@dg{4LmaS7hL`a*6iUl0mvvb&hZ;1|VF@<4#Y~N1nsO{g{2raCM$N z2Ccd8s}2KNZHg8w3*8UZK1VZzOVI)JTYJDL*hq6qI%voTi0eZ8$eSpCxk`?1=PKWq zYQ7y79XY$$pLk6aOFjcJ4iS^4?{f=I=aD7a-OEB|oB?Y#4byZM^maf@0Lotw+A|@0 ziIsi7kcoY*9(=sJ2G4{7*$l9ns2fRAe`;egk7G{|Xk!-#P$&DO&fw^$1QFXlB{sy< z#u0i-QcEmy;aVDj_5=}|*p~)8Eo~e{M>e)cP1Kbn@dlWT*HV8PjL(lR;VDlLuLP02 zX?UiAoe{wbsNYAWTf`ga^Sr2k>FiplT88QfV|2dwW0QbRWX9QTTx7->GUMzYcLt|s zzh3k~{0xEa418ovk_fNXd$`G<{iV@n-=63al_%W+nUmi+@x?3tMnFepVGA3LU37*}pW}B+MUCySZ1-{(7;_#Q(;P%m~xvi|Fx#f+Y%(SdYW|F82zQ z+Ej~#CRyl;bth*cGtQbXgDbiKwb(RytvSp>6|XRa`HJCEh>|-hF|Eih8e?})FkR=( zpgJsjsoU)>p;nt>;?#;%A6(XUL4R<927rjI-;(-e-@qwtcshvHK1y4iiVaxxJa&l_F+ZBy^4W5 zy1-1{s^p=-vIr1-v-#H-Lur*YLhrSxm|}EOd&Yk&AGV=*LPk&^OmY(o-(l2_S7c~? zMe<%$91pFZXdK77y4@Ty^h>D^R0vYg#*kKZ!Ip; zvuANWoD!~oqo#|4?YG>bXR5lbV0EY0F<;lQ-QM;+_=;$<6SV|tg=T+W9~11)80|BgF@Q?q|g z3OGMIS$9@Su+(&knf}*FL3s3a2pv~6EruouyzF{o*YfNukCKoXXUL4R|H7TY+i8Nk zcM;-en6Iy<7v$%Z4yt|maK(&0uFJXm4%=XXD*lzTOgyhSzjV#uFmX>wAT!SX#on66 zmWHHv4n}P_(95BsY_IHT>?uI#=5FuKt?9Pby4SIZ%s4}4oc-?3;M5G< zJL?cXLxekX>^Z=7z3~w|4|XmxbC>A2G|G<3vxH>mxYoE<0l@pTJ!HliGUM!DbZ2mC z2JYTvh@ThohitkmSu60kBpg-ziz+*i&YQO)lz4wmC>TewX zFN!1?*(5?p$_SOcvu9SgY?+zK-c)2}WbZu+QIU}{BHKk$i0te=&)0EZ*ZuhYKHt8d z&wan|E|2?jKTd!4&pFpQuW_8~c|D({hGu@&3b0v1zI|4r?Xkt{x#fU}HGb|7O9G-S z8=kzESBrEizS>C$+KS6Tjx)${_M$pFtc_lObwgb@=_3hCwhziFF%en$HA${Ql?VsjHn-(Qs#v$>KFh|BU8QqDn=<)-nKL^m$ zJsGqfHx>?Gkq`c*O5Yt;=%w@7;B?Vm%c}LGcuK*;L#+d$Kxn1L)sOrOsXmb7OiB&h zo>!<6$qxSj3Xx_d%M5nG3dCB;T;6lh%GD60j1TV*dt87JjMYus@*Mx1%YLxV!AyW> znm@EwQx~`B8jF~>O%HnSaRFC&$qu&!a-4C~96j(*s0tmyUn6l`7?0(Ia+1|7YZf}) z`uYbnlg&7HyF%bbRZg74S*HSS&FHL#_S8;6jx)${)&`E9+4IPdCNi#hygf~Dg@qhv z23<7PeBYe^iJm5S-KNNb7aHI$jq*tFEU@W=(Zq&go`b66dXBb@q|N;BxB;0$;>K%! zH~_TD9g^MQ`aApHhoN_3*3~Rnu}9SjEAop1cT4TfZfdXvk#^HPf1V-!J@FS;?ajFN z=f)a7CH?I<7U0(v5}zLhki^oPuIA??Sk%RZDPCA@Y`;TV)xQ8J^?GW%)WltX_9&dN zYlEO88v9KnO(@8Mj?DkhxU7szo`s6N)068L^L(fEqc?ouv zHS(72_wL2+Q^t*&trW%YT-bRq-IVg+dKxbo!Q;%3yY?JDSE-u>sMmKmZk1Dq-Dnyl zxIyZqMP#gbF(6yzu>-H`hs$zmu?3eqrkR8#;;*g*%E|_$-yFKe%c^?kbsT$JQzQ0D zX+^ZGMuK5XNi5C6_u?TRb|Uk*rXkhzu|zc)aL1UWlrt(3^0@Qzr%QYMs?Z+AbG*~I>2@Ez9?>79oo?;==ZDjc{V#~I`}TYP9`29D8XR7evU*F;B86C9(E znRuPE|HDB}R&v2I4N;H@qhiM5eBrtHwUjH&aw`<%~W0B|BvU0Rqq98NK zakkZ+X*c>KVroEwK~)lRob5u6v;ToL19zr16l8`3&(Oh|f%9~c7IK{7UOiQ`XKbj+ zK>M3p z@kvIAMoL{?p^I?$g9Y+mbH@Zn?;iEIVsVN`8@CUW_a?@hTeKQ1x^%Y6y1n&GA4GF; z-^_(Ea&qrEp4YR#$L&Vks0=yIws(0#{jTCPa2ALqwe;My;vj8fkge2cOiixg2IuK6 zZKhe)hS?d$N_J;r9-I)V⪙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 WAFxXIaqo0QtLWSJ6d3>D=lVaJBc=5K literal 0 HcmV?d00001 diff --git a/apps/native/android/app/src/main/res/raw/fengying_notification.wav b/apps/native/android/app/src/main/res/raw/fengying_notification.wav new file mode 100644 index 0000000000000000000000000000000000000000..705db4958d34f85057be1bb5e7aed354bc9dfcf9 GIT binary patch literal 594510 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 z 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%0A&9%CJ7S&%I%y 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�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 + + + + diff --git a/apps/native/android/app/src/main/res/values-night/styles.xml b/apps/native/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/apps/native/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/native/android/app/src/main/res/values-v35/styles.xml b/apps/native/android/app/src/main/res/values-v35/styles.xml new file mode 100644 index 0000000..0f7a45e --- /dev/null +++ b/apps/native/android/app/src/main/res/values-v35/styles.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/apps/native/android/app/src/main/res/values/styles.xml b/apps/native/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/apps/native/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/apps/native/android/app/src/main/res/xml/file_paths.xml b/apps/native/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..4a1b1a5 --- /dev/null +++ b/apps/native/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/native/android/app/src/profile/AndroidManifest.xml b/apps/native/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/apps/native/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/apps/native/android/build.gradle b/apps/native/android/build.gradle new file mode 100644 index 0000000..d71850a --- /dev/null +++ b/apps/native/android/build.gradle @@ -0,0 +1,27 @@ +allprojects { + repositories { + google() + mavenCentral() + maven { url "https://developer.huawei.com/repo/" } + maven { url "https://repos.vivo.com.cn/maven/repository/external-lib/" } + maven { + url "https://maven.columbus.heytapmobi.com/repository/releases/" + credentials { + username "nexus" + password "c0b08da17e3ec36c3870fed674a0bcb36abc2e23" + } + } + } +} + +rootProject.buildDir = "../build" +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/apps/native/android/gradle.properties b/apps/native/android/gradle.properties new file mode 100644 index 0000000..f43af91 --- /dev/null +++ b/apps/native/android/gradle.properties @@ -0,0 +1,8 @@ +org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true +apmsInstrumentationEnabled=false +# This builtInKotlin flag was added automatically by Flutter migrator +android.builtInKotlin=false +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false diff --git a/apps/native/android/gradle/wrapper/gradle-wrapper.properties b/apps/native/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..efdcc4a --- /dev/null +++ b/apps/native/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip diff --git a/apps/native/android/settings.gradle b/apps/native/android/settings.gradle new file mode 100644 index 0000000..f4cdfab --- /dev/null +++ b/apps/native/android/settings.gradle @@ -0,0 +1,34 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + maven { url "https://developer.huawei.com/repo/" } + maven { url "https://repos.vivo.com.cn/maven/repository/external-lib/" } + maven { + url "https://maven.columbus.heytapmobi.com/repository/releases/" + credentials { + username "nexus" + password "c0b08da17e3ec36c3870fed674a0bcb36abc2e23" + } + } + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "8.9.1" apply false + id "org.jetbrains.kotlin.android" version "1.9.24" apply false +} + +include ":app" diff --git a/apps/native/assets/brand/fengying-logo-512.png b/apps/native/assets/brand/fengying-logo-512.png new file mode 100644 index 0000000000000000000000000000000000000000..0e9f647a9719a628611e0b786766ea4ed304ecb6 GIT binary patch literal 25574 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

?)Y|$h}sn@cP=`zFZyIZb`OXg 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 literal 0 HcmV?d00001 diff --git a/apps/native/assets/sounds/ring.wav b/apps/native/assets/sounds/ring.wav new file mode 100644 index 0000000000000000000000000000000000000000..dd619a014c09cda9bfe220e2de673e72e1c84c5a GIT binary patch literal 52998 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