create-pardx-scaffold 0.1.2 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/package.json +1 -1
  2. package/template/apps/api/libs/infra/clients/internal/ip-info/dto/ip-info.dto.ts +15 -0
  3. package/template/apps/api/libs/infra/clients/internal/ip-info/index.ts +3 -0
  4. package/template/apps/api/libs/infra/clients/internal/ip-info/ip-info.client.ts +57 -0
  5. package/template/apps/api/libs/infra/clients/internal/ip-info/ip-info.module.ts +17 -0
  6. package/template/apps/api/libs/infra/common/ts-rest/response.helper.ts +9 -1
  7. package/template/apps/api/libs/infra/shared-services/file-storage/README.md +4 -4
  8. package/template/apps/api/libs/infra/shared-services/file-storage/bucket-resolver.ts +4 -4
  9. package/template/apps/api/libs/infra/shared-services/file-storage/file-storage.module.ts +3 -3
  10. package/template/apps/api/libs/infra/shared-services/ip-geo/continent-mapping.ts +86 -0
  11. package/template/apps/api/libs/infra/shared-services/ip-geo/index.ts +37 -0
  12. package/template/apps/api/libs/infra/shared-services/ip-geo/ip-geo.module.ts +21 -0
  13. package/template/apps/api/libs/infra/shared-services/ip-geo/ip-geo.service.ts +135 -0
  14. package/template/apps/api/libs/infra/shared-services/uploader/index.ts +1 -1
  15. package/template/apps/api/libs/infra/shared-services/uploader/uploader.service.ts +14 -6
  16. package/template/apps/api/src/app.module.ts +3 -12
  17. package/template/apps/api/src/modules/uploader/uploader.controller.ts +290 -0
  18. package/template/apps/api/src/modules/uploader/uploader.module.ts +17 -0
  19. package/template/apps/web/components/index.ts +21 -0
  20. package/template/apps/web/components/skeletons.tsx +188 -0
  21. package/template/apps/web/components/suspense-utils.tsx +123 -0
  22. package/template/apps/web/lib/api/contracts/client.ts +4 -0
  23. package/template/apps/web/lib/deprecation-warning.ts +150 -0
  24. package/template/apps/web/lib/queries/optimistic-update.ts +204 -0
  25. package/template/packages/constants/src/index.ts +22 -0
  26. package/template/apps/api/src/modules/health/health.controller.ts +0 -13
  27. package/template/apps/api/src/modules/health/health.module.ts +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pardx-scaffold",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "description": "Scaffold a new project from PardxAI monorepo (git-tracked files)",
5
5
  "license": "MIT",
6
6
  "bin": "./cli.js",
@@ -0,0 +1,15 @@
1
+ /**
2
+ * IP Info API Response DTO
3
+ *
4
+ * @description ipinfo.io API 返回的数据结构
5
+ */
6
+ export interface IpInfoResponse {
7
+ ip: string;
8
+ country: string;
9
+ region?: string;
10
+ city?: string;
11
+ loc?: string;
12
+ org?: string;
13
+ postal?: string;
14
+ timezone?: string;
15
+ }
@@ -0,0 +1,3 @@
1
+ export * from './ip-info.module';
2
+ export * from './ip-info.client';
3
+ export * from './dto/ip-info.dto';
@@ -0,0 +1,57 @@
1
+ /**
2
+ * IP Info Client
3
+ *
4
+ * 职责:仅负责与 ipinfo.io API 通信
5
+ * - 调用 ipinfo.io API 获取 IP 地理位置信息
6
+ * - 不访问数据库
7
+ * - 不包含业务逻辑
8
+ */
9
+ import { Injectable, Inject } from '@nestjs/common';
10
+ import { HttpService } from '@nestjs/axios';
11
+ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
12
+ import { Logger } from 'winston';
13
+ import { ConfigService } from '@nestjs/config';
14
+ import { firstValueFrom } from 'rxjs';
15
+ import { IpInfoResponse } from './dto/ip-info.dto';
16
+ import { IpInfoConfig } from '@/config/validation';
17
+
18
+ @Injectable()
19
+ export class IpInfoClient {
20
+ private readonly config: IpInfoConfig;
21
+
22
+ constructor(
23
+ private readonly httpService: HttpService,
24
+ private readonly configService: ConfigService,
25
+ @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
26
+ ) {
27
+ this.config = configService.getOrThrow<IpInfoConfig>('ipinfo');
28
+ }
29
+
30
+ /**
31
+ * 获取 IP 地理位置信息
32
+ * @param ip IP 地址
33
+ * @returns IP 信息
34
+ */
35
+ async getIpInfo(ip: string): Promise<IpInfoResponse> {
36
+ const url = `${this.config.url}/${ip}?token=${this.config.token}`;
37
+
38
+ try {
39
+ const response = await firstValueFrom(
40
+ this.httpService.get<IpInfoResponse>(url, {
41
+ timeout: 10000,
42
+ }),
43
+ );
44
+
45
+ this.logger.debug(`IP info fetched for ${ip}`, {
46
+ country: response.data.country,
47
+ });
48
+
49
+ return response.data;
50
+ } catch (error) {
51
+ this.logger.error(`Failed to fetch IP info for ${ip}`, {
52
+ error: (error as Error).message,
53
+ });
54
+ throw error;
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,17 @@
1
+ import { Module } from '@nestjs/common';
2
+ import { HttpModule } from '@nestjs/axios';
3
+ import { ConfigModule } from '@nestjs/config';
4
+ import { IpInfoClient } from './ip-info.client';
5
+
6
+ @Module({
7
+ imports: [
8
+ ConfigModule,
9
+ HttpModule.register({
10
+ timeout: 10000,
11
+ maxRedirects: 3,
12
+ }),
13
+ ],
14
+ providers: [IpInfoClient],
15
+ exports: [IpInfoClient],
16
+ })
17
+ export class IpInfoClientModule {}
@@ -42,6 +42,14 @@ export type TsRestResponse<T, S extends number = 200> = {
42
42
  body: SuccessBody<T> | ErrorBody;
43
43
  };
44
44
 
45
+ /**
46
+ * ts-rest 成功响应类型(仅包含成功体)
47
+ */
48
+ export type TsRestSuccessResponse<T, S extends number = 200> = {
49
+ status: S;
50
+ body: SuccessBody<T>;
51
+ };
52
+
45
53
  /**
46
54
  * 创建成功响应
47
55
  *
@@ -56,7 +64,7 @@ export type TsRestResponse<T, S extends number = 200> = {
56
64
  * }
57
65
  * ```
58
66
  */
59
- export function success<T>(data: T, status: 200 = 200): TsRestResponse<T, 200> {
67
+ export function success<T>(data: T, status: 200 = 200): TsRestSuccessResponse<T, 200> {
60
68
  return {
61
69
  status,
62
70
  body: {
@@ -59,7 +59,7 @@ file-storage/
59
59
 
60
60
  ```typescript
61
61
  import { Module } from '@nestjs/common';
62
- import { FileStorageServiceModule } from '@app/services/file-storage';
62
+ import { FileStorageServiceModule } from '@app/shared-services/file-storage';
63
63
 
64
64
  @Module({
65
65
  imports: [FileStorageServiceModule],
@@ -71,7 +71,7 @@ export class VideoModule {}
71
71
 
72
72
  ```typescript
73
73
  import { Injectable } from '@nestjs/common';
74
- import { FileStorageService } from '@app/services/file-storage';
74
+ import { FileStorageService } from '@app/shared-services/file-storage';
75
75
  import { FileBucketVendor } from '@prisma/client';
76
76
 
77
77
  @Injectable()
@@ -244,7 +244,7 @@ const audioInfo = await fileStorage.getAudioInfo('oss', 'bucket', 'audio.mp3');
244
244
  用于高级场景,直接操作存储客户端。
245
245
 
246
246
  ```typescript
247
- import { FileStorageClientFactory } from '@app/services/file-storage';
247
+ import { FileStorageClientFactory } from '@app/shared-services/file-storage';
248
248
 
249
249
  // 获取客户端
250
250
  const client = factory.getClient('oss', 'my-bucket');
@@ -267,7 +267,7 @@ const configs = factory.getAllBucketConfigs();
267
267
  用于存储桶解析的高级场景。
268
268
 
269
269
  ```typescript
270
- import { BucketResolver } from '@app/services/file-storage';
270
+ import { BucketResolver } from '@app/shared-services/file-storage';
271
271
 
272
272
  // 解析存储桶
273
273
  const result = await resolver.resolve({
@@ -16,7 +16,7 @@ import { Logger } from 'winston';
16
16
  import { FileBucketVendor } from '@prisma/client';
17
17
 
18
18
  import { PardxUploader } from '@app/clients/internal/file-storage';
19
- import { IpInfoService } from '@app/services/ip-info';
19
+ import { IpGeoService } from '@app/shared-services/ip-geo';
20
20
  import { AppConfig } from '@/config/validation';
21
21
  import arrayUtil from '@/utils/array.util';
22
22
  import enviromentUtil from '@/utils/enviroment.util';
@@ -89,12 +89,12 @@ export class BucketResolver {
89
89
  * 构造函数
90
90
  *
91
91
  * @param {ConfigService} configService - NestJS 配置服务
92
- * @param {IpInfoService} ipInfoService - IP 信息服务
92
+ * @param {IpGeoService} ipGeoService - IP 地理位置服务(infra 层)
93
93
  * @param {Logger} logger - Winston 日志记录器
94
94
  */
95
95
  constructor(
96
96
  private readonly configService: ConfigService,
97
- private readonly ipInfoService: IpInfoService,
97
+ private readonly ipGeoService: IpGeoService,
98
98
  @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
99
99
  ) {
100
100
  this.bucketConfigs =
@@ -178,7 +178,7 @@ export class BucketResolver {
178
178
  // 确定区域
179
179
  let zone = enviromentUtil.getBaseZone();
180
180
  if (ip) {
181
- const continent = await this.ipInfoService.getContinent(ip);
181
+ const continent = await this.ipGeoService.getContinent(ip);
182
182
  zone = continent ?? zone;
183
183
  }
184
184
  locale = locale ?? zone;
@@ -12,7 +12,7 @@
12
12
  import { Module } from '@nestjs/common';
13
13
  import { ConfigModule } from '@nestjs/config';
14
14
  import { HttpModule } from '@nestjs/axios';
15
- import { IpInfoServiceModule } from '@app/services/ip-info';
15
+ import { IpGeoModule } from '@app/shared-services/ip-geo';
16
16
  import { RedisModule } from '@app/redis';
17
17
  import { FileStorageService } from './file-storage.service';
18
18
  import { FileStorageClientFactory } from './file-storage.factory';
@@ -32,7 +32,7 @@ import { BucketResolver } from './bucket-resolver';
32
32
  * - `ConfigModule`: 配置服务
33
33
  * - `HttpModule`: HTTP 客户端
34
34
  * - `RedisModule`: Redis 缓存
35
- * - `IpInfoServiceModule`: IP 信息服务(用于区域感知)
35
+ * - `IpGeoModule`: IP 地理位置服务(用于区域感知,纯 infra 层)
36
36
  *
37
37
  * @example
38
38
  * ```typescript
@@ -55,7 +55,7 @@ import { BucketResolver } from './bucket-resolver';
55
55
  * ```
56
56
  */
57
57
  @Module({
58
- imports: [ConfigModule, RedisModule, IpInfoServiceModule, HttpModule],
58
+ imports: [ConfigModule, RedisModule, IpGeoModule, HttpModule],
59
59
  providers: [BucketResolver, FileStorageClientFactory, FileStorageService],
60
60
  exports: [FileStorageService, FileStorageClientFactory, BucketResolver],
61
61
  })
@@ -0,0 +1,86 @@
1
+ /**
2
+ * @fileoverview 国家代码到大洲的静态映射
3
+ *
4
+ * 本文件提供纯静态的国家代码到大洲映射,不依赖数据库。
5
+ * 用于 infra 层的 IP 地理位置服务,避免 infra 依赖 domain。
6
+ *
7
+ * @module ip-geo/continent-mapping
8
+ */
9
+
10
+ /**
11
+ * 大洲类型
12
+ */
13
+ export type Continent = 'as' | 'eu' | 'na' | 'sa' | 'af' | 'oc' | 'an';
14
+
15
+ /**
16
+ * 大洲到国家代码的映射
17
+ *
18
+ * 数据来源:ISO 3166-1 alpha-2 国家代码
19
+ */
20
+ export const CONTINENT_COUNTRIES: Record<Continent, string[]> = {
21
+ // 亚洲 (Asia)
22
+ as: [
23
+ 'AF', 'AM', 'AZ', 'BH', 'BD', 'BT', 'BN', 'KH', 'CN', 'CY', 'GE', 'HK',
24
+ 'IN', 'ID', 'IR', 'IQ', 'IL', 'JP', 'JO', 'KZ', 'KW', 'KG', 'LA', 'LB',
25
+ 'MO', 'MY', 'MV', 'MN', 'MM', 'NP', 'KP', 'OM', 'PK', 'PS', 'PH', 'QA',
26
+ 'SA', 'SG', 'KR', 'LK', 'SY', 'TW', 'TJ', 'TH', 'TL', 'TR', 'TM', 'AE',
27
+ 'UZ', 'VN', 'YE',
28
+ ],
29
+ // 欧洲 (Europe)
30
+ eu: [
31
+ 'AL', 'AD', 'AT', 'BY', 'BE', 'BA', 'BG', 'HR', 'CZ', 'DK', 'EE', 'FI',
32
+ 'FR', 'DE', 'GR', 'HU', 'IS', 'IE', 'IT', 'XK', 'LV', 'LI', 'LT', 'LU',
33
+ 'MT', 'MD', 'MC', 'ME', 'NL', 'MK', 'NO', 'PL', 'PT', 'RO', 'RU', 'SM',
34
+ 'RS', 'SK', 'SI', 'ES', 'SE', 'CH', 'UA', 'GB', 'VA',
35
+ ],
36
+ // 北美洲 (North America)
37
+ na: [
38
+ 'AG', 'BS', 'BB', 'BZ', 'CA', 'CR', 'CU', 'DM', 'DO', 'SV', 'GD', 'GT',
39
+ 'HT', 'HN', 'JM', 'MX', 'NI', 'PA', 'KN', 'LC', 'VC', 'TT', 'US',
40
+ ],
41
+ // 南美洲 (South America)
42
+ sa: [
43
+ 'AR', 'BO', 'BR', 'CL', 'CO', 'EC', 'GY', 'PY', 'PE', 'SR', 'UY', 'VE',
44
+ ],
45
+ // 非洲 (Africa)
46
+ af: [
47
+ 'DZ', 'AO', 'BJ', 'BW', 'BF', 'BI', 'CV', 'CM', 'CF', 'TD', 'KM', 'CG',
48
+ 'CD', 'DJ', 'EG', 'GQ', 'ER', 'SZ', 'ET', 'GA', 'GM', 'GH', 'GN', 'GW',
49
+ 'CI', 'KE', 'LS', 'LR', 'LY', 'MG', 'MW', 'ML', 'MR', 'MU', 'MA', 'MZ',
50
+ 'NA', 'NE', 'NG', 'RW', 'ST', 'SN', 'SC', 'SL', 'SO', 'ZA', 'SS', 'SD',
51
+ 'TZ', 'TG', 'TN', 'UG', 'ZM', 'ZW',
52
+ ],
53
+ // 大洋洲 (Oceania)
54
+ oc: [
55
+ 'AU', 'FJ', 'KI', 'MH', 'FM', 'NR', 'NZ', 'PW', 'PG', 'WS', 'SB', 'TO',
56
+ 'TV', 'VU',
57
+ ],
58
+ // 南极洲 (Antarctica) - 通常无常住人口
59
+ an: ['AQ'],
60
+ };
61
+
62
+ /**
63
+ * 国家代码到大洲的反向映射(运行时生成)
64
+ */
65
+ export const COUNTRY_TO_CONTINENT: Record<string, Continent> = {};
66
+
67
+ // 构建反向映射
68
+ for (const [continent, countries] of Object.entries(CONTINENT_COUNTRIES)) {
69
+ for (const country of countries) {
70
+ COUNTRY_TO_CONTINENT[country] = continent as Continent;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 根据国家代码获取大洲
76
+ *
77
+ * @param countryCode - ISO 3166-1 alpha-2 国家代码
78
+ * @param defaultContinent - 默认大洲(找不到时返回)
79
+ * @returns 大洲代码
80
+ */
81
+ export function getContinentByCountry(
82
+ countryCode: string,
83
+ defaultContinent: Continent = 'as',
84
+ ): Continent {
85
+ return COUNTRY_TO_CONTINENT[countryCode?.toUpperCase()] ?? defaultContinent;
86
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @fileoverview IP 地理位置服务导出入口(Infra 层)
3
+ *
4
+ * 本模块提供纯 infra 层的 IP 地理位置服务:
5
+ * - IpGeoService: IP 地理位置查询服务
6
+ * - IpGeoModule: NestJS 模块
7
+ * - getContinentByCountry: 静态大洲映射函数
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { IpGeoModule, IpGeoService } from '@app/shared-services/ip-geo';
12
+ *
13
+ * @Module({
14
+ * imports: [IpGeoModule],
15
+ * })
16
+ * export class MyModule {}
17
+ *
18
+ * @Injectable()
19
+ * class MyService {
20
+ * constructor(private readonly ipGeo: IpGeoService) {}
21
+ *
22
+ * async getRegion(ip: string) {
23
+ * return await this.ipGeo.getContinent(ip);
24
+ * }
25
+ * }
26
+ * ```
27
+ *
28
+ * @module ip-geo
29
+ */
30
+ export { IpGeoModule } from './ip-geo.module';
31
+ export { IpGeoService } from './ip-geo.service';
32
+ export {
33
+ getContinentByCountry,
34
+ Continent,
35
+ CONTINENT_COUNTRIES,
36
+ COUNTRY_TO_CONTINENT,
37
+ } from './continent-mapping';
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @fileoverview IP 地理位置服务模块(Infra 层)
3
+ *
4
+ * @module ip-geo/module
5
+ */
6
+ import { Module } from '@nestjs/common';
7
+ import { RedisModule } from '@app/redis';
8
+ import { IpInfoClientModule } from '@app/clients/internal/ip-info';
9
+ import { IpGeoService } from './ip-geo.service';
10
+
11
+ /**
12
+ * IP 地理位置服务模块
13
+ *
14
+ * @description 提供纯 infra 层的 IP 地理位置服务,不依赖 domain 层。
15
+ */
16
+ @Module({
17
+ imports: [RedisModule, IpInfoClientModule],
18
+ providers: [IpGeoService],
19
+ exports: [IpGeoService],
20
+ })
21
+ export class IpGeoModule {}
@@ -0,0 +1,135 @@
1
+ /**
2
+ * @fileoverview IP 地理位置服务(Infra 层)
3
+ *
4
+ * 本服务提供纯 infra 层的 IP 地理位置查询功能:
5
+ * - IP 信息查询(via IpInfoClient)
6
+ * - 国家代码查询
7
+ * - 大洲查询(使用静态映射,不依赖数据库)
8
+ *
9
+ * 注意:此服务不依赖 domain 层,可在 infra 层安全使用。
10
+ * 如需完整的 IP 信息服务(含数据库查询),请使用 domain/services/ip-info。
11
+ *
12
+ * @module ip-geo/service
13
+ */
14
+ import { Injectable, Inject } from '@nestjs/common';
15
+ import { WINSTON_MODULE_PROVIDER } from 'nest-winston';
16
+ import { Logger } from 'winston';
17
+ import { RedisService } from '@app/redis';
18
+ import { FastifyRequest } from 'fastify';
19
+ import ipUtil from '@/utils/ip.util';
20
+ import validateUtil from '@/utils/validate.util';
21
+ import { PardxApp } from '@/config/dto/config.dto';
22
+ import enviromentUtil from '@/utils/enviroment.util';
23
+ import { getContinentByCountry, Continent } from './continent-mapping';
24
+ import { IpInfoClient, IpInfoResponse } from '@app/clients/internal/ip-info';
25
+
26
+ /**
27
+ * IP 地理位置服务(Infra 层)
28
+ *
29
+ * @description 提供 IP 地理位置查询功能,使用静态大洲映射,不依赖数据库。
30
+ *
31
+ * @class IpGeoService
32
+ */
33
+ @Injectable()
34
+ export class IpGeoService {
35
+ protected ipinfoRedisKey = 'ipinfo';
36
+
37
+ constructor(
38
+ private readonly redis: RedisService,
39
+ private readonly ipInfoClient: IpInfoClient,
40
+ @Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger,
41
+ ) {}
42
+
43
+ /**
44
+ * 从请求中提取 IP 地址
45
+ */
46
+ extractIp(req: FastifyRequest): string {
47
+ return ipUtil.extractIp(req);
48
+ }
49
+
50
+ /**
51
+ * 获取 IP 信息
52
+ */
53
+ async getIpInfo(ip: string): Promise<Partial<PardxApp.IPInfo>> {
54
+ if (enviromentUtil.getBaseZone() === 'cn') {
55
+ return {
56
+ ip,
57
+ country: 'CN',
58
+ region: 'Beijing',
59
+ city: 'Beijing',
60
+ loc: '1.2897,103.8501',
61
+ timezone: 'Asia/Shanghai',
62
+ };
63
+ }
64
+
65
+ if (validateUtil.isBlank(ip) || ip === '127.0.0.1') {
66
+ return {
67
+ ip,
68
+ country: 'SG',
69
+ region: 'Singapore',
70
+ city: 'Singapore',
71
+ loc: '1.2897,103.8501',
72
+ timezone: 'Asia/Singapore',
73
+ };
74
+ }
75
+
76
+ const ipinfo = await this.redis.getData(this.ipinfoRedisKey, ip);
77
+ if (ipinfo) return ipinfo;
78
+
79
+ try {
80
+ const response: IpInfoResponse = await this.ipInfoClient.getIpInfo(ip);
81
+ let ipInfoData: PardxApp.IPInfo = response;
82
+ this.logger.info('IP info:', { ipInfoData });
83
+ await this.redis.saveData(this.ipinfoRedisKey, ip, ipInfoData);
84
+
85
+ if (ipInfoData.country === 'CN') {
86
+ ipInfoData = {
87
+ ip,
88
+ country: 'SG',
89
+ region: 'Singapore',
90
+ city: 'Singapore',
91
+ loc: '1.2897,103.8501',
92
+ timezone: 'Asia/Singapore',
93
+ };
94
+ }
95
+ return ipInfoData;
96
+ } catch (error) {
97
+ this.logger.error('Failed to fetch IP info:', error);
98
+ return {
99
+ ip,
100
+ country: 'SG',
101
+ region: 'Singapore',
102
+ city: 'Singapore',
103
+ loc: '1.2897,103.8501',
104
+ timezone: 'Asia/Singapore',
105
+ };
106
+ }
107
+ }
108
+
109
+ /**
110
+ * 获取 IP 对应的国家代码
111
+ */
112
+ async getIpCountry(ip: string): Promise<string> {
113
+ const ipInfo = await this.getIpInfo(ip);
114
+ return ipInfo.country;
115
+ }
116
+
117
+ /**
118
+ * 获取 IP 对应的大洲
119
+ *
120
+ * @description 使用静态映射,不依赖数据库
121
+ */
122
+ async getContinent(ip: string): Promise<Continent> {
123
+ const countryCode = await this.getIpCountry(ip);
124
+ const defaultZone = enviromentUtil.getBaseZone() as Continent;
125
+ return getContinentByCountry(countryCode, defaultZone);
126
+ }
127
+
128
+ /**
129
+ * 获取 IP 对应的时区
130
+ */
131
+ async getTimeZone(ip: string): Promise<string> {
132
+ const ipInfo = await this.getIpInfo(ip);
133
+ return ipInfo.timezone;
134
+ }
135
+ }
@@ -1,2 +1,2 @@
1
1
  export { UploaderModule } from './uploader.module';
2
- export { UploaderService } from './uploader.service';
2
+ export { UploaderService, TokenRequest, SignatureData } from './uploader.service';
@@ -16,7 +16,11 @@ import enviromentUtil from '@/utils/enviroment.util';
16
16
  import fileUtil from '@/utils/file.util';
17
17
  import { FileBucketVendor } from '@prisma/client';
18
18
 
19
- interface SignatureData {
19
+ /**
20
+ * 签名数据接口
21
+ * 从加密签名中解析出的数据结构
22
+ */
23
+ export interface SignatureData {
20
24
  timestamp?: number;
21
25
  filename?: string;
22
26
  fileId?: string;
@@ -25,7 +29,11 @@ interface SignatureData {
25
29
  sha256?: string;
26
30
  }
27
31
 
28
- interface TokenRequest {
32
+ /**
33
+ * 上传令牌请求接口
34
+ * 用于获取上传凭证的请求参数
35
+ */
36
+ export interface TokenRequest {
29
37
  signature?: string;
30
38
  filename?: string;
31
39
  vendor?: FileBucketVendor;
@@ -64,7 +72,7 @@ export class UploaderService {
64
72
  try {
65
73
  const jsonString = rsaDecrypt(cmd.signature);
66
74
  if (!jsonString || jsonString.trim() === '') {
67
- console.error(
75
+ this.logger.error(
68
76
  '[Signature Validation] Decryption failed: empty result',
69
77
  {
70
78
  signature: cmd.signature?.substring(0, 50) + '...',
@@ -75,7 +83,7 @@ export class UploaderService {
75
83
  }
76
84
  signatureData = JSON.parse(jsonString);
77
85
  } catch (e) {
78
- console.error('[Signature Validation] Decryption or parsing failed:', {
86
+ this.logger.error('[Signature Validation] Decryption or parsing failed:', {
79
87
  error: e.message || e,
80
88
  signature: cmd.signature?.substring(0, 50) + '...',
81
89
  userId,
@@ -86,7 +94,7 @@ export class UploaderService {
86
94
  ? undefined
87
95
  : signatureData.userId;
88
96
  if (userId != uploaderUserId) {
89
- console.error('[Signature Validation] UserId mismatch:', {
97
+ this.logger.error('[Signature Validation] UserId mismatch:', {
90
98
  expectedUserId: userId,
91
99
  signatureUserId: uploaderUserId,
92
100
  signatureData,
@@ -99,7 +107,7 @@ export class UploaderService {
99
107
  enviromentUtil.isProduction() &&
100
108
  now - signatureData.timestamp > 15 * 1000
101
109
  ) {
102
- console.error('[Signature Validation] Timestamp expired:', {
110
+ this.logger.error('[Signature Validation] Timestamp expired:', {
103
111
  now,
104
112
  signatureTimestamp: signatureData.timestamp,
105
113
  diff: now - signatureData.timestamp,
@@ -17,8 +17,8 @@ import { HttpExceptionFilter } from '@/common/filter/exception/http.exception';
17
17
  import { WinstonModule } from 'nest-winston';
18
18
  import * as loggerUtil from '@/utils/logger.util';
19
19
 
20
- /** health module */
21
- import { HealthModule } from './modules/health/health.module';
20
+ /** uploader module */
21
+ import { UploaderModule } from './modules/uploader/uploader.module';
22
22
 
23
23
  /** i18n */
24
24
  import {
@@ -158,16 +158,6 @@ import { DbMetricsService } from '@app/prisma/db-metrics/src/db-metrics.service'
158
158
  };
159
159
  },
160
160
  }),
161
- WinstonModule.forRootAsync({
162
- imports: [ConfigModule],
163
- useFactory: (configService: ConfigService) => {
164
- const output =
165
- configService.get<loggerUtil.LogOutputMode>('app.nestLogOutput') ||
166
- 'file';
167
- return loggerUtil.getWinstonConfig(output);
168
- },
169
- inject: [ConfigService],
170
- }),
171
161
  IpInfoServiceModule,
172
162
  ScheduleModule.forRoot(),
173
163
  RedisModule,
@@ -179,6 +169,7 @@ import { DbMetricsService } from '@app/prisma/db-metrics/src/db-metrics.service'
179
169
  VerifyModule,
180
170
  SystemHealthModule,
181
171
  JwtModule,
172
+ UploaderModule,
182
173
  ],
183
174
  providers: [
184
175
  {