From 69ff0f369685669d9450ed8eb94d79048a234aab Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 27 Jan 2025 22:43:03 +0800 Subject: [PATCH 01/10] add --- pnpm-lock.yaml | 105 +++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 51 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ae81f3d..ddc16bf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -142,7 +142,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.0.0 - version: 10.4.9(@swc/core@1.10.6) + version: 10.4.9(@swc/core@1.10.6(@swc/helpers@0.5.15)) '@nestjs/schematics': specifier: ^10.0.0 version: 10.2.3(chokidar@3.6.0)(typescript@5.7.2) @@ -196,7 +196,7 @@ importers: version: 5.2.1(@types/eslint@9.6.1)(eslint-config-prettier@9.1.0(eslint@8.57.1))(eslint@8.57.1)(prettier@3.4.2) jest: specifier: ^29.5.0 - version: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)) + version: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) prettier: specifier: ^3.0.0 version: 3.4.2 @@ -208,13 +208,13 @@ importers: version: 6.3.4 ts-jest: specifier: ^29.1.0 - version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)))(typescript@5.7.2) + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)))(typescript@5.7.2) ts-loader: specifier: ^9.4.3 - version: 9.5.1(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.10.6)) + version: 9.5.1(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))) ts-node: specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2) + version: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2) tsconfig-paths: specifier: ^4.2.0 version: 4.2.0 @@ -299,6 +299,9 @@ importers: '@nice/ui': specifier: workspace:^ version: link:../../packages/ui + '@nice/utils': + specifier: workspace:^ + version: link:../../packages/utils '@tanstack/query-async-storage-persister': specifier: ^5.51.9 version: 5.62.16 @@ -443,7 +446,7 @@ importers: version: 8.4.49 tailwindcss: specifier: ^3.4.10 - version: 3.4.17(ts-node@10.9.2(@types/node@20.17.12)(typescript@5.7.2)) + version: 3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) typescript: specifier: ^5.5.4 version: 5.7.2 @@ -504,7 +507,7 @@ importers: version: 6.0.1 tsup: specifier: ^8.3.5 - version: 8.3.5(@swc/core@1.10.6)(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) + version: 8.3.5(@swc/core@1.10.6(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) typescript: specifier: ^5.5.4 version: 5.7.2 @@ -538,10 +541,10 @@ importers: version: 6.0.1 ts-node: specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2) + version: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2) tsup: specifier: ^8.3.5 - version: 8.3.5(@swc/core@1.10.6)(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) + version: 8.3.5(@swc/core@1.10.6(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) typescript: specifier: ^5.5.4 version: 5.7.2 @@ -665,10 +668,10 @@ importers: version: 6.0.1 ts-node: specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2) + version: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2) tsup: specifier: ^8.3.5 - version: 8.3.5(@swc/core@1.10.6)(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) + version: 8.3.5(@swc/core@1.10.6(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) typescript: specifier: ^5.5.4 version: 5.7.2 @@ -686,10 +689,10 @@ importers: version: 6.0.1 ts-node: specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2) + version: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2) tsup: specifier: ^8.3.5 - version: 8.3.5(@swc/core@1.10.6)(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) + version: 8.3.5(@swc/core@1.10.6(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) typescript: specifier: ^5.5.4 version: 5.7.2 @@ -741,10 +744,10 @@ importers: version: 13.2.3 ts-node: specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2) + version: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2) tsup: specifier: ^8.3.5 - version: 8.3.5(@swc/core@1.10.6)(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) + version: 8.3.5(@swc/core@1.10.6(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) typescript: specifier: ^5.5.4 version: 5.7.2 @@ -799,10 +802,10 @@ importers: version: 6.0.1 ts-node: specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2) + version: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2) tsup: specifier: ^8.3.5 - version: 8.3.5(@swc/core@1.10.6)(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) + version: 8.3.5(@swc/core@1.10.6(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) typescript: specifier: ^5.5.4 version: 5.7.2 @@ -820,10 +823,10 @@ importers: version: 6.0.1 ts-node: specifier: ^10.9.1 - version: 10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2) + version: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2) tsup: specifier: ^8.3.5 - version: 8.3.5(@swc/core@1.10.6)(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) + version: 8.3.5(@swc/core@1.10.6(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0) typescript: specifier: ^5.5.4 version: 5.7.2 @@ -8627,7 +8630,7 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2))': + '@jest/core@29.7.0(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2))': dependencies: '@jest/console': 29.7.0 '@jest/reporters': 29.7.0 @@ -8641,7 +8644,7 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 @@ -8845,7 +8848,7 @@ snapshots: bullmq: 5.34.8 tslib: 2.8.1 - '@nestjs/cli@10.4.9(@swc/core@1.10.6)': + '@nestjs/cli@10.4.9(@swc/core@1.10.6(@swc/helpers@0.5.15))': dependencies: '@angular-devkit/core': 17.3.11(chokidar@3.6.0) '@angular-devkit/schematics': 17.3.11(chokidar@3.6.0) @@ -8855,7 +8858,7 @@ snapshots: chokidar: 3.6.0 cli-table3: 0.6.5 commander: 4.1.1 - fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.10.6)) + fork-ts-checker-webpack-plugin: 9.0.2(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))) glob: 10.4.5 inquirer: 8.2.6 node-emoji: 1.11.0 @@ -8864,7 +8867,7 @@ snapshots: tsconfig-paths: 4.2.0 tsconfig-paths-webpack-plugin: 4.2.0 typescript: 5.7.2 - webpack: 5.97.1(@swc/core@1.10.6) + webpack: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15)) webpack-node-externals: 3.0.0 optionalDependencies: '@swc/core': 1.10.6(@swc/helpers@0.5.15) @@ -11026,13 +11029,13 @@ snapshots: crc-32: 1.2.2 readable-stream: 3.6.2 - create-jest@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)): + create-jest@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -11763,7 +11766,7 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 - fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.10.6)): + fork-ts-checker-webpack-plugin@9.0.2(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))): dependencies: '@babel/code-frame': 7.26.2 chalk: 4.1.2 @@ -11778,7 +11781,7 @@ snapshots: semver: 7.6.3 tapable: 2.2.1 typescript: 5.7.2 - webpack: 5.97.1(@swc/core@1.10.6) + webpack: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15)) form-data@4.0.1: dependencies: @@ -12226,16 +12229,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)): + jest-cli@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)) + create-jest: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)) + jest-config: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -12245,7 +12248,7 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)): + jest-config@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)): dependencies: '@babel/core': 7.26.0 '@jest/test-sequencer': 29.7.0 @@ -12271,7 +12274,7 @@ snapshots: strip-json-comments: 3.1.1 optionalDependencies: '@types/node': 20.17.12 - ts-node: 10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2) + ts-node: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -12497,12 +12500,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)): + jest@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)): dependencies: - '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)) + '@jest/core': 29.7.0(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)) + jest-cli: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -13305,13 +13308,13 @@ snapshots: camelcase-css: 2.0.1 postcss: 8.4.49 - postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@20.17.12)(typescript@5.7.2)): + postcss-load-config@4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)): dependencies: lilconfig: 3.1.3 yaml: 2.7.0 optionalDependencies: postcss: 8.4.49 - ts-node: 10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2) + ts-node: 10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2) postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.4.49)(yaml@2.7.0): dependencies: @@ -14403,7 +14406,7 @@ snapshots: tailwind-merge@2.6.0: {} - tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.17.12)(typescript@5.7.2)): + tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -14422,7 +14425,7 @@ snapshots: postcss: 8.4.49 postcss-import: 15.1.0(postcss@8.4.49) postcss-js: 4.0.1(postcss@8.4.49) - postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@types/node@20.17.12)(typescript@5.7.2)) + postcss-load-config: 4.0.2(postcss@8.4.49)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) postcss-nested: 6.2.0(postcss@8.4.49) postcss-selector-parser: 6.1.2 resolve: 1.22.10 @@ -14452,14 +14455,14 @@ snapshots: minimatch: 3.1.2 resolve-from: 2.0.0 - terser-webpack-plugin@5.3.11(@swc/core@1.10.6)(webpack@5.97.1(@swc/core@1.10.6)): + terser-webpack-plugin@5.3.11(@swc/core@1.10.6(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))): dependencies: '@jridgewell/trace-mapping': 0.3.25 jest-worker: 27.5.1 schema-utils: 4.3.0 serialize-javascript: 6.0.2 terser: 5.37.0 - webpack: 5.97.1(@swc/core@1.10.6) + webpack: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15)) optionalDependencies: '@swc/core': 1.10.6(@swc/helpers@0.5.15) @@ -14545,12 +14548,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)))(typescript@5.7.2): + ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 fast-json-stable-stringify: 2.1.0 - jest: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2)) + jest: 29.7.0(@types/node@20.17.12)(ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2)) jest-util: 29.7.0 json5: 2.2.3 lodash.memoize: 4.1.2 @@ -14564,7 +14567,7 @@ snapshots: '@jest/types': 29.6.3 babel-jest: 29.7.0(@babel/core@7.26.0) - ts-loader@9.5.1(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.10.6)): + ts-loader@9.5.1(typescript@5.7.2)(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))): dependencies: chalk: 4.1.2 enhanced-resolve: 5.18.0 @@ -14572,9 +14575,9 @@ snapshots: semver: 7.6.3 source-map: 0.7.4 typescript: 5.7.2 - webpack: 5.97.1(@swc/core@1.10.6) + webpack: 5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15)) - ts-node@10.9.2(@swc/core@1.10.6)(@types/node@20.17.12)(typescript@5.7.2): + ts-node@10.9.2(@swc/core@1.10.6(@swc/helpers@0.5.15))(@types/node@20.17.12)(typescript@5.7.2): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 @@ -14611,7 +14614,7 @@ snapshots: tslib@2.8.1: {} - tsup@8.3.5(@swc/core@1.10.6)(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0): + tsup@8.3.5(@swc/core@1.10.6(@swc/helpers@0.5.15))(jiti@1.21.7)(postcss@8.4.49)(typescript@5.7.2)(yaml@2.7.0): dependencies: bundle-require: 5.1.0(esbuild@0.24.2) cac: 6.7.14 @@ -14815,7 +14818,7 @@ snapshots: webpack-sources@3.2.3: {} - webpack@5.97.1(@swc/core@1.10.6): + webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15)): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.6 @@ -14837,7 +14840,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 3.3.0 tapable: 2.2.1 - terser-webpack-plugin: 5.3.11(@swc/core@1.10.6)(webpack@5.97.1(@swc/core@1.10.6)) + terser-webpack-plugin: 5.3.11(@swc/core@1.10.6(@swc/helpers@0.5.15))(webpack@5.97.1(@swc/core@1.10.6(@swc/helpers@0.5.15))) watchpack: 2.4.2 webpack-sources: 3.2.3 transitivePeerDependencies: From f149f20052644f953b33dd073b5317e756c97cdf Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 27 Jan 2025 22:43:05 +0800 Subject: [PATCH 02/10] add --- apps/server/src/main.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index 1875a5f..8b82ed1 100755 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -8,7 +8,7 @@ async function bootstrap() { // 启用 CORS 并允许所有来源 app.enableCors({ - origin: "*", + origin: '*', }); const wsService = app.get(WebSocketService); await wsService.initialize(app.getHttpServer()); @@ -18,6 +18,5 @@ async function bootstrap() { const port = process.env.SERVER_PORT || 3000; await app.listen(port); - } bootstrap(); From 6b67107b8c26f4a08eda3af8864df88f3f302161 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 27 Jan 2025 22:43:07 +0800 Subject: [PATCH 03/10] ad --- apps/server/src/auth/auth.service.ts | 67 +++++++++++++++------------- 1 file changed, 37 insertions(+), 30 deletions(-) diff --git a/apps/server/src/auth/auth.service.ts b/apps/server/src/auth/auth.service.ts index 82a65b9..f7f679c 100755 --- a/apps/server/src/auth/auth.service.ts +++ b/apps/server/src/auth/auth.service.ts @@ -4,14 +4,9 @@ import { BadRequestException, Logger, InternalServerErrorException, - } from '@nestjs/common'; import { StaffService } from '../models/staff/staff.service'; -import { - db, - AuthSchema, - JwtPayload, -} from '@nice/common'; +import { db, AuthSchema, JwtPayload } from '@nice/common'; import * as argon2 from 'argon2'; import { JwtService } from '@nestjs/jwt'; import { redis } from '@server/utils/redis/redis.service'; @@ -24,14 +19,12 @@ import { TusService } from '@server/upload/tus.service'; import { extractFileIdFromNginxUrl } from '@server/upload/utils'; @Injectable() export class AuthService { - private logger = new Logger(AuthService.name) + private logger = new Logger(AuthService.name); constructor( private readonly staffService: StaffService, private readonly jwtService: JwtService, - private readonly sessionService: SessionService - ) { - - } + private readonly sessionService: SessionService, + ) {} async validateFileRequest(params: FileRequest): Promise { try { // 基础参数验证 @@ -39,27 +32,32 @@ export class AuthService { return { isValid: false, error: FileValidationErrorType.INVALID_URI }; } const fileId = extractFileIdFromNginxUrl(params.originalUri); - console.log(params.originalUri, fileId) + console.log(params.originalUri, fileId); const resource = await db.resource.findFirst({ where: { fileId } }); // 资源验证 if (!resource) { - return { isValid: false, error: FileValidationErrorType.RESOURCE_NOT_FOUND }; + return { + isValid: false, + error: FileValidationErrorType.RESOURCE_NOT_FOUND, + }; } // 处理公开资源 if (resource.isPublic) { - return { isValid: true, - resourceType: resource.type || 'unknown' + resourceType: resource.type || 'unknown', }; } // 处理私有资源 const token = extractTokenFromAuthorization(params.authorization); if (!token) { - return { isValid: false, error: FileValidationErrorType.AUTHORIZATION_REQUIRED }; + return { + isValid: false, + error: FileValidationErrorType.AUTHORIZATION_REQUIRED, + }; } - const payload: JwtPayload = await this.jwtService.verify(token) + const payload: JwtPayload = await this.jwtService.verify(token); if (!payload.sub) { return { isValid: false, error: FileValidationErrorType.INVALID_TOKEN }; } @@ -67,9 +65,8 @@ export class AuthService { return { isValid: true, userId: payload.sub, - resourceType: resource.type || 'unknown' + resourceType: resource.type || 'unknown', }; - } catch (error) { this.logger.error('File validation error:', error); return { isValid: false, error: FileValidationErrorType.UNKNOWN_ERROR }; @@ -93,7 +90,9 @@ export class AuthService { return { accessToken, refreshToken }; } - async signIn(data: z.infer): Promise { + async signIn( + data: z.infer, + ): Promise { const { username, password, phoneNumber } = data; let staff = await db.staff.findFirst({ @@ -113,7 +112,8 @@ export class AuthService { if (!staff.enabled) { throw new UnauthorizedException('帐号已禁用'); } - const isPasswordMatch = phoneNumber || await argon2.verify(staff.password, password); + const isPasswordMatch = + phoneNumber || (await argon2.verify(staff.password, password)); if (!isPasswordMatch) { throw new UnauthorizedException('帐号或密码错误'); } @@ -143,7 +143,7 @@ export class AuthService { const existingUser = await db.staff.findFirst({ where: { OR: [{ username }, { officerId }, { phoneNumber }], - deletedAt: null + deletedAt: null, }, }); @@ -155,7 +155,7 @@ export class AuthService { data: { ...data, domainId: data.deptId, - } + }, }); } async refreshToken(data: z.infer) { @@ -168,12 +168,17 @@ export class AuthService { throw new UnauthorizedException('用户会话已过期'); } - const session = await this.sessionService.getSession(payload.sub, sessionId); + const session = await this.sessionService.getSession( + payload.sub, + sessionId, + ); if (!session || session.refresh_token !== refreshToken) { throw new UnauthorizedException('用户会话已过期'); } - const user = await db.staff.findUnique({ where: { id: payload.sub, deletedAt: null } }); + const user = await db.staff.findUnique({ + where: { id: payload.sub, deletedAt: null }, + }); if (!user) { throw new UnauthorizedException('用户不存在'); } @@ -186,14 +191,17 @@ export class AuthService { const updatedSession = { ...session, access_token: accessToken, - access_token_expires_at: Date.now() + tokenConfig.accessToken.expirationMs, + access_token_expires_at: + Date.now() + tokenConfig.accessToken.expirationMs, }; await this.sessionService.saveSession( payload.sub, updatedSession, tokenConfig.accessToken.expirationTTL, ); - await redis.del(UserProfileService.instance.getProfileCacheKey(payload.sub)); + await redis.del( + UserProfileService.instance.getProfileCacheKey(payload.sub), + ); return { access_token: accessToken, access_token_expires_at: updatedSession.access_token_expires_at, @@ -212,7 +220,7 @@ export class AuthService { where: { id: user?.id }, data: { password: newPassword, - } + }, }); return { message: '密码已修改' }; @@ -232,5 +240,4 @@ export class AuthService { return { message: '注销成功' }; } - -} \ No newline at end of file +} From 8c87e39c6a76b5716e0c6830e4b03b79988d090b Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 27 Jan 2025 22:43:10 +0800 Subject: [PATCH 04/10] add --- .../server/src/models/course/course.router.ts | 160 +++++++++--------- 1 file changed, 84 insertions(+), 76 deletions(-) diff --git a/apps/server/src/models/course/course.router.ts b/apps/server/src/models/course/course.router.ts index f08d35c..4ed98c4 100644 --- a/apps/server/src/models/course/course.router.ts +++ b/apps/server/src/models/course/course.router.ts @@ -3,84 +3,92 @@ import { TrpcService } from '@server/trpc/trpc.service'; import { Prisma, UpdateOrderSchema } from '@nice/common'; import { CourseService } from './course.service'; import { z, ZodType } from 'zod'; -const CourseCreateArgsSchema: ZodType = z.any() -const CourseUpdateArgsSchema: ZodType = z.any() -const CourseCreateManyInputSchema: ZodType = z.any() -const CourseDeleteManyArgsSchema: ZodType = z.any() -const CourseFindManyArgsSchema: ZodType = z.any() -const CourseFindFirstArgsSchema: ZodType = z.any() -const CourseWhereInputSchema: ZodType = z.any() -const CourseSelectSchema: ZodType = z.any() +const CourseCreateArgsSchema: ZodType = z.any(); +const CourseUpdateArgsSchema: ZodType = z.any(); +const CourseCreateManyInputSchema: ZodType = + z.any(); +const CourseDeleteManyArgsSchema: ZodType = + z.any(); +const CourseFindManyArgsSchema: ZodType = z.any(); +const CourseFindFirstArgsSchema: ZodType = z.any(); +const CourseWhereInputSchema: ZodType = z.any(); +const CourseSelectSchema: ZodType = z.any(); @Injectable() export class CourseRouter { - constructor( - private readonly trpc: TrpcService, - private readonly courseService: CourseService, - ) { } - router = this.trpc.router({ - create: this.trpc.protectProcedure - .input(CourseCreateArgsSchema) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.courseService.create(input, { staff }); - }), - update: this.trpc.protectProcedure - .input(CourseUpdateArgsSchema) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; - return await this.courseService.update(input, { staff }); - }), - createMany: this.trpc.protectProcedure.input(z.array(CourseCreateManyInputSchema)) - .mutation(async ({ ctx, input }) => { - const { staff } = ctx; + constructor( + private readonly trpc: TrpcService, + private readonly courseService: CourseService, + ) {} + router = this.trpc.router({ + create: this.trpc.protectProcedure + .input(CourseCreateArgsSchema) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; - return await this.courseService.createMany({ data: input }, staff); - }), - deleteMany: this.trpc.procedure - .input(CourseDeleteManyArgsSchema) - .mutation(async ({ input }) => { - return await this.courseService.deleteMany(input); - }), - findFirst: this.trpc.procedure - .input(CourseFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword - .query(async ({ input }) => { - return await this.courseService.findFirst(input); - }), - softDeleteByIds: this.trpc.protectProcedure - .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema - .mutation(async ({ input }) => { - return this.courseService.softDeleteByIds(input.ids); - }), - updateOrder: this.trpc.protectProcedure - .input(UpdateOrderSchema) - .mutation(async ({ input }) => { - return this.courseService.updateOrder(input); - }), - findMany: this.trpc.procedure - .input(CourseFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword - .query(async ({ input }) => { - return await this.courseService.findMany(input); - }), - findManyWithCursor: this.trpc.protectProcedure - .input(z.object({ - cursor: z.any().nullish(), - take: z.number().optional(), - where: CourseWhereInputSchema.optional(), - select: CourseSelectSchema.optional() - })) - .query(async ({ ctx, input }) => { - return await this.courseService.findManyWithCursor(input); - }), - findManyWithPagination: this.trpc.procedure - .input(z.object({ - page: z.number().optional(), - pageSize: z.number().optional(), - where: CourseWhereInputSchema.optional(), - select: CourseSelectSchema.optional() - })) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword - .query(async ({ input }) => { - return await this.courseService.findManyWithPagination(input); - }), - }); + return await this.courseService.create(input, { staff }); + }), + update: this.trpc.protectProcedure + .input(CourseUpdateArgsSchema) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + return await this.courseService.update(input, { staff }); + }), + createMany: this.trpc.protectProcedure + .input(z.array(CourseCreateManyInputSchema)) + .mutation(async ({ ctx, input }) => { + const { staff } = ctx; + + return await this.courseService.createMany({ data: input }, staff); + }), + deleteMany: this.trpc.procedure + .input(CourseDeleteManyArgsSchema) + .mutation(async ({ input }) => { + return await this.courseService.deleteMany(input); + }), + findFirst: this.trpc.procedure + .input(CourseFindFirstArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.courseService.findFirst(input); + }), + softDeleteByIds: this.trpc.protectProcedure + .input(z.object({ ids: z.array(z.string()) })) // expect input according to the schema + .mutation(async ({ input }) => { + return this.courseService.softDeleteByIds(input.ids); + }), + updateOrder: this.trpc.protectProcedure + .input(UpdateOrderSchema) + .mutation(async ({ input }) => { + return this.courseService.updateOrder(input); + }), + findMany: this.trpc.procedure + .input(CourseFindManyArgsSchema) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.courseService.findMany(input); + }), + findManyWithCursor: this.trpc.protectProcedure + .input( + z.object({ + cursor: z.any().nullish(), + take: z.number().optional(), + where: CourseWhereInputSchema.optional(), + select: CourseSelectSchema.optional(), + }), + ) + .query(async ({ ctx, input }) => { + return await this.courseService.findManyWithCursor(input); + }), + findManyWithPagination: this.trpc.procedure + .input( + z.object({ + page: z.number().optional(), + pageSize: z.number().optional(), + where: CourseWhereInputSchema.optional(), + select: CourseSelectSchema.optional(), + }), + ) // Assuming StaffMethodSchema.findMany is the Zod schema for finding staffs by keyword + .query(async ({ input }) => { + return await this.courseService.findManyWithPagination(input); + }), + }); } From 88b66c50bf52f0dbeb851f58e1c7823e74079b86 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 27 Jan 2025 22:43:15 +0800 Subject: [PATCH 05/10] add --- apps/server/src/auth/auth.controller.ts | 42 +- .../src/models/base/row-model.service.ts | 497 ++++++++++-------- apps/server/src/models/course/utils.ts | 77 +-- 3 files changed, 354 insertions(+), 262 deletions(-) diff --git a/apps/server/src/auth/auth.controller.ts b/apps/server/src/auth/auth.controller.ts index e396a51..de67161 100755 --- a/apps/server/src/auth/auth.controller.ts +++ b/apps/server/src/auth/auth.controller.ts @@ -1,4 +1,19 @@ -import { Controller, Headers, Post, Body, UseGuards, Get, Req, HttpException, HttpStatus, BadRequestException, InternalServerErrorException, NotFoundException, UnauthorizedException, Logger } from '@nestjs/common'; +import { + Controller, + Headers, + Post, + Body, + UseGuards, + Get, + Req, + HttpException, + HttpStatus, + BadRequestException, + InternalServerErrorException, + NotFoundException, + UnauthorizedException, + Logger, +} from '@nestjs/common'; import { AuthService } from './auth.service'; import { AuthSchema, JwtPayload } from '@nice/common'; import { AuthGuard } from './auth.guard'; @@ -7,8 +22,8 @@ import { z } from 'zod'; import { FileValidationErrorType } from './types'; @Controller('auth') export class AuthController { - private logger = new Logger(AuthController.name) - constructor(private readonly authService: AuthService) { } + private logger = new Logger(AuthController.name); + constructor(private readonly authService: AuthService) {} @Get('file') async authFileRequset( @Headers('x-original-uri') originalUri: string, @@ -18,7 +33,6 @@ export class AuthController { @Headers('host') host: string, @Headers('authorization') authorization: string, ) { - try { const fileRequest = { originalUri, @@ -26,10 +40,11 @@ export class AuthController { method, queryParams, host, - authorization + authorization, }; - const authResult = await this.authService.validateFileRequest(fileRequest); + const authResult = + await this.authService.validateFileRequest(fileRequest); if (!authResult.isValid) { // 使用枚举类型进行错误处理 switch (authResult.error) { @@ -41,7 +56,9 @@ export class AuthController { case FileValidationErrorType.INVALID_TOKEN: throw new UnauthorizedException(authResult.error); default: - throw new InternalServerErrorException(authResult.error || FileValidationErrorType.UNKNOWN_ERROR); + throw new InternalServerErrorException( + authResult.error || FileValidationErrorType.UNKNOWN_ERROR, + ); } } return { @@ -51,17 +68,20 @@ export class AuthController { }, }; } catch (error: any) { - this.logger.verbose(`File request auth failed from ${realIp} reason:${error.message}`) + this.logger.verbose( + `File request auth failed from ${realIp} reason:${error.message}`, + ); throw error; } } @UseGuards(AuthGuard) @Get('user-profile') async getUserProfile(@Req() request: Request) { - const payload: JwtPayload = (request as any).user; - const { staff } = await UserProfileService.instance.getUserProfileById(payload.sub); - return staff + const { staff } = await UserProfileService.instance.getUserProfileById( + payload.sub, + ); + return staff; } @Post('login') async login(@Body() body: z.infer) { diff --git a/apps/server/src/models/base/row-model.service.ts b/apps/server/src/models/base/row-model.service.ts index 406beab..a0cbb6f 100644 --- a/apps/server/src/models/base/row-model.service.ts +++ b/apps/server/src/models/base/row-model.service.ts @@ -1,238 +1,307 @@ -import { Logger } from "@nestjs/common"; -import { UserProfile, db, RowModelRequest } from "@nice/common"; +import { Logger } from '@nestjs/common'; +import { UserProfile, db, RowModelRequest } from '@nice/common'; import { LogicalCondition, OperatorType, SQLBuilder } from './sql-builder'; export interface GetRowOptions { - id?: string; - ids?: string[]; - extraCondition?: LogicalCondition; - staff?: UserProfile; + id?: string; + ids?: string[]; + extraCondition?: LogicalCondition; + staff?: UserProfile; } export abstract class RowModelService { - private keywords: Set = new Set([ - 'SELECT', 'FROM', 'WHERE', 'ORDER', 'BY', 'GROUP', 'JOIN', 'AND', 'OR' - // 添加更多需要引号的关键词 - ]); - protected logger = new Logger(this.tableName); - protected constructor(protected tableName: string) { } - protected async getRowDto(row: any, staff?: UserProfile): Promise { - return row; + private keywords: Set = new Set([ + 'SELECT', + 'FROM', + 'WHERE', + 'ORDER', + 'BY', + 'GROUP', + 'JOIN', + 'AND', + 'OR', + // 添加更多需要引号的关键词 + ]); + protected logger = new Logger(this.tableName); + protected constructor(protected tableName: string) {} + protected async getRowDto(row: any, staff?: UserProfile): Promise { + return row; + } + protected async getRowsSqlWrapper( + sql: string, + request?: RowModelRequest, + staff?: UserProfile, + ) { + if (request) return SQLBuilder.join([sql, this.getLimitSql(request)]); + return sql; + } + protected getLimitSql(request: RowModelRequest) { + return SQLBuilder.limit( + request.endRow - request.startRow, + request.startRow, + ); + } + abstract createJoinSql(request?: RowModelRequest): string[]; + async getRows(request: RowModelRequest, staff?: UserProfile) { + try { + let SQL = SQLBuilder.join([ + SQLBuilder.select(this.getRowSelectCols(request)), + SQLBuilder.from(this.tableName), + SQLBuilder.join(this.createJoinSql(request)), + SQLBuilder.where(this.createGetRowsFilters(request, staff)), + SQLBuilder.groupBy(this.getGroupByColumns(request)), + SQLBuilder.orderBy(this.getOrderByColumns(request)), + ]); + SQL = await this.getRowsSqlWrapper(SQL, request, staff); + + this.logger.debug('getrows', SQL); + + const results: any[] = (await db?.$queryRawUnsafe(SQL)) || []; + + const rowDataDto = await Promise.all( + results.map((row) => this.getRowDto(row, staff)), + ); + return { + rowCount: this.getRowCount(request, rowDataDto) || 0, + rowData: rowDataDto, + }; + } catch (error: any) { + this.logger.error('Error executing getRows:', error); } - protected async getRowsSqlWrapper(sql: string, request?: RowModelRequest, staff?: UserProfile) { - if (request) - return SQLBuilder.join([sql, this.getLimitSql(request)]) - return sql + } + getRowCount(request: RowModelRequest, results: any[]) { + if (results === null || results === undefined || results.length === 0) { + return null; } - protected getLimitSql(request: RowModelRequest) { - return SQLBuilder.limit(request.endRow - request.startRow, request.startRow) + const currentLastRow = request.startRow + results.length; + return currentLastRow <= request.endRow ? currentLastRow : -1; + } + + async getRowById(options: GetRowOptions): Promise { + const { + id, + extraCondition = { + field: `${this.tableName}.deleted_at`, + op: 'blank', + type: 'date', + }, + staff, + } = options; + return this.getSingleRow( + { AND: [this.createGetByIdFilter(id!), extraCondition] }, + staff, + ); + } + + async getRowByIds(options: GetRowOptions): Promise { + const { + ids, + extraCondition = { + field: `${this.tableName}.deleted_at`, + op: 'blank', + type: 'date', + }, + staff, + } = options; + return this.getMultipleRows( + { AND: [this.createGetByIdsFilter(ids!), extraCondition] }, + staff, + ); + } + + protected createGetRowsFilters( + request: RowModelRequest, + staff?: UserProfile, + ): LogicalCondition { + let groupConditions: LogicalCondition[] = []; + if (this.isDoingTreeGroup(request)) { + groupConditions = [ + { + field: 'parent_id', + op: 'equals' as OperatorType, + value: request.groupKeys[request.groupKeys.length - 1], + }, + ]; + } else { + groupConditions = request?.groupKeys?.map((key, index) => ({ + field: request.rowGroupCols[index].field, + op: 'equals' as OperatorType, + value: key, + })); } - abstract createJoinSql(request?: RowModelRequest): string[]; - async getRows(request: RowModelRequest, staff?: UserProfile) { - try { - let SQL = SQLBuilder.join([ - SQLBuilder.select(this.getRowSelectCols(request)), - SQLBuilder.from(this.tableName), - SQLBuilder.join(this.createJoinSql(request)), - SQLBuilder.where(this.createGetRowsFilters(request, staff)), - SQLBuilder.groupBy(this.getGroupByColumns(request)), - SQLBuilder.orderBy(this.getOrderByColumns(request)), - ]); - SQL = await this.getRowsSqlWrapper(SQL, request, staff) + const condition: LogicalCondition = { + AND: [ + ...groupConditions, + ...this.buildFilterConditions(request.filterModel), + ], + }; - this.logger.debug('getrows', SQL) + return condition; + } + private buildFilterConditions(filterModel: any): LogicalCondition[] { + return filterModel + ? Object.entries(filterModel)?.map(([key, item]) => + SQLBuilder.createFilterSql( + key === 'ag-Grid-AutoColumn' ? 'name' : key, + item, + ), + ) + : []; + } - const results: any[] = await db?.$queryRawUnsafe(SQL) || []; + getRowSelectCols(request: RowModelRequest): string[] { + return this.isDoingGroup(request) + ? this.createGroupingRowSelect(request) + : this.createUnGroupingRowSelect(request); + } + protected createUnGroupingRowSelect(request?: RowModelRequest): string[] { + return ['*']; + } + protected createAggSqlForWrapper(request: RowModelRequest) { + const { rowGroupCols, valueCols, groupKeys } = request; + return valueCols.map( + (valueCol) => + `${valueCol.aggFunc}(${valueCol.field.replace('.', '_')}) AS ${valueCol.field.split('.').join('_')}`, + ); + } + protected createGroupingRowSelect( + request: RowModelRequest, + wrapperSql: boolean = false, + ): string[] { + const { rowGroupCols, valueCols, groupKeys } = request; + const colsToSelect: string[] = []; - let rowDataDto = await Promise.all(results.map(row => this.getRowDto(row, staff))) - return { rowCount: this.getRowCount(request, rowDataDto) || 0, rowData: rowDataDto }; - } catch (error: any) { - this.logger.error('Error executing getRows:', error); + const rowGroupCol = rowGroupCols[groupKeys!.length]; + if (rowGroupCol) { + colsToSelect.push( + `${rowGroupCol.field} AS ${rowGroupCol.field.replace('.', '_')}`, + ); + } + colsToSelect.push( + ...valueCols.map( + (valueCol) => + `${wrapperSql ? '' : valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`, + ), + ); + return colsToSelect; + } + + getGroupByColumns(request: RowModelRequest): string[] { + return this.isDoingGroup(request) + ? [request.rowGroupCols[request.groupKeys!.length]?.field] + : []; + } + + getOrderByColumns(request: RowModelRequest): string[] { + const { sortModel, rowGroupCols, groupKeys } = request; + const grouping = this.isDoingGroup(request); + const sortParts: string[] = []; + + if (sortModel) { + const groupColIds = rowGroupCols + .map((groupCol) => groupCol.id) + .slice(0, groupKeys.length + 1); + sortModel.forEach((item) => { + if ( + !grouping || + (groupColIds.indexOf(item.colId) >= 0 && + rowGroupCols[groupKeys.length].field === item.colId) + ) { + const colId = this.keywords.has(item.colId.toUpperCase()) + ? `"${item.colId}"` + : item.colId; + sortParts.push(`${colId} ${item.sort}`); } - } - getRowCount(request: RowModelRequest, results: any[]) { - if (results === null || results === undefined || results.length === 0) { - return null; - } - const currentLastRow = request.startRow + results.length; - return currentLastRow <= request.endRow ? currentLastRow : -1; + }); } - async getRowById(options: GetRowOptions): Promise { - const { id, extraCondition = { - field: `${this.tableName}.deleted_at`, - op: "blank", - type: "date" - }, staff } = options; - return this.getSingleRow({ AND: [this.createGetByIdFilter(id!), extraCondition] }, staff); - } + return sortParts; + } + isDoingGroup(requset: RowModelRequest): boolean { + return requset.rowGroupCols.length > requset.groupKeys.length; + } + isDoingTreeGroup(requset: RowModelRequest): boolean { + return requset.rowGroupCols.length === 0 && requset.groupKeys.length > 0; + } + private async getSingleRow( + condition: LogicalCondition, + staff?: UserProfile, + ): Promise { + const results = await this.getRowsWithFilters(condition, staff); + return results[0]; + } + private async getMultipleRows( + condition: LogicalCondition, + staff?: UserProfile, + ): Promise { + return this.getRowsWithFilters(condition, staff); + } - async getRowByIds(options: GetRowOptions): Promise { - const { ids, extraCondition = { - field: `${this.tableName}.deleted_at`, - op: "blank", - type: "date" - }, staff } = options; - return this.getMultipleRows({ AND: [this.createGetByIdsFilter(ids!), extraCondition] }, staff); - } + private async getRowsWithFilters( + condition: LogicalCondition, + staff?: UserProfile, + ): Promise { + try { + const SQL = SQLBuilder.join([ + SQLBuilder.select(this.createUnGroupingRowSelect()), + SQLBuilder.from(this.tableName), + SQLBuilder.join(this.createJoinSql()), + SQLBuilder.where(condition), + ]); - protected createGetRowsFilters(request: RowModelRequest, staff?: UserProfile): LogicalCondition { - let groupConditions: LogicalCondition[] = [] - if (this.isDoingTreeGroup(request)) { - groupConditions = [ - { - field: 'parent_id', - op: "equals" as OperatorType, - value: request.groupKeys[request.groupKeys.length - 1] - } - ] - } else { - groupConditions = request?.groupKeys?.map((key, index) => ({ - field: request.rowGroupCols[index].field, - op: "equals" as OperatorType, - value: key - })) - } + // this.logger.debug(SQL) + const results: any[] = await db.$queryRawUnsafe(SQL); - const condition: LogicalCondition = { - AND: [...groupConditions, ...this.buildFilterConditions(request.filterModel)] - } + const rowDataDto = await Promise.all( + results.map((item) => this.getRowDto(item, staff)), + ); - return condition; - } - private buildFilterConditions(filterModel: any): LogicalCondition[] { - return filterModel - ? Object.entries(filterModel)?.map(([key, item]) => SQLBuilder.createFilterSql(key === 'ag-Grid-AutoColumn' ? 'name' : key, item)) - : []; + // rowDataDto = getUniqueItems(rowDataDto, "id") + return rowDataDto; + } catch (error) { + this.logger.error('Error executing query:', error); + throw error; } + } - getRowSelectCols(request: RowModelRequest): string[] { - return this.isDoingGroup(request) - ? this.createGroupingRowSelect(request) - : this.createUnGroupingRowSelect(request); + async getAggValues(request: RowModelRequest) { + try { + const SQL = SQLBuilder.join([ + SQLBuilder.select(this.buildAggSelect(request.valueCols)), + SQLBuilder.from(this.tableName), + SQLBuilder.join(this.createJoinSql(request)), + SQLBuilder.where(this.createGetRowsFilters(request)), + SQLBuilder.groupBy(this.buildAggGroupBy()), + ]); + const result: any[] = await db.$queryRawUnsafe(SQL); + return result[0]; + } catch (error) { + this.logger.error('Error executing query:', error); + throw error; } - protected createUnGroupingRowSelect(request?: RowModelRequest): string[] { - return ['*']; - } - protected createAggSqlForWrapper(request: RowModelRequest) { - const { rowGroupCols, valueCols, groupKeys } = request; - return valueCols.map(valueCol => - `${valueCol.aggFunc}(${valueCol.field.replace('.', '_')}) AS ${valueCol.field.split('.').join('_')}` - ); - } - protected createGroupingRowSelect(request: RowModelRequest, wrapperSql: boolean = false): string[] { - const { rowGroupCols, valueCols, groupKeys } = request; - const colsToSelect: string[] = []; + } + protected buildAggGroupBy(): string[] { + return []; + } + protected buildAggSelect(valueCols: any[]): string[] { + return valueCols.map( + (valueCol) => + `${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}`, + ); + } - const rowGroupCol = rowGroupCols[groupKeys!.length]; - if (rowGroupCol) { - colsToSelect.push(`${rowGroupCol.field} AS ${rowGroupCol.field.replace('.', '_')}`); - } - colsToSelect.push(...valueCols.map(valueCol => - `${wrapperSql ? "" : valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}` - )); - - return colsToSelect; - } - - getGroupByColumns(request: RowModelRequest): string[] { - return this.isDoingGroup(request) - ? [request.rowGroupCols[request.groupKeys!.length]?.field] - : []; - } - - - getOrderByColumns(request: RowModelRequest): string[] { - const { sortModel, rowGroupCols, groupKeys } = request; - const grouping = this.isDoingGroup(request); - const sortParts: string[] = []; - - if (sortModel) { - const groupColIds = rowGroupCols.map(groupCol => groupCol.id).slice(0, groupKeys.length + 1); - sortModel.forEach(item => { - if (!grouping || (groupColIds.indexOf(item.colId) >= 0 && rowGroupCols[groupKeys.length].field === item.colId)) { - const colId = this.keywords.has(item.colId.toUpperCase()) ? `"${item.colId}"` : item.colId; - sortParts.push(`${colId} ${item.sort}`); - } - }); - } - - return sortParts; - } - isDoingGroup(requset: RowModelRequest): boolean { - return requset.rowGroupCols.length > requset.groupKeys.length; - } - isDoingTreeGroup(requset: RowModelRequest): boolean { - return requset.rowGroupCols.length === 0 && requset.groupKeys.length > 0; - } - private async getSingleRow(condition: LogicalCondition, staff?: UserProfile): Promise { - const results = await this.getRowsWithFilters(condition, staff) - return results[0] - } - private async getMultipleRows(condition: LogicalCondition, staff?: UserProfile): Promise { - return this.getRowsWithFilters(condition, staff); - } - - private async getRowsWithFilters(condition: LogicalCondition, staff?: UserProfile): Promise { - try { - let SQL = SQLBuilder.join([ - SQLBuilder.select(this.createUnGroupingRowSelect()), - SQLBuilder.from(this.tableName), - SQLBuilder.join(this.createJoinSql()), - SQLBuilder.where(condition) - ]); - - // this.logger.debug(SQL) - const results: any[] = await db.$queryRawUnsafe(SQL); - - let rowDataDto = await Promise.all(results.map(item => this.getRowDto(item, staff))); - - // rowDataDto = getUniqueItems(rowDataDto, "id") - return rowDataDto - } catch (error) { - this.logger.error('Error executing query:', error); - throw error; - } - } - - async getAggValues(request: RowModelRequest) { - try { - const SQL = SQLBuilder.join([ - SQLBuilder.select(this.buildAggSelect(request.valueCols)), - SQLBuilder.from(this.tableName), - SQLBuilder.join(this.createJoinSql(request)), - SQLBuilder.where(this.createGetRowsFilters(request)), - SQLBuilder.groupBy(this.buildAggGroupBy()) - ]); - const result: any[] = await db.$queryRawUnsafe(SQL); - return result[0]; - } catch (error) { - this.logger.error('Error executing query:', error); - throw error; - } - } - protected buildAggGroupBy(): string[] { - return []; - } - protected buildAggSelect(valueCols: any[]): string[] { - return valueCols.map(valueCol => - `${valueCol.aggFunc}(${valueCol.field}) AS ${valueCol.field.replace('.', '_')}` - ); - } - - private createGetByIdFilter(id: string): LogicalCondition { - return { - field: `${this.tableName}.id`, - value: id, - op: "equals" - } - } - private createGetByIdsFilter(ids: string[]): LogicalCondition { - return { - field: `${this.tableName}.id`, - value: ids, - op: "in" - }; - } + private createGetByIdFilter(id: string): LogicalCondition { + return { + field: `${this.tableName}.id`, + value: id, + op: 'equals', + }; + } + private createGetByIdsFilter(ids: string[]): LogicalCondition { + return { + field: `${this.tableName}.id`, + value: ids, + op: 'in', + }; + } } - diff --git a/apps/server/src/models/course/utils.ts b/apps/server/src/models/course/utils.ts index 9d2092c..9cd98c7 100644 --- a/apps/server/src/models/course/utils.ts +++ b/apps/server/src/models/course/utils.ts @@ -1,46 +1,49 @@ -import { db, EnrollmentStatus, PostType } from "@nice/common"; +import { db, EnrollmentStatus, PostType } from '@nice/common'; // 更新课程评价统计 export async function updateCourseReviewStats(courseId: string) { - const reviews = await db.post.findMany({ - where: { - courseId, - type: PostType.COURSE_REVIEW, - deletedAt: null - }, - select: { rating: true } - }); - const numberOfReviews = reviews.length; - const averageRating = numberOfReviews > 0 - ? reviews.reduce((sum, review) => sum + review.rating, 0) / numberOfReviews - : 0; + const reviews = await db.post.findMany({ + where: { + courseId, + type: PostType.COURSE_REVIEW, + deletedAt: null, + }, + select: { rating: true }, + }); + const numberOfReviews = reviews.length; + const averageRating = + numberOfReviews > 0 + ? reviews.reduce((sum, review) => sum + review.rating, 0) / + numberOfReviews + : 0; - return db.course.update({ - where: { id: courseId }, - data: { numberOfReviews, averageRating } - }); + return db.course.update({ + where: { id: courseId }, + data: { + // numberOfReviews, + //averageRating, + }, + }); } // 更新课程注册统计 export async function updateCourseEnrollmentStats(courseId: string) { - const completedEnrollments = await db.enrollment.count({ - where: { - courseId, - status: EnrollmentStatus.COMPLETED - } - }); - const totalEnrollments = await db.enrollment.count({ - where: { courseId } - }); - const completionRate = totalEnrollments > 0 - ? (completedEnrollments / totalEnrollments) * 100 - : 0; - return db.course.update({ - where: { id: courseId }, - data: { - numberOfStudents: totalEnrollments, - completionRate - } - }); + const completedEnrollments = await db.enrollment.count({ + where: { + courseId, + status: EnrollmentStatus.COMPLETED, + }, + }); + const totalEnrollments = await db.enrollment.count({ + where: { courseId }, + }); + const completionRate = + totalEnrollments > 0 ? (completedEnrollments / totalEnrollments) * 100 : 0; + return db.course.update({ + where: { id: courseId }, + data: { + // numberOfStudents: totalEnrollments, + // completionRate, + }, + }); } - From 4499e0e38db11b5199b4f7049fea378cfa8fa70a Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 27 Jan 2025 22:43:18 +0800 Subject: [PATCH 06/10] add --- apps/server/src/queue/queue.module.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/server/src/queue/queue.module.ts b/apps/server/src/queue/queue.module.ts index 57a1a9d..ca825eb 100755 --- a/apps/server/src/queue/queue.module.ts +++ b/apps/server/src/queue/queue.module.ts @@ -25,11 +25,10 @@ import { join } from 'path'; { name: 'file-queue', // 新增文件处理队列 processors: [join(__dirname, 'worker/file.processor.js')], // 文件处理器的路径 - } + }, ), ], providers: [Logger], - exports: [] - + exports: [], }) -export class QueueModule { } +export class QueueModule {} From a47401971e8ecd463b01e01984b357f08a67b32d Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 27 Jan 2025 22:43:20 +0800 Subject: [PATCH 07/10] add --- .../src/models/term/term.row.service.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/apps/server/src/models/term/term.row.service.ts b/apps/server/src/models/term/term.row.service.ts index 65ba56e..2b20e62 100644 --- a/apps/server/src/models/term/term.row.service.ts +++ b/apps/server/src/models/term/term.row.service.ts @@ -17,20 +17,22 @@ export class TermRowService extends RowCacheService { createUnGroupingRowSelect( requset: z.infer, ): string[] { - const result = super.createUnGroupingRowSelect(requset).concat([ - `${this.tableName}.name AS name`, - `${this.tableName}.order AS order`, - `${this.tableName}.has_children AS has_children`, - `${this.tableName}.parent_id AS parent_id`, - `${this.tableName}.domain_id AS domain_id`, - `taxonomy.name AS taxonomy_name`, - `taxonomy.id AS taxonomy_id` - ]); + const result = super + .createUnGroupingRowSelect(requset) + .concat([ + `${this.tableName}.name AS name`, + `${this.tableName}.order AS order`, + `${this.tableName}.has_children AS has_children`, + `${this.tableName}.parent_id AS parent_id`, + `${this.tableName}.domain_id AS domain_id`, + `taxonomy.name AS taxonomy_name`, + `taxonomy.id AS taxonomy_id`, + ]); return result; } createJoinSql(request?: RowModelRequest): string[] { return [ - `LEFT JOIN taxonomy ON ${this.tableName}.taxonomy_id = taxonomy.id` + `LEFT JOIN taxonomy ON ${this.tableName}.taxonomy_id = taxonomy.id`, ]; } protected createGetRowsFilters( @@ -53,7 +55,7 @@ export class TermRowService extends RowCacheService { } else if (parentId === null) { condition.AND.push({ field: `${this.tableName}.parent_id`, - op: "blank", + op: 'blank', }); } } @@ -66,7 +68,7 @@ export class TermRowService extends RowCacheService { } else if (domainId === null) { condition.AND.push({ field: `${this.tableName}.domain_id`, - op: "blank", + op: 'blank', }); } if (taxonomyId) { @@ -84,8 +86,6 @@ export class TermRowService extends RowCacheService { }); } - return condition; } - } From 18d9b5e33c5d425a1d7ed54cdf8062c8f53bbc4f Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 27 Jan 2025 22:43:22 +0800 Subject: [PATCH 08/10] add --- apps/server/src/models/lecture/utils.ts | 64 ++++++++++++------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/apps/server/src/models/lecture/utils.ts b/apps/server/src/models/lecture/utils.ts index 008bc87..f5216f8 100644 --- a/apps/server/src/models/lecture/utils.ts +++ b/apps/server/src/models/lecture/utils.ts @@ -1,39 +1,39 @@ -import { db, Lecture } from "@nice/common" +import { db, Lecture } from '@nice/common'; export async function updateSectionLectureStats(sectionId: string) { - const sectionStats = await db.lecture.aggregate({ - where: { - sectionId, - deletedAt: null - }, - _count: { _all: true }, - _sum: { duration: true } - }); + const sectionStats = await db.lecture.aggregate({ + where: { + sectionId, + deletedAt: null, + }, + _count: { _all: true }, + _sum: { duration: true }, + }); - await db.section.update({ - where: { id: sectionId }, - data: { - totalLectures: sectionStats._count._all, - totalDuration: sectionStats._sum.duration || 0 - } - }); + await db.section.update({ + where: { id: sectionId }, + data: { + // totalLectures: sectionStats._count._all, + // totalDuration: sectionStats._sum.duration || 0, + }, + }); } export async function updateCourseLectureStats(courseId: string) { - const courseStats = await db.lecture.aggregate({ - where: { - courseId, - deletedAt: null - }, - _count: { _all: true }, - _sum: { duration: true } - }); + const courseStats = await db.lecture.aggregate({ + where: { + courseId, + deletedAt: null, + }, + _count: { _all: true }, + _sum: { duration: true }, + }); - await db.course.update({ - where: { id: courseId }, - data: { - totalLectures: courseStats._count._all, - totalDuration: courseStats._sum.duration || 0 - } - }); -} \ No newline at end of file + await db.course.update({ + where: { id: courseId }, + data: { + //totalLectures: courseStats._count._all, + //totalDuration: courseStats._sum.duration || 0, + }, + }); +} From 52555bc6451d1b6c798aa260f3af57510e34a1d3 Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 27 Jan 2025 22:43:25 +0800 Subject: [PATCH 09/10] add --- apps/server/src/queue/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/server/src/queue/types.ts b/apps/server/src/queue/types.ts index db4f92c..c509c71 100755 --- a/apps/server/src/queue/types.ts +++ b/apps/server/src/queue/types.ts @@ -1,4 +1,4 @@ export enum QueueJobType { - UPDATE_STATS = "update_stats", - FILE_PROCESS = "file_process" + UPDATE_STATS = 'update_stats', + FILE_PROCESS = 'file_process', } From 6777149a0c54f64895d21d2448289e0c13dd04ce Mon Sep 17 00:00:00 2001 From: ditiqi Date: Mon, 27 Jan 2025 22:43:31 +0800 Subject: [PATCH 10/10] add --- .../queue/postprocess/postprocess.service.ts | 44 +- apps/server/src/queue/stats/stats.service.ts | 128 +++--- .../server/src/queue/worker/file.processor.ts | 24 +- apps/server/src/queue/worker/processor.ts | 77 ++-- apps/server/src/tasks/init/init.service.ts | 2 +- apps/server/src/upload/tus.service.ts | 204 +++++---- apps/server/src/upload/types.ts | 29 +- apps/server/src/upload/upload.controller.ts | 87 ++-- apps/server/src/upload/upload.module.ts | 18 +- apps/server/src/upload/utils.ts | 2 +- apps/server/src/utils/file.ts | 69 ++- apps/server/src/utils/minio/minio.service.ts | 38 +- apps/web/package.json | 183 ++++---- apps/web/src/app/main/course/detail/page.tsx | 1 - .../src/app/main/courses/instructor/page.tsx | 2 +- apps/web/src/app/main/home/page.tsx | 97 +--- .../common/uploader/FileUploader.tsx | 414 ++++++++++-------- .../components/presentation/TusUploader.tsx | 40 ++ apps/web/src/env.ts | 8 + apps/web/src/hooks/useTusUpload.ts | 125 ++++++ packages/client/src/api/hooks/useCourse.ts | 126 +++--- .../client/src/api/hooks/useDepartment.ts | 29 -- packages/client/tsconfig.json | 46 +- packages/common/package.json | 67 +-- packages/common/src/select.ts | 18 +- packages/common/tsup.config.ts | 22 +- packages/utils/package.json | 1 + packages/utils/src/index.ts | 44 +- packages/utils/src/types.ts | 1 + 29 files changed, 1058 insertions(+), 888 deletions(-) create mode 100644 apps/web/src/components/presentation/TusUploader.tsx create mode 100644 apps/web/src/hooks/useTusUpload.ts create mode 100644 packages/utils/src/types.ts diff --git a/apps/server/src/queue/postprocess/postprocess.service.ts b/apps/server/src/queue/postprocess/postprocess.service.ts index ddbc821..eef571b 100644 --- a/apps/server/src/queue/postprocess/postprocess.service.ts +++ b/apps/server/src/queue/postprocess/postprocess.service.ts @@ -1,28 +1,24 @@ -import { InjectQueue } from "@nestjs/bullmq"; -import { Injectable } from "@nestjs/common"; -import EventBus from "@server/utils/event-bus"; -import { Queue } from "bullmq"; -import { ObjectType } from "@nice/common"; -import { QueueJobType } from "../types"; +import { InjectQueue } from '@nestjs/bullmq'; +import { Injectable } from '@nestjs/common'; +import EventBus from '@server/utils/event-bus'; +import { Queue } from 'bullmq'; +import { ObjectType } from '@nice/common'; +import { QueueJobType } from '../types'; @Injectable() export class PostProcessService { - constructor( - @InjectQueue('general') private generalQueue: Queue - ) { - - } + constructor(@InjectQueue('general') private generalQueue: Queue) {} - private generateJobId(type: ObjectType, data: any): string { - // 根据类型和相关ID生成唯一的job标识 - switch (type) { - case ObjectType.ENROLLMENT: - return `stats_${type}_${data.courseId}`; - case ObjectType.LECTURE: - return `stats_${type}_${data.courseId}_${data.sectionId}`; - case ObjectType.POST: - return `stats_${type}_${data.courseId}`; - default: - return `stats_${type}_${Date.now()}`; - } + private generateJobId(type: ObjectType, data: any): string { + // 根据类型和相关ID生成唯一的job标识 + switch (type) { + case ObjectType.ENROLLMENT: + return `stats_${type}_${data.courseId}`; + case ObjectType.LECTURE: + return `stats_${type}_${data.courseId}_${data.sectionId}`; + case ObjectType.POST: + return `stats_${type}_${data.courseId}`; + default: + return `stats_${type}_${Date.now()}`; } -} \ No newline at end of file + } +} diff --git a/apps/server/src/queue/stats/stats.service.ts b/apps/server/src/queue/stats/stats.service.ts index a498704..e8ce2dc 100644 --- a/apps/server/src/queue/stats/stats.service.ts +++ b/apps/server/src/queue/stats/stats.service.ts @@ -1,70 +1,68 @@ -import { InjectQueue } from "@nestjs/bullmq"; -import { Injectable } from "@nestjs/common"; -import EventBus from "@server/utils/event-bus"; -import { Queue } from "bullmq"; -import { ObjectType } from "@nice/common"; -import { QueueJobType } from "../types"; +import { InjectQueue } from '@nestjs/bullmq'; +import { Injectable } from '@nestjs/common'; +import EventBus from '@server/utils/event-bus'; +import { Queue } from 'bullmq'; +import { ObjectType } from '@nice/common'; +import { QueueJobType } from '../types'; @Injectable() export class StatsService { - constructor( - @InjectQueue('general') private generalQueue: Queue - ) { - EventBus.on("dataChanged", async ({ type, data }) => { - const jobOptions = { - removeOnComplete: true, - jobId: this.generateJobId(type as ObjectType, data) // 使用唯一ID防止重复任务 - }; - switch (type) { - case ObjectType.ENROLLMENT: - await this.generalQueue.add( - QueueJobType.UPDATE_STATS, - { - courseId: data.courseId, - type: ObjectType.ENROLLMENT - }, - jobOptions - ); - break; + constructor(@InjectQueue('general') private generalQueue: Queue) { + EventBus.on('dataChanged', async ({ type, data }) => { + const jobOptions = { + removeOnComplete: true, + jobId: this.generateJobId(type as ObjectType, data), // 使用唯一ID防止重复任务 + }; + switch (type) { + case ObjectType.ENROLLMENT: + await this.generalQueue.add( + QueueJobType.UPDATE_STATS, + { + courseId: data.courseId, + type: ObjectType.ENROLLMENT, + }, + jobOptions, + ); + break; - case ObjectType.LECTURE: - await this.generalQueue.add( - QueueJobType.UPDATE_STATS, - { - sectionId: data.sectionId, - courseId: data.courseId, - type: ObjectType.LECTURE - }, - jobOptions - ); - break; + case ObjectType.LECTURE: + await this.generalQueue.add( + QueueJobType.UPDATE_STATS, + { + sectionId: data.sectionId, + courseId: data.courseId, + type: ObjectType.LECTURE, + }, + jobOptions, + ); + break; - case ObjectType.POST: - if (data.courseId) { - await this.generalQueue.add( - QueueJobType.UPDATE_STATS, - { - courseId: data.courseId, - type: ObjectType.POST - }, - jobOptions - ); - } - break; - } - }); + case ObjectType.POST: + if (data.courseId) { + await this.generalQueue.add( + QueueJobType.UPDATE_STATS, + { + courseId: data.courseId, + type: ObjectType.POST, + }, + jobOptions, + ); + } + break; + } + }); + } + + private generateJobId(type: ObjectType, data: any): string { + // 根据类型和相关ID生成唯一的job标识 + switch (type) { + case ObjectType.ENROLLMENT: + return `stats_${type}_${data.courseId}`; + case ObjectType.LECTURE: + return `stats_${type}_${data.courseId}_${data.sectionId}`; + case ObjectType.POST: + return `stats_${type}_${data.courseId}`; + default: + return `stats_${type}_${Date.now()}`; } - - private generateJobId(type: ObjectType, data: any): string { - // 根据类型和相关ID生成唯一的job标识 - switch (type) { - case ObjectType.ENROLLMENT: - return `stats_${type}_${data.courseId}`; - case ObjectType.LECTURE: - return `stats_${type}_${data.courseId}_${data.sectionId}`; - case ObjectType.POST: - return `stats_${type}_${data.courseId}`; - default: - return `stats_${type}_${Date.now()}`; - } - } -} \ No newline at end of file + } +} diff --git a/apps/server/src/queue/worker/file.processor.ts b/apps/server/src/queue/worker/file.processor.ts index de3d836..b416af4 100644 --- a/apps/server/src/queue/worker/file.processor.ts +++ b/apps/server/src/queue/worker/file.processor.ts @@ -6,17 +6,17 @@ import { ImageProcessor } from '@server/models/resource/processor/ImageProcessor import { VideoProcessor } from '@server/models/resource/processor/VideoProcessor'; const logger = new Logger('FileProcessorWorker'); const pipeline = new ResourceProcessingPipeline() - .addProcessor(new ImageProcessor()) - .addProcessor(new VideoProcessor()) + .addProcessor(new ImageProcessor()) + .addProcessor(new VideoProcessor()); export default async function processJob(job: Job) { - if (job.name === QueueJobType.FILE_PROCESS) { - console.log(job) - const { resource } = job.data; - if (!resource) { - throw new Error('No resource provided in job data'); - } - const result = await pipeline.execute(resource); - - return result; + if (job.name === QueueJobType.FILE_PROCESS) { + console.log('job', job); + const { resource } = job.data; + if (!resource) { + throw new Error('No resource provided in job data'); } -} \ No newline at end of file + const result = await pipeline.execute(resource); + + return result; + } +} diff --git a/apps/server/src/queue/worker/processor.ts b/apps/server/src/queue/worker/processor.ts index 6574912..e5b2052 100755 --- a/apps/server/src/queue/worker/processor.ts +++ b/apps/server/src/queue/worker/processor.ts @@ -1,49 +1,52 @@ import { Job } from 'bullmq'; import { Logger } from '@nestjs/common'; import { - updateCourseLectureStats, - updateSectionLectureStats + updateCourseLectureStats, + updateSectionLectureStats, } from '@server/models/lecture/utils'; import { ObjectType } from '@nice/common'; import { - updateCourseEnrollmentStats, - updateCourseReviewStats + updateCourseEnrollmentStats, + updateCourseReviewStats, } from '@server/models/course/utils'; import { QueueJobType } from '../types'; const logger = new Logger('QueueWorker'); export default async function processJob(job: Job) { - try { - if (job.name === QueueJobType.UPDATE_STATS) { - const { sectionId, courseId, type } = job.data; - // 处理 section 统计 - if (sectionId) { - await updateSectionLectureStats(sectionId); - logger.debug(`Updated section stats for sectionId: ${sectionId}`); - } - // 如果没有 courseId,提前返回 - if (!courseId) { - return; - } - // 处理 course 相关统计 - switch (type) { - case ObjectType.LECTURE: - await updateCourseLectureStats(courseId); - break; - case ObjectType.ENROLLMENT: - await updateCourseEnrollmentStats(courseId); - break; - case ObjectType.POST: - await updateCourseReviewStats(courseId); - break; - default: - logger.warn(`Unknown update stats type: ${type}`); - } + try { + if (job.name === QueueJobType.UPDATE_STATS) { + const { sectionId, courseId, type } = job.data; + // 处理 section 统计 + if (sectionId) { + await updateSectionLectureStats(sectionId); + logger.debug(`Updated section stats for sectionId: ${sectionId}`); + } + // 如果没有 courseId,提前返回 + if (!courseId) { + return; + } + // 处理 course 相关统计 + switch (type) { + case ObjectType.LECTURE: + await updateCourseLectureStats(courseId); + break; + case ObjectType.ENROLLMENT: + await updateCourseEnrollmentStats(courseId); + break; + case ObjectType.POST: + await updateCourseReviewStats(courseId); + break; + default: + logger.warn(`Unknown update stats type: ${type}`); + } - logger.debug(`Updated course stats for courseId: ${courseId}, type: ${type}`); - } - - - } catch (error: any) { - logger.error(`Error processing stats update job: ${error.message}`, error.stack); + logger.debug( + `Updated course stats for courseId: ${courseId}, type: ${type}`, + ); } -} \ No newline at end of file + } catch (error: any) { + logger.error( + `Error processing stats update job: ${error.message}`, + error.stack, + ); + } +} diff --git a/apps/server/src/tasks/init/init.service.ts b/apps/server/src/tasks/init/init.service.ts index 4f68e8e..8d6a955 100755 --- a/apps/server/src/tasks/init/init.service.ts +++ b/apps/server/src/tasks/init/init.service.ts @@ -19,7 +19,7 @@ export class InitService { private readonly minioService: MinioService, private readonly authService: AuthService, private readonly genDevService: GenDevService, - ) { } + ) {} private async createRoles() { this.logger.log('Checking existing system roles'); for (const role of InitRoles) { diff --git a/apps/server/src/upload/tus.service.ts b/apps/server/src/upload/tus.service.ts index 0eea246..0173d25 100644 --- a/apps/server/src/upload/tus.service.ts +++ b/apps/server/src/upload/tus.service.ts @@ -1,7 +1,7 @@ import { Injectable, OnModuleInit, Logger } from '@nestjs/common'; -import { Server, Uid, Upload } from "@nice/tus" +import { Server, Uid, Upload } from '@nice/tus'; import { FileStore } from '@nice/tus'; -import { Request, Response } from "express" +import { Request, Response } from 'express'; import { db, ResourceStatus } from '@nice/common'; import { getFilenameWithoutExt } from '@server/utils/file'; import { ResourceService } from '@server/models/resource/resource.service'; @@ -12,104 +12,122 @@ import { QueueJobType } from '@server/queue/types'; import { nanoid } from 'nanoid-cjs'; import { slugify } from 'transliteration'; const FILE_UPLOAD_CONFIG = { - directory: process.env.UPLOAD_DIR, - maxSizeBytes: 20_000_000_000, // 20GB - expirationPeriod: 24 * 60 * 60 * 1000 // 24 hours + directory: process.env.UPLOAD_DIR, + maxSizeBytes: 20_000_000_000, // 20GB + expirationPeriod: 24 * 60 * 60 * 1000, // 24 hours }; @Injectable() export class TusService implements OnModuleInit { - private readonly logger = new Logger(TusService.name); - private tusServer: Server; - constructor(private readonly resourceService: ResourceService, - @InjectQueue("file-queue") private fileQueue: Queue - ) { } - onModuleInit() { - this.initializeTusServer(); - this.setupTusEventHandlers(); - } - private initializeTusServer() { - this.tusServer = new Server({ - namingFunction(req, metadata) { - const safeFilename = slugify(metadata.filename); - const now = new Date(); - const year = now.getFullYear(); - const month = String(now.getMonth() + 1).padStart(2, '0'); - const day = String(now.getDate()).padStart(2, '0'); - const uniqueId = nanoid(10); - return `${year}/${month}/${day}/${uniqueId}/${safeFilename}`; - }, - path: '/upload', - datastore: new FileStore({ - directory: FILE_UPLOAD_CONFIG.directory, - expirationPeriodInMilliseconds: FILE_UPLOAD_CONFIG.expirationPeriod - }), - maxSize: FILE_UPLOAD_CONFIG.maxSizeBytes, - postReceiveInterval: 1000, - getFileIdFromRequest: (req, lastPath) => { - const match = req.url.match(/\/upload\/(.+)/); - return match ? match[1] : lastPath; - } - }); - } + private readonly logger = new Logger(TusService.name); + private tusServer: Server; + constructor( + private readonly resourceService: ResourceService, + @InjectQueue('file-queue') private fileQueue: Queue, + ) {} + onModuleInit() { + this.initializeTusServer(); + this.setupTusEventHandlers(); + } + private initializeTusServer() { + this.tusServer = new Server({ + namingFunction(req, metadata) { + const safeFilename = slugify(metadata.filename); + const now = new Date(); + const year = now.getFullYear(); + const month = String(now.getMonth() + 1).padStart(2, '0'); + const day = String(now.getDate()).padStart(2, '0'); + const uniqueId = nanoid(10); + return `${year}/${month}/${day}/${uniqueId}/${safeFilename}`; + }, + path: '/upload', + datastore: new FileStore({ + directory: FILE_UPLOAD_CONFIG.directory, + expirationPeriodInMilliseconds: FILE_UPLOAD_CONFIG.expirationPeriod, + }), + maxSize: FILE_UPLOAD_CONFIG.maxSizeBytes, + postReceiveInterval: 1000, + getFileIdFromRequest: (req, lastPath) => { + const match = req.url.match(/\/upload\/(.+)/); + return match ? match[1] : lastPath; + }, + }); + } - private setupTusEventHandlers() { - this.tusServer.on("POST_CREATE", this.handleUploadCreate.bind(this)); - this.tusServer.on("POST_FINISH", this.handleUploadFinish.bind(this)); + private setupTusEventHandlers() { + this.tusServer.on('POST_CREATE', this.handleUploadCreate.bind(this)); + this.tusServer.on('POST_FINISH', this.handleUploadFinish.bind(this)); + } + private getFileId(uploadId: string) { + return uploadId.replace(/\/[^/]+$/, ''); + } + private async handleUploadCreate( + req: Request, + res: Response, + upload: Upload, + url: string, + ) { + try { + const fileId = this.getFileId(upload.id); + // const filename = upload.metadata.filename; + await this.resourceService.create({ + data: { + title: getFilenameWithoutExt(upload.metadata.filename), + fileId, // 移除最后的文件名 + url: upload.id, + metadata: upload.metadata, + status: ResourceStatus.UPLOADING, + }, + }); + } catch (error) { + this.logger.error('Failed to create resource during upload', error); } - private getFileId(uploadId: string) { - return uploadId.replace(/\/[^/]+$/, '') - } - private async handleUploadCreate(req: Request, res: Response, upload: Upload, url: string) { - try { + } - const fileId = this.getFileId(upload.id) - const filename = upload.metadata.filename - await this.resourceService.create({ - data: { - title: getFilenameWithoutExt(upload.metadata.filename), - fileId, // 移除最后的文件名 - url: upload.id, - metadata: upload.metadata, - status: ResourceStatus.UPLOADING - } - }); - } catch (error) { - this.logger.error('Failed to create resource during upload', error); - } + private async handleUploadFinish( + req: Request, + res: Response, + upload: Upload, + ) { + try { + console.log('upload.id', upload.id); + console.log('fileId', this.getFileId(upload.id)); + const resource = await this.resourceService.update({ + where: { fileId: this.getFileId(upload.id) }, + data: { status: ResourceStatus.UPLOADED }, + }); + this.fileQueue.add( + QueueJobType.FILE_PROCESS, + { resource }, + { jobId: resource.id }, + ); + this.logger.log(`Upload finished ${resource.url}`); + } catch (error) { + this.logger.error('Failed to update resource after upload', error); } + } - private async handleUploadFinish(req: Request, res: Response, upload: Upload) { - try { - const resource = await this.resourceService.update({ - where: { fileId: this.getFileId(upload.id) }, - data: { status: ResourceStatus.UPLOADED } - }); - this.fileQueue.add(QueueJobType.FILE_PROCESS, { resource }, { jobId: resource.id }) - this.logger.log(`Upload finished ${resource.url}`); - } catch (error) { - this.logger.error('Failed to update resource after upload', error); - } + @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) + async cleanupExpiredUploads() { + try { + // Delete incomplete uploads older than 24 hours + const deletedResources = await db.resource.deleteMany({ + where: { + createdAt: { + lt: new Date(Date.now() - FILE_UPLOAD_CONFIG.expirationPeriod), + }, + status: ResourceStatus.UPLOADING, + }, + }); + const expiredUploadCount = await this.tusServer.cleanUpExpiredUploads(); + this.logger.log( + `Cleanup complete: ${deletedResources.count} resources and ${expiredUploadCount} uploads removed`, + ); + } catch (error) { + this.logger.error('Expired uploads cleanup failed', error); } + } - @Cron(CronExpression.EVERY_DAY_AT_MIDNIGHT) - async cleanupExpiredUploads() { - try { - // Delete incomplete uploads older than 24 hours - const deletedResources = await db.resource.deleteMany({ - where: { - createdAt: { lt: new Date(Date.now() - FILE_UPLOAD_CONFIG.expirationPeriod) }, - status: ResourceStatus.UPLOADING - } - }); - const expiredUploadCount = await this.tusServer.cleanUpExpiredUploads(); - this.logger.log(`Cleanup complete: ${deletedResources.count} resources and ${expiredUploadCount} uploads removed`); - } catch (error) { - this.logger.error('Expired uploads cleanup failed', error); - } - } - - async handleTus(req: Request, res: Response) { - - return this.tusServer.handle(req, res); - } -} \ No newline at end of file + async handleTus(req: Request, res: Response) { + return this.tusServer.handle(req, res); + } +} diff --git a/apps/server/src/upload/types.ts b/apps/server/src/upload/types.ts index ef60aaa..2140ebc 100644 --- a/apps/server/src/upload/types.ts +++ b/apps/server/src/upload/types.ts @@ -1,19 +1,24 @@ export interface UploadCompleteEvent { - identifier: string; - filename: string; - size: number; - hash: string; - integrityVerified: boolean; + identifier: string; + filename: string; + size: number; + hash: string; + integrityVerified: boolean; } export type UploadEvent = { - uploadStart: { identifier: string; filename: string; totalSize: number, resuming?: boolean }; - uploadComplete: UploadCompleteEvent - uploadError: { identifier: string; error: string, filename: string }; -} + uploadStart: { + identifier: string; + filename: string; + totalSize: number; + resuming?: boolean; + }; + uploadComplete: UploadCompleteEvent; + uploadError: { identifier: string; error: string; filename: string }; +}; export interface UploadLock { - clientId: string; - timestamp: number; + clientId: string; + timestamp: number; } // 添加重试机制,处理临时网络问题 // 实现定期清理过期的临时文件 @@ -21,4 +26,4 @@ export interface UploadLock { // 实现上传进度持久化,支持服务重启后恢复 // 添加并发限制,防止系统资源耗尽 // 实现文件去重功能,避免重复上传 -// 添加日志记录和监控机制 \ No newline at end of file +// 添加日志记录和监控机制 diff --git a/apps/server/src/upload/upload.controller.ts b/apps/server/src/upload/upload.controller.ts index ff3e38b..f014c42 100644 --- a/apps/server/src/upload/upload.controller.ts +++ b/apps/server/src/upload/upload.controller.ts @@ -1,55 +1,54 @@ import { - Controller, - All, - Req, - Res, - Get, - Post, - Patch, - Param, - Delete, - Head, - Options, + Controller, + All, + Req, + Res, + Get, + Post, + Patch, + Param, + Delete, + Head, + Options, } from '@nestjs/common'; -import { Request, Response } from "express" +import { Request, Response } from 'express'; import { TusService } from './tus.service'; @Controller('upload') export class UploadController { - constructor(private readonly tusService: TusService) { } - // @Post() - // async handlePost(@Req() req: Request, @Res() res: Response) { - // return this.tusService.handleTus(req, res); - // } + constructor(private readonly tusService: TusService) {} + // @Post() + // async handlePost(@Req() req: Request, @Res() res: Response) { + // return this.tusService.handleTus(req, res); + // } + @Options() + async handleOptions(@Req() req: Request, @Res() res: Response) { + return this.tusService.handleTus(req, res); + } - @Options() - async handleOptions(@Req() req: Request, @Res() res: Response) { - return this.tusService.handleTus(req, res); - } + @Head() + async handleHead(@Req() req: Request, @Res() res: Response) { + return this.tusService.handleTus(req, res); + } - @Head() - async handleHead(@Req() req: Request, @Res() res: Response) { - return this.tusService.handleTus(req, res); - } + @Post() + async handlePost(@Req() req: Request, @Res() res: Response) { + return this.tusService.handleTus(req, res); + } + @Get('/*') + async handleGet(@Req() req: Request, @Res() res: Response) { + return this.tusService.handleTus(req, res); + } - @Post() - async handlePost(@Req() req: Request, @Res() res: Response) { - return this.tusService.handleTus(req, res); - } - @Get("/*") - async handleGet(@Req() req: Request, @Res() res: Response) { - return this.tusService.handleTus(req, res); - } + @Patch('/*') + async handlePatch(@Req() req: Request, @Res() res: Response) { + return this.tusService.handleTus(req, res); + } - @Patch("/*") - async handlePatch(@Req() req: Request, @Res() res: Response) { - return this.tusService.handleTus(req, res); - } - - // Keeping the catch-all method as a fallback - @All() - async handleUpload(@Req() req: Request, @Res() res: Response) { - return this.tusService.handleTus(req, res); - } -} \ No newline at end of file + // Keeping the catch-all method as a fallback + @All() + async handleUpload(@Req() req: Request, @Res() res: Response) { + return this.tusService.handleTus(req, res); + } +} diff --git a/apps/server/src/upload/upload.module.ts b/apps/server/src/upload/upload.module.ts index 0a54ccb..6c8e1b0 100644 --- a/apps/server/src/upload/upload.module.ts +++ b/apps/server/src/upload/upload.module.ts @@ -5,13 +5,13 @@ import { TusService } from './tus.service'; import { ResourceModule } from '@server/models/resource/resource.module'; @Module({ - imports: [ - BullModule.registerQueue({ - name: 'file-queue', // 确保这个名称与 service 中注入的队列名称一致 - }), - ResourceModule - ], - controllers: [UploadController], - providers: [TusService], + imports: [ + BullModule.registerQueue({ + name: 'file-queue', // 确保这个名称与 service 中注入的队列名称一致 + }), + ResourceModule, + ], + controllers: [UploadController], + providers: [TusService], }) -export class UploadModule { } \ No newline at end of file +export class UploadModule {} diff --git a/apps/server/src/upload/utils.ts b/apps/server/src/upload/utils.ts index a171d7f..a7c189f 100644 --- a/apps/server/src/upload/utils.ts +++ b/apps/server/src/upload/utils.ts @@ -1,4 +1,4 @@ export function extractFileIdFromNginxUrl(url: string) { const match = url.match(/uploads\/(\d{4}\/\d{2}\/\d{2}\/[^/]+)/); return match ? match[1] : ''; -} \ No newline at end of file +} diff --git a/apps/server/src/utils/file.ts b/apps/server/src/utils/file.ts index 776d2ec..c24d5d9 100644 --- a/apps/server/src/utils/file.ts +++ b/apps/server/src/utils/file.ts @@ -1,11 +1,10 @@ - import { createHash } from 'crypto'; import { createReadStream } from 'fs'; import path from 'path'; import * as dotenv from 'dotenv'; dotenv.config(); export function getFilenameWithoutExt(filename: string) { - return filename ? filename.replace(/\.[^/.]+$/, '') : filename; + return filename ? filename.replace(/\.[^/.]+$/, '') : filename; } /** * 计算文件的 SHA-256 哈希值 @@ -13,31 +12,31 @@ export function getFilenameWithoutExt(filename: string) { * @returns Promise 返回文件的哈希值(十六进制字符串) */ export async function calculateFileHash(filePath: string): Promise { - return new Promise((resolve, reject) => { - // 创建一个 SHA-256 哈希对象 - const hash = createHash('sha256'); - // 创建文件读取流 - const readStream = createReadStream(filePath); - // 处理读取错误 - readStream.on('error', (error) => { - reject(new Error(`Failed to read file: ${error.message}`)); - }); - // 处理哈希计算错误 - hash.on('error', (error) => { - reject(new Error(`Failed to calculate hash: ${error.message}`)); - }); - // 流式处理文件内容 - readStream - .pipe(hash) - .on('finish', () => { - // 获取最终的哈希值(十六进制格式) - const fileHash = hash.digest('hex'); - resolve(fileHash); - }) - .on('error', (error) => { - reject(new Error(`Hash calculation failed: ${error.message}`)); - }); + return new Promise((resolve, reject) => { + // 创建一个 SHA-256 哈希对象 + const hash = createHash('sha256'); + // 创建文件读取流 + const readStream = createReadStream(filePath); + // 处理读取错误 + readStream.on('error', (error) => { + reject(new Error(`Failed to read file: ${error.message}`)); }); + // 处理哈希计算错误 + hash.on('error', (error) => { + reject(new Error(`Failed to calculate hash: ${error.message}`)); + }); + // 流式处理文件内容 + readStream + .pipe(hash) + .on('finish', () => { + // 获取最终的哈希值(十六进制格式) + const fileHash = hash.digest('hex'); + resolve(fileHash); + }) + .on('error', (error) => { + reject(new Error(`Hash calculation failed: ${error.message}`)); + }); + }); } /** @@ -46,9 +45,9 @@ export async function calculateFileHash(filePath: string): Promise { * @returns string 返回 Buffer 的哈希值(十六进制字符串) */ export function calculateBufferHash(buffer: Buffer): string { - const hash = createHash('sha256'); - hash.update(buffer); - return hash.digest('hex'); + const hash = createHash('sha256'); + hash.update(buffer); + return hash.digest('hex'); } /** @@ -57,11 +56,11 @@ export function calculateBufferHash(buffer: Buffer): string { * @returns string 返回字符串的哈希值(十六进制字符串) */ export function calculateStringHash(content: string): string { - const hash = createHash('sha256'); - hash.update(content); - return hash.digest('hex'); + const hash = createHash('sha256'); + hash.update(content); + return hash.digest('hex'); } export const getUploadFilePath = (fileId: string): string => { - const uploadDirectory = process.env.UPLOAD_DIR; - return path.join(uploadDirectory, fileId); -}; \ No newline at end of file + const uploadDirectory = process.env.UPLOAD_DIR; + return path.join(uploadDirectory, fileId); +}; diff --git a/apps/server/src/utils/minio/minio.service.ts b/apps/server/src/utils/minio/minio.service.ts index 8f402ee..e949bf6 100644 --- a/apps/server/src/utils/minio/minio.service.ts +++ b/apps/server/src/utils/minio/minio.service.ts @@ -3,24 +3,24 @@ import * as Minio from 'minio'; @Injectable() export class MinioService { - private readonly logger = new Logger(MinioService.name) - private readonly minioClient: Minio.Client; - constructor() { - this.minioClient = new Minio.Client({ - endPoint: process.env.MINIO_HOST || 'localhost', - port: parseInt(process.env.MINIO_PORT || '9000'), - useSSL: false, - accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin', - secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin' - }); - } - async createBucket(bucketName: string): Promise { - const exists = await this.minioClient.bucketExists(bucketName); - if (!exists) { - await this.minioClient.makeBucket(bucketName, ''); - this.logger.log(`Bucket ${bucketName} created successfully.`); - } else { - this.logger.log(`Bucket ${bucketName} already exists.`); - } + private readonly logger = new Logger(MinioService.name); + private readonly minioClient: Minio.Client; + constructor() { + this.minioClient = new Minio.Client({ + endPoint: process.env.MINIO_HOST || 'localhost', + port: parseInt(process.env.MINIO_PORT || '9000'), + useSSL: false, + accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin', + secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin', + }); + } + async createBucket(bucketName: string): Promise { + const exists = await this.minioClient.bucketExists(bucketName); + if (!exists) { + await this.minioClient.makeBucket(bucketName, ''); + this.logger.log(`Bucket ${bucketName} created successfully.`); + } else { + this.logger.log(`Bucket ${bucketName} already exists.`); } + } } diff --git a/apps/web/package.json b/apps/web/package.json index a7fa5de..3370827 100755 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,92 +1,93 @@ { - "name": "web", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite", - "build": "tsc -b && vite build", - "lint": "eslint .", - "preview": "vite preview" - }, - "dependencies": { - "@ag-grid-community/client-side-row-model": "~32.3.2", - "@ag-grid-community/core": "~32.3.2", - "@ag-grid-community/react": "~32.3.2", - "@ag-grid-enterprise/clipboard": "~32.3.2", - "@ag-grid-enterprise/column-tool-panel": "~32.3.2", - "@ag-grid-enterprise/core": "~32.3.2", - "@ag-grid-enterprise/filter-tool-panel": "~32.3.2", - "@ag-grid-enterprise/master-detail": "~32.3.2", - "@ag-grid-enterprise/menu": "~32.3.2", - "@ag-grid-enterprise/range-selection": "~32.3.2", - "@ag-grid-enterprise/server-side-row-model": "~32.3.2", - "@ag-grid-enterprise/set-filter": "~32.3.2", - "@ag-grid-enterprise/status-bar": "~32.3.2", - "@ant-design/icons": "^5.4.0", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@floating-ui/react": "^0.26.25", - "@heroicons/react": "^2.2.0", - "@hookform/resolvers": "^3.9.1", - "@nice/client": "workspace:^", - "@nice/common": "workspace:^", - "@nice/iconer": "workspace:^", - "@nice/mindmap": "workspace:^", - "@nice/ui": "workspace:^", - "@tanstack/query-async-storage-persister": "^5.51.9", - "@tanstack/react-query": "^5.51.21", - "@tanstack/react-query-persist-client": "^5.51.9", - "@trpc/client": "11.0.0-rc.456", - "@trpc/react-query": "11.0.0-rc.456", - "@trpc/server": "11.0.0-rc.456", - "@xyflow/react": "^12.3.6", - "ag-grid-community": "~32.3.2", - "ag-grid-enterprise": "~32.3.2", - "ag-grid-react": "~32.3.2", - "antd": "^5.19.3", - "axios": "^1.7.2", - "browser-image-compression": "^2.0.2", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "d3-dag": "^1.1.0", - "d3-hierarchy": "^3.1.2", - "dayjs": "^1.11.12", - "elkjs": "^0.9.3", - "framer-motion": "^11.15.0", - "hls.js": "^1.5.18", - "idb-keyval": "^6.2.1", - "mitt": "^3.0.1", - "quill": "2.0.3", - "react": "18.2.0", - "react-beautiful-dnd": "^13.1.1", - "react-dom": "18.2.0", - "react-dropzone": "^14.3.5", - "react-hook-form": "^7.54.2", - "react-hot-toast": "^2.4.1", - "react-resizable": "^3.0.5", - "react-router-dom": "^6.24.1", - "superjson": "^2.2.1", - "tailwind-merge": "^2.6.0", - "uuid": "^10.0.0", - "yjs": "^13.6.20", - "zod": "^3.23.8" - }, - "devDependencies": { - "@eslint/js": "^9.9.0", - "@types/react": "18.2.38", - "@types/react-dom": "18.2.15", - "@vitejs/plugin-react-swc": "^3.5.0", - "autoprefixer": "^10.4.20", - "eslint": "^9.9.0", - "eslint-plugin-react-hooks": "^5.1.0-rc.0", - "eslint-plugin-react-refresh": "^0.4.9", - "globals": "^15.9.0", - "postcss": "^8.4.41", - "tailwindcss": "^3.4.10", - "typescript": "^5.5.4", - "typescript-eslint": "^8.0.1", - "vite": "^5.4.1" - } -} \ No newline at end of file + "name": "web", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@ag-grid-community/client-side-row-model": "~32.3.2", + "@ag-grid-community/core": "~32.3.2", + "@ag-grid-community/react": "~32.3.2", + "@ag-grid-enterprise/clipboard": "~32.3.2", + "@ag-grid-enterprise/column-tool-panel": "~32.3.2", + "@ag-grid-enterprise/core": "~32.3.2", + "@ag-grid-enterprise/filter-tool-panel": "~32.3.2", + "@ag-grid-enterprise/master-detail": "~32.3.2", + "@ag-grid-enterprise/menu": "~32.3.2", + "@ag-grid-enterprise/range-selection": "~32.3.2", + "@ag-grid-enterprise/server-side-row-model": "~32.3.2", + "@ag-grid-enterprise/set-filter": "~32.3.2", + "@ag-grid-enterprise/status-bar": "~32.3.2", + "@ant-design/icons": "^5.4.0", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@floating-ui/react": "^0.26.25", + "@heroicons/react": "^2.2.0", + "@hookform/resolvers": "^3.9.1", + "@nice/client": "workspace:^", + "@nice/common": "workspace:^", + "@nice/iconer": "workspace:^", + "@nice/mindmap": "workspace:^", + "@nice/ui": "workspace:^", + "@nice/utils": "workspace:^", + "@tanstack/query-async-storage-persister": "^5.51.9", + "@tanstack/react-query": "^5.51.21", + "@tanstack/react-query-persist-client": "^5.51.9", + "@trpc/client": "11.0.0-rc.456", + "@trpc/react-query": "11.0.0-rc.456", + "@trpc/server": "11.0.0-rc.456", + "@xyflow/react": "^12.3.6", + "ag-grid-community": "~32.3.2", + "ag-grid-enterprise": "~32.3.2", + "ag-grid-react": "~32.3.2", + "antd": "^5.19.3", + "axios": "^1.7.2", + "browser-image-compression": "^2.0.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "d3-dag": "^1.1.0", + "d3-hierarchy": "^3.1.2", + "dayjs": "^1.11.12", + "elkjs": "^0.9.3", + "framer-motion": "^11.15.0", + "hls.js": "^1.5.18", + "idb-keyval": "^6.2.1", + "mitt": "^3.0.1", + "quill": "2.0.3", + "react": "18.2.0", + "react-beautiful-dnd": "^13.1.1", + "react-dom": "18.2.0", + "react-dropzone": "^14.3.5", + "react-hook-form": "^7.54.2", + "react-hot-toast": "^2.4.1", + "react-resizable": "^3.0.5", + "react-router-dom": "^6.24.1", + "superjson": "^2.2.1", + "tailwind-merge": "^2.6.0", + "uuid": "^10.0.0", + "yjs": "^13.6.20", + "zod": "^3.23.8" + }, + "devDependencies": { + "@eslint/js": "^9.9.0", + "@types/react": "18.2.38", + "@types/react-dom": "18.2.15", + "@vitejs/plugin-react-swc": "^3.5.0", + "autoprefixer": "^10.4.20", + "eslint": "^9.9.0", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.9", + "globals": "^15.9.0", + "postcss": "^8.4.41", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.4", + "typescript-eslint": "^8.0.1", + "vite": "^5.4.1" + } +} diff --git a/apps/web/src/app/main/course/detail/page.tsx b/apps/web/src/app/main/course/detail/page.tsx index d4b385a..d1bf172 100644 --- a/apps/web/src/app/main/course/detail/page.tsx +++ b/apps/web/src/app/main/course/detail/page.tsx @@ -1,5 +1,4 @@ import CourseDetail from "@web/src/components/models/course/detail/CourseDetail"; -import CourseEditor from "@web/src/components/models/course/manage/CourseEditor"; import { useParams } from "react-router-dom"; export function CourseDetailPage() { diff --git a/apps/web/src/app/main/courses/instructor/page.tsx b/apps/web/src/app/main/courses/instructor/page.tsx index 92920de..032409a 100644 --- a/apps/web/src/app/main/courses/instructor/page.tsx +++ b/apps/web/src/app/main/courses/instructor/page.tsx @@ -52,7 +52,7 @@ export default function InstructorCoursesPage() { renderItem={(course) => ( { - navigate(`/course/${course.id}/manage`, { + navigate(`/course/${course.id}/editor`, { replace: true, }); }} diff --git a/apps/web/src/app/main/home/page.tsx b/apps/web/src/app/main/home/page.tsx index fa47a49..b749462 100644 --- a/apps/web/src/app/main/home/page.tsx +++ b/apps/web/src/app/main/home/page.tsx @@ -1,84 +1,25 @@ -import GraphEditor from '@web/src/components/common/editor/graph/GraphEditor'; -import MindMapEditor from '@web/src/components/presentation/mind-map'; -import React, { useState, useCallback } from 'react'; -import * as tus from 'tus-js-client'; +import GraphEditor from "@web/src/components/common/editor/graph/GraphEditor"; +import FileUploader from "@web/src/components/common/uploader/FileUploader"; + +import React, { useState, useCallback } from "react"; +import * as tus from "tus-js-client"; interface TusUploadProps { - onSuccess?: (response: any) => void; - onError?: (error: Error) => void; + onSuccess?: (response: any) => void; + onError?: (error: Error) => void; } -const TusUploader: React.FC = ({ - onSuccess, - onError -}) => { - const [progress, setProgress] = useState(0); - const [isUploading, setIsUploading] = useState(false); - const [uploadError, setUploadError] = useState(null); - const handleFileUpload = useCallback((file: File) => { - if (!file) return; - setIsUploading(true); - setProgress(0); - setUploadError(null); - // Extract file extension - const extension = file.name.split('.').pop() || ''; - const upload = new tus.Upload(file, { - endpoint: "http://localhost:3000/upload", - retryDelays: [0, 1000, 3000, 5000], - metadata: { - filename: file.name, - size: file.size.toString(), - mimeType: file.type, - extension: extension, - modifiedAt: new Date(file.lastModified).toISOString(), - }, - onProgress: (bytesUploaded, bytesTotal) => { - const percentage = ((bytesUploaded / bytesTotal) * 100).toFixed(2); - setProgress(Number(percentage)); - }, - onSuccess: () => { - setIsUploading(false); - setProgress(100); - onSuccess && onSuccess(upload); - }, - onError: (error) => { - setIsUploading(false); - setUploadError(error.message); - onError && onError(error); - } - }); - - upload.start(); - }, [onSuccess, onError]); - - return ( -
-
- -
- {/*
+const HomePage: React.FC = ({ onSuccess, onError }) => { + return ( +
+ +
+ +
+ {/*
*/} - {/* */} - - { - const file = e.target.files?.[0]; - if (file) handleFileUpload(file); - }} - /> - {isUploading && ( -
- - {progress}% -
- )} - {uploadError && ( -
- 上传错误: {uploadError} -
- )} -
- ); + {/* */} +
+ ); }; -export default TusUploader; \ No newline at end of file +export default HomePage; diff --git a/apps/web/src/components/common/uploader/FileUploader.tsx b/apps/web/src/components/common/uploader/FileUploader.tsx index 0d3c62c..3635063 100644 --- a/apps/web/src/components/common/uploader/FileUploader.tsx +++ b/apps/web/src/components/common/uploader/FileUploader.tsx @@ -1,211 +1,237 @@ -import { useState, useCallback, useRef, memo } from 'react' -import { CloudArrowUpIcon, XMarkIcon, DocumentIcon, ExclamationCircleIcon } from '@heroicons/react/24/outline' -import * as tus from 'tus-js-client' -import { motion, AnimatePresence } from 'framer-motion' -import { toast } from 'react-hot-toast' +// FileUploader.tsx +import React, { useRef, memo, useState } from "react"; +import { + CloudArrowUpIcon, + XMarkIcon, + DocumentIcon, + ExclamationCircleIcon, + CheckCircleIcon, +} from "@heroicons/react/24/outline"; +import { motion, AnimatePresence } from "framer-motion"; +import { toast } from "react-hot-toast"; +import { useTusUpload } from "@web/src/hooks/useTusUpload"; + interface FileUploaderProps { - endpoint?: string - onSuccess?: (url: string) => void - onError?: (error: Error) => void - maxSize?: number - allowedTypes?: string[] - placeholder?: string + endpoint?: string; + onSuccess?: (url: string) => void; + onError?: (error: Error) => void; + maxSize?: number; + allowedTypes?: string[]; + placeholder?: string; } -const FileItem = memo(({ file, progress, onRemove }: { - file: File - progress?: number - onRemove: (name: string) => void -}) => ( - - -
-
-

{file.name}

- -
- {progress !== undefined && ( -
-
- -
- {progress}% -
- )} -
-
-)) +interface FileItemProps { + file: File; + progress?: number; + onRemove: (name: string) => void; + isUploaded: boolean; +} -export default function FileUploader({ - endpoint='', - onSuccess, - onError, - maxSize = 100, - placeholder = '点击或拖拽文件到这里上传', - allowedTypes = ['*/*'] -}: FileUploaderProps) { - const [isDragging, setIsDragging] = useState(false) - const [files, setFiles] = useState([]) - const [progress, setProgress] = useState<{ [key: string]: number }>({}) - const fileInputRef = useRef(null) +const FileItem: React.FC = memo( + ({ file, progress, onRemove, isUploaded }) => ( + + +
+
+

+ {file.name} +

+ +
+ {!isUploaded && progress !== undefined && ( +
+
+ +
+ + {progress}% + +
+ )} + {isUploaded && ( +
+ + 上传完成 +
+ )} +
+
+ ) +); - const handleError = useCallback((error: Error) => { - toast.error(error.message) - onError?.(error) - }, [onError]) +const FileUploader: React.FC = ({ + endpoint = "", + onSuccess, + onError, + maxSize = 100, + placeholder = "点击或拖拽文件到这里上传", + allowedTypes = ["*/*"], +}) => { + const [isDragging, setIsDragging] = useState(false); + const [files, setFiles] = useState< + Array<{ file: File; isUploaded: boolean }> + >([]); + const fileInputRef = useRef(null); - const handleDrag = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - if (e.type === 'dragenter' || e.type === 'dragover') { - setIsDragging(true) - } else if (e.type === 'dragleave') { - setIsDragging(false) - } - }, []) + const { progress, isUploading, uploadError, handleFileUpload } = + useTusUpload(); - const validateFile = useCallback((file: File) => { - if (file.size > maxSize * 1024 * 1024) { - throw new Error(`文件大小不能超过 ${maxSize}MB`) - } - if (!allowedTypes.includes('*/*') && !allowedTypes.includes(file.type)) { - throw new Error(`不支持的文件类型。支持的类型: ${allowedTypes.join(', ')}`) - } - }, [maxSize, allowedTypes]) + const handleError = (error: Error) => { + toast.error(error.message); + onError?.(error); + }; - const uploadFile = async (file: File) => { - try { - validateFile(file) + const handleDrag = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + if (e.type === "dragenter" || e.type === "dragover") { + setIsDragging(true); + } else if (e.type === "dragleave") { + setIsDragging(false); + } + }; - const upload = new tus.Upload(file, { - endpoint, - retryDelays: [0, 3000, 5000, 10000, 20000], - metadata: { - filename: file.name, - filetype: file.type - }, - onError: handleError, - onProgress: (bytesUploaded, bytesTotal) => { - const percentage = (bytesUploaded / bytesTotal * 100).toFixed(2) - setProgress(prev => ({ - ...prev, - [file.name]: parseFloat(percentage) - })) - }, - onSuccess: () => { - onSuccess?.(upload.url || '') - setProgress(prev => { - const newProgress = { ...prev } - delete newProgress[file.name] - return newProgress - }) - } - }) + const validateFile = (file: File) => { + if (file.size > maxSize * 1024 * 1024) { + throw new Error(`文件大小不能超过 ${maxSize}MB`); + } + if ( + !allowedTypes.includes("*/*") && + !allowedTypes.includes(file.type) + ) { + throw new Error( + `不支持的文件类型。支持的类型: ${allowedTypes.join(", ")}` + ); + } + }; - upload.start() - } catch (error) { - handleError(error as Error) - } - } + const uploadFile = (file: File) => { + try { + validateFile(file); + handleFileUpload( + file, + (upload) => { + onSuccess?.(upload.url || ""); + setFiles((prev) => + prev.map((f) => + f.file.name === file.name + ? { ...f, isUploaded: true } + : f + ) + ); + }, + handleError + ); + } catch (error) { + handleError(error as Error); + } + }; - const handleDrop = useCallback((e: React.DragEvent) => { - e.preventDefault() - e.stopPropagation() - setIsDragging(false) + const handleDrop = (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsDragging(false); - const droppedFiles = Array.from(e.dataTransfer.files) - setFiles(prev => [...prev, ...droppedFiles]) - droppedFiles.forEach(uploadFile) - }, []) + const droppedFiles = Array.from(e.dataTransfer.files); + setFiles((prev) => [ + ...prev, + ...droppedFiles.map((file) => ({ file, isUploaded: false })), + ]); + droppedFiles.forEach(uploadFile); + }; - const handleFileSelect = (e: React.ChangeEvent) => { - if (e.target.files) { - const selectedFiles = Array.from(e.target.files) - setFiles(prev => [...prev, ...selectedFiles]) - selectedFiles.forEach(uploadFile) - } - } + const handleFileSelect = (e: React.ChangeEvent) => { + if (e.target.files) { + const selectedFiles = Array.from(e.target.files); + setFiles((prev) => [ + ...prev, + ...selectedFiles.map((file) => ({ file, isUploaded: false })), + ]); + selectedFiles.forEach(uploadFile); + } + }; - const removeFile = (fileName: string) => { - setFiles(prev => prev.filter(file => file.name !== fileName)) - setProgress(prev => { - const newProgress = { ...prev } - delete newProgress[fileName] - return newProgress - }) - } + const removeFile = (fileName: string) => { + setFiles((prev) => prev.filter(({ file }) => file.name !== fileName)); + }; - return ( -
- fileInputRef.current?.click()} - aria-label="文件上传区域" - > - + const handleClick = () => { + fileInputRef.current?.click(); + }; -
- - - -
-

{placeholder}

-
-

- - 支持的文件类型: {allowedTypes.join(', ')} · 最大文件大小: {maxSize}MB -

-
-
+ return ( +
+
+ + +

{placeholder}

+ {isDragging && ( +
+

+ 释放文件以上传 +

+
+ )} +
- -
- {files.map(file => ( - - ))} -
-
-
- ) -} \ No newline at end of file + +
+ {files.map(({ file, isUploaded }) => ( + + ))} +
+
+ + {uploadError && ( +
+ + {uploadError} +
+ )} +
+ ); +}; + +export default FileUploader; diff --git a/apps/web/src/components/presentation/TusUploader.tsx b/apps/web/src/components/presentation/TusUploader.tsx new file mode 100644 index 0000000..3d8b983 --- /dev/null +++ b/apps/web/src/components/presentation/TusUploader.tsx @@ -0,0 +1,40 @@ +import React from "react"; +import { useTusUpload } from "@web/src/hooks/useTusUpload"; // 假设 useTusUpload 在同一个目录下 +import * as tus from "tus-js-client"; +interface TusUploadProps { + onSuccess?: (upload: tus.Upload) => void; + onError?: (error: Error) => void; +} + +export const TusUploader: React.FC = ({ + onSuccess, + onError, +}) => { + const { progress, isUploading, uploadError, handleFileUpload } = + useTusUpload(); + + return ( +
+ { + const file = e.target.files?.[0]; + if (file) handleFileUpload(file, onSuccess, onError); + }} + /> + + {isUploading && ( +
+ + {progress}% +
+ )} + + {uploadError && ( +
上传错误: {uploadError}
+ )} +
+ ); +}; + +export default TusUploader; diff --git a/apps/web/src/env.ts b/apps/web/src/env.ts index d364783..1f22121 100755 --- a/apps/web/src/env.ts +++ b/apps/web/src/env.ts @@ -2,6 +2,8 @@ export const env: { APP_NAME: string; SERVER_IP: string; VERSION: string; + UOLOAD_PORT: string; + SERVER_PORT: string; } = { APP_NAME: import.meta.env.PROD ? (window as any).env.VITE_APP_APP_NAME @@ -9,6 +11,12 @@ export const env: { SERVER_IP: import.meta.env.PROD ? (window as any).env.VITE_APP_SERVER_IP : import.meta.env.VITE_APP_SERVER_IP, + UOLOAD_PORT: import.meta.env.PROD + ? (window as any).env.VITE_APP_UOLOAD_PORT + : import.meta.env.VITE_APP_UOLOAD_PORT, + SERVER_PORT: import.meta.env.PROD + ? (window as any).env.VITE_APP_SERVER_PORT + : import.meta.env.VITE_APP_SERVER_PORT, VERSION: import.meta.env.PROD ? (window as any).env.VITE_APP_VERSION : import.meta.env.VITE_APP_VERSION, diff --git a/apps/web/src/hooks/useTusUpload.ts b/apps/web/src/hooks/useTusUpload.ts new file mode 100644 index 0000000..695d5a4 --- /dev/null +++ b/apps/web/src/hooks/useTusUpload.ts @@ -0,0 +1,125 @@ +import { useState } from "react"; +import * as tus from "tus-js-client"; +import { env } from "../env"; +import { getCompressedImageUrl } from "@nice/utils"; +// useTusUpload.ts +interface UploadProgress { + fileId: string; + progress: number; +} + +interface UploadResult { + compressedUrl: string; + url: string; + fileId: string; +} + +export function useTusUpload() { + const [uploadProgress, setUploadProgress] = useState< + Record + >({}); + const [isUploading, setIsUploading] = useState(false); + const [uploadError, setUploadError] = useState(null); + + const getFileId = (url: string) => { + const parts = url.split("/"); + const uploadIndex = parts.findIndex((part) => part === "upload"); + if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) { + throw new Error("Invalid upload URL format"); + } + return parts.slice(uploadIndex + 1, uploadIndex + 5).join("/"); + }; + const getResourceUrl = (url: string) => { + const parts = url.split("/"); + const uploadIndex = parts.findIndex((part) => part === "upload"); + if (uploadIndex === -1 || uploadIndex + 4 >= parts.length) { + throw new Error("Invalid upload URL format"); + } + const resUrl = `http://${env.SERVER_IP}:${env.UOLOAD_PORT}/uploads/${parts.slice(uploadIndex + 1, uploadIndex + 6).join("/")}`; + + return resUrl; + }; + const handleFileUpload = async ( + file: File, + onSuccess: (result: UploadResult) => void, + onError: (error: Error) => void, + fileKey: string // 添加文件唯一标识 + ) => { + // if (!file || !file.name || !file.type) { + // const error = new Error("不可上传该类型文件"); + // setUploadError(error.message); + // onError(error); + // return; + // } + + setIsUploading(true); + setUploadProgress((prev) => ({ ...prev, [fileKey]: 0 })); + setUploadError(null); + + try { + const upload = new tus.Upload(file, { + endpoint: `http://${env.SERVER_IP}:${env.SERVER_PORT}/upload`, + retryDelays: [0, 1000, 3000, 5000], + metadata: { + filename: file.name, + filetype: file.type, + size: file.size as any, + }, + onProgress: (bytesUploaded, bytesTotal) => { + const progress = Number( + ((bytesUploaded / bytesTotal) * 100).toFixed(2) + ); + setUploadProgress((prev) => ({ + ...prev, + [fileKey]: progress, + })); + }, + onSuccess: async (payload) => { + try { + if (upload.url) { + const fileId = getFileId(upload.url); + const url = getResourceUrl(upload.url); + setIsUploading(false); + setUploadProgress((prev) => ({ + ...prev, + [fileKey]: 100, + })); + onSuccess({ + compressedUrl: getCompressedImageUrl(url), + url, + fileId, + }); + } + } catch (error) { + const err = + error instanceof Error + ? error + : new Error("Unknown error"); + setIsUploading(false); + setUploadError(err.message); + onError(err); + } + }, + onError: (error) => { + setIsUploading(false); + setUploadError(error.message); + onError(error); + }, + }); + upload.start(); + } catch (error) { + const err = + error instanceof Error ? error : new Error("Upload failed"); + setIsUploading(false); + setUploadError(err.message); + onError(err); + } + }; + + return { + uploadProgress, + isUploading, + uploadError, + handleFileUpload, + }; +} diff --git a/packages/client/src/api/hooks/useCourse.ts b/packages/client/src/api/hooks/useCourse.ts index 5061072..8e09848 100644 --- a/packages/client/src/api/hooks/useCourse.ts +++ b/packages/client/src/api/hooks/useCourse.ts @@ -1,56 +1,76 @@ import { api } from "../trpc"; -export function useCourse() { - const utils = api.useUtils(); - return { - // Queries - findMany: api.course.findMany.useQuery, - findFirst: api.course.findFirst.useQuery, - findManyWithCursor: api.course.findManyWithCursor.useQuery, +// 定义返回类型 +type UseCourseReturn = { + // Queries + findMany: typeof api.course.findMany.useQuery; + findFirst: typeof api.course.findFirst.useQuery; + findManyWithCursor: typeof api.course.findManyWithCursor.useQuery; - // Mutations - create: api.course.create.useMutation({ - onSuccess: () => { - utils.course.invalidate() - utils.course.findMany.invalidate(); - utils.course.findManyWithCursor.invalidate(); - utils.course.findManyWithPagination.invalidate() - }, - }), - update: api.course.update.useMutation({ - onSuccess: () => { - utils.course.findMany.invalidate(); - utils.course.findManyWithCursor.invalidate(); - utils.course.findManyWithPagination.invalidate() - }, - }), - createMany: api.course.createMany.useMutation({ - onSuccess: () => { - utils.course.findMany.invalidate(); - utils.course.findManyWithCursor.invalidate(); - utils.course.findManyWithPagination.invalidate() - }, - }), - deleteMany: api.course.deleteMany.useMutation({ - onSuccess: () => { - utils.course.findMany.invalidate(); - utils.course.findManyWithCursor.invalidate(); - utils.course.findManyWithPagination.invalidate() - }, - }), - softDeleteByIds: api.course.softDeleteByIds.useMutation({ - onSuccess: () => { - utils.course.findMany.invalidate(); - utils.course.findManyWithCursor.invalidate(); - utils.course.findManyWithPagination.invalidate() - }, - }), - updateOrder: api.course.updateOrder.useMutation({ - onSuccess: () => { - utils.course.findMany.invalidate(); - utils.course.findManyWithCursor.invalidate(); - utils.course.findManyWithPagination.invalidate() - }, - }) - }; -} \ No newline at end of file + // Mutations + create: ReturnType; + // create: ReturnType; + update: ReturnType; + // update: ReturnType; + createMany: ReturnType; + deleteMany: ReturnType; + softDeleteByIds: ReturnType; + // softDeleteByIds: ReturnType; + updateOrder: ReturnType; + // updateOrder: ReturnType; +}; + +export function useCourse(): UseCourseReturn { + const utils = api.useUtils(); + return { + // Queries + findMany: api.course.findMany.useQuery, + findFirst: api.course.findFirst.useQuery, + findManyWithCursor: api.course.findManyWithCursor.useQuery, + + // Mutations + create: api.course.create.useMutation({ + onSuccess: () => { + utils.course.invalidate(); + utils.course.findMany.invalidate(); + utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate(); + }, + }), + update: api.course.update.useMutation({ + onSuccess: () => { + utils.course.findMany.invalidate(); + utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate(); + }, + }), + createMany: api.course.createMany.useMutation({ + onSuccess: () => { + utils.course.findMany.invalidate(); + utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate(); + }, + }), + deleteMany: api.course.deleteMany.useMutation({ + onSuccess: () => { + utils.course.findMany.invalidate(); + utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate(); + }, + }), + softDeleteByIds: api.course.softDeleteByIds.useMutation({ + onSuccess: () => { + utils.course.findMany.invalidate(); + utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate(); + }, + }), + updateOrder: api.course.updateOrder.useMutation({ + onSuccess: () => { + utils.course.findMany.invalidate(); + utils.course.findManyWithCursor.invalidate(); + utils.course.findManyWithPagination.invalidate(); + }, + }), + }; +} diff --git a/packages/client/src/api/hooks/useDepartment.ts b/packages/client/src/api/hooks/useDepartment.ts index 27e9c19..d12928b 100755 --- a/packages/client/src/api/hooks/useDepartment.ts +++ b/packages/client/src/api/hooks/useDepartment.ts @@ -50,35 +50,6 @@ export function useDepartment() { return node; }); }; - - // const getTreeData = () => { - // const uniqueData: DepartmentDto[] = getCacheDataFromQuery( - // queryClient, - // api.department - // ); - // const treeData: DataNode[] = buildTree(uniqueData); - // return treeData; - // }; - // const getTreeData = () => { - // const cacheArray = queryClient.getQueriesData({ - // queryKey: getQueryKey(api.department.getChildren), - // }); - // const data: DepartmentDto[] = cacheArray - // .flatMap((cache) => cache.slice(1)) - // .flat() - // .filter((item) => item !== undefined) as any; - // const uniqueDataMap = new Map(); - - // data?.forEach((item) => { - // if (item && item.id) { - // uniqueDataMap.set(item.id, item); - // } - // }); - // // Convert the Map back to an array - // const uniqueData: DepartmentDto[] = Array.from(uniqueDataMap.values()); - // const treeData: DataNode[] = buildTree(uniqueData); - // return treeData; - // }; const getDept = (key: string) => { return findQueryData(queryClient, api.department, key); }; diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index 3379233..f0a72d9 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -1,28 +1,20 @@ { - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "target": "esnext", - "module": "esnext", - "allowJs": true, - "esModuleInterop": true, - "lib": [ - "dom", - "esnext" - ], - "jsx": "react-jsx", - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "outDir": "dist", - "moduleResolution": "node", - "incremental": true, - "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo" - }, - "include": [ - "src" - ], - "exclude": [ - "node_modules", - "dist" - ], -} \ No newline at end of file + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "allowJs": true, + "esModuleInterop": true, + "lib": ["dom", "esnext"], + "jsx": "react-jsx", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "moduleResolution": "node", + "incremental": true, + "tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo", + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/common/package.json b/packages/common/package.json index eb5c31c..9c6edea 100644 --- a/packages/common/package.json +++ b/packages/common/package.json @@ -1,34 +1,35 @@ { - "name": "@nice/common", - "version": "1.0.0", - "main": "./dist/index.js", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "private": true, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "generate": "pnpm prisma generate", - "build": "pnpm generate && tsup", - "dev": "pnpm generate && tsup --watch ", - "studio": "pnpm prisma studio", - "db:clear": "rm -rf prisma/migrations && pnpm prisma migrate dev --name init", - "postinstall": "pnpm generate" - }, - "dependencies": { - "@prisma/client": "5.17.0", - "prisma": "5.17.0" - }, - "peerDependencies": { - "zod": "^3.23.8", - "yjs": "^13.6.20", - "lib0": "^0.2.98" - }, - "devDependencies": { - "@types/node": "^20.3.1", - "ts-node": "^10.9.1", - "typescript": "^5.5.4", - "concurrently": "^8.0.0", - "tsup": "^8.3.5", - "rimraf": "^6.0.1" - } -} \ No newline at end of file + "name": "@nice/common", + "version": "1.0.0", + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "private": true, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "generate": "pnpm prisma generate", + "build": "pnpm generate && tsup", + "dev": "pnpm generate && tsup --watch ", + "dev-nowatch": "pnpm generate && tsup --no-watch ", + "studio": "pnpm prisma studio", + "db:clear": "rm -rf prisma/migrations && pnpm prisma migrate dev --name init", + "postinstall": "pnpm generate" + }, + "dependencies": { + "@prisma/client": "5.17.0", + "prisma": "5.17.0" + }, + "peerDependencies": { + "zod": "^3.23.8", + "yjs": "^13.6.20", + "lib0": "^0.2.98" + }, + "devDependencies": { + "@types/node": "^20.3.1", + "ts-node": "^10.9.1", + "typescript": "^5.5.4", + "concurrently": "^8.0.0", + "tsup": "^8.3.5", + "rimraf": "^6.0.1" + } +} diff --git a/packages/common/src/select.ts b/packages/common/src/select.ts index 7c5b46e..cd9979e 100644 --- a/packages/common/src/select.ts +++ b/packages/common/src/select.ts @@ -74,16 +74,16 @@ export const courseDetailSelect: Prisma.CourseSelect = { level: true, requirements: true, objectives: true, - skills: true, - audiences: true, - totalDuration: true, - totalLectures: true, - averageRating: true, - numberOfReviews: true, - numberOfStudents: true, - completionRate: true, + // skills: true, + // audiences: true, + // totalDuration: true, + // totalLectures: true, + // averageRating: true, + // numberOfReviews: true, + // numberOfStudents: true, + // completionRate: true, status: true, - isFeatured: true, + // isFeatured: true, createdAt: true, publishedAt: true, // 关联表选择 diff --git a/packages/common/tsup.config.ts b/packages/common/tsup.config.ts index 1eacf7a..8dc73a1 100644 --- a/packages/common/tsup.config.ts +++ b/packages/common/tsup.config.ts @@ -1,10 +1,18 @@ -import { defineConfig } from 'tsup'; +import { defineConfig } from "tsup"; export default defineConfig({ - entry: ['src/index.ts'], - format: ['cjs', 'esm'], - splitting: false, - sourcemap: true, - clean: false, - dts: true + entry: ["src/index.ts"], + format: ["cjs", "esm"], + splitting: false, + sourcemap: true, + clean: false, + dts: true, + // watch 可以是布尔值或字符串数组 + watch: [ + "src/**/*.ts", + "!src/**/*.test.ts", + "!src/**/*.spec.ts", + "!node_modules/**", + "!dist/**", + ], }); diff --git a/packages/utils/package.json b/packages/utils/package.json index 8f3fd0e..c02c2c3 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -8,6 +8,7 @@ "scripts": { "build": "tsup", "dev": "tsup --watch", + "dev-static": "tsup --no-watch", "clean": "rimraf dist", "typecheck": "tsc --noEmit" }, diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index aea398b..77aa06e 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -4,20 +4,38 @@ * @returns 唯一ID字符串 */ export function generateUniqueId(prefix?: string): string { - // 获取当前时间戳 - const timestamp = Date.now(); + // 获取当前时间戳 + const timestamp = Date.now(); - // 生成随机数部分 - const randomPart = Math.random().toString(36).substring(2, 8); + // 生成随机数部分 + const randomPart = Math.random().toString(36).substring(2, 8); - // 获取环境特定的额外随机性 - const environmentPart = typeof window !== 'undefined' - ? window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36) - : require('crypto').randomBytes(4).toString('hex'); + // 获取环境特定的额外随机性 + const environmentPart = + typeof window !== "undefined" + ? window.crypto.getRandomValues(new Uint32Array(1))[0].toString(36) + : require("crypto").randomBytes(4).toString("hex"); - // 组合所有部分 - const uniquePart = `${timestamp}${randomPart}${environmentPart}`; + // 组合所有部分 + const uniquePart = `${timestamp}${randomPart}${environmentPart}`; - // 如果提供了前缀,则添加前缀 - return prefix ? `${prefix}_${uniquePart}` : uniquePart; -} \ No newline at end of file + // 如果提供了前缀,则添加前缀 + return prefix ? `${prefix}_${uniquePart}` : uniquePart; +} +export const formatFileSize = (bytes: number) => { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; + if (bytes < 1024 * 1024 * 1024) + return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; + return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`; +}; +// 压缩图片路径生成函数 +export const getCompressedImageUrl = (originalUrl: string): string => { + if (!originalUrl) { + return originalUrl; + } + const cleanUrl = originalUrl.split(/[?#]/)[0]; // 移除查询参数和哈希 + const lastSlashIndex = cleanUrl.lastIndexOf("/"); + return `${cleanUrl.slice(0, lastSlashIndex)}/compressed/${cleanUrl.slice(lastSlashIndex + 1).replace(/\.[^.]+$/, ".webp")}`; +}; +export * from "./types"; diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts new file mode 100644 index 0000000..d3ed883 --- /dev/null +++ b/packages/utils/src/types.ts @@ -0,0 +1 @@ +export type NonVoid = T extends void ? never : T; \ No newline at end of file