Browse Source

feat(media-config): refactor media configuration handling and introduce SysConfigReaderService

Dave 3 tháng trước cách đây
mục cha
commit
1608526bc6

+ 2 - 2
apps/box-app-api/src/app.module.ts

@@ -12,7 +12,7 @@ import { VideoModule } from './feature/video/video.module';
 import { AdModule } from './feature/ads/ad.module';
 import { HomepageModule } from './feature/homepage/homepage.module';
 import { SysParamsModule } from './feature/sys-params/sys-params.module';
-import { ImageConfigModule } from './feature/media-config/image-config.module';
+import { MediaConfigModule } from './feature/media-config/media-config.module';
 import { RedisModule } from '@box/db/redis/redis.module';
 import { RabbitmqModule } from './rabbitmq/rabbitmq.module';
 import { AuthModule } from './feature/auth/auth.module';
@@ -61,7 +61,7 @@ import path from 'path';
     // RecommendationModule,
     HomepageModule,
     SysParamsModule,
-    ImageConfigModule,
+    MediaConfigModule,
   ],
   providers: [
     {

+ 0 - 57
apps/box-app-api/src/feature/media-config/image-config.service.ts

@@ -1,57 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { PrismaMongoService } from '../../prisma/prisma-mongo.service';
-
-type ImageConfigDoc = {
-  _id: number;
-  imageConfig?: ImageConfig;
-};
-
-type ImageConfig = {
-  local?: {
-    rootPath?: string;
-    baseUrl?: string;
-  };
-  s3?: {
-    region?: string;
-    endpointUrl?: string;
-    accessKeyId?: string;
-    secretAccessKey?: string;
-    bucket?: string;
-  };
-};
-
-type MongoFindResult<T> = {
-  cursor: {
-    firstBatch: T[];
-  };
-};
-
-function isMongoFindResult<T>(value: unknown): value is MongoFindResult<T> {
-  if (typeof value !== 'object' || value === null) {
-    return false;
-  }
-
-  const anyValue = value as { cursor?: { firstBatch?: unknown } };
-  const firstBatch = anyValue.cursor?.firstBatch;
-  return Array.isArray(firstBatch);
-}
-
-@Injectable()
-export class ImageConfigService {
-  constructor(private readonly prisma: PrismaMongoService) {}
-
-  async getImageConfig(): Promise<ImageConfig> {
-    const raw: unknown = await this.prisma.$runCommandRaw({
-      find: 'sysConfig',
-      filter: { _id: -1 },
-      limit: 1,
-    });
-
-    if (!isMongoFindResult<ImageConfigDoc>(raw)) {
-      return {};
-    }
-
-    const doc = raw.cursor.firstBatch[0];
-    return doc?.imageConfig ?? {};
-  }
-}

+ 8 - 9
apps/box-app-api/src/feature/media-config/image-config.module.ts → apps/box-app-api/src/feature/media-config/media-config.module.ts

@@ -1,12 +1,12 @@
 import { Module } from '@nestjs/common';
-import { PrismaMongoModule } from '../../prisma/prisma-mongo.module';
-import { ImageConfigService } from './image-config.service';
 import { MediaManagerModule } from '@box/core/media-manager/media-manager.module';
+import { SysConfigModule } from '@box/core/sys-config/sys-config.module';
+import { SysConfigReaderService } from '@box/core/sys-config/sys-config-reader.service';
 
 const mediaManagerModule = MediaManagerModule.registerAsync({
-  imports: [PrismaMongoModule],
-  useFactory: async (imageConfigService: ImageConfigService) => {
-    const imageConfig = await imageConfigService.getImageConfig();
+  imports: [SysConfigModule],
+  useFactory: async (sysConfigReader: SysConfigReaderService) => {
+    const imageConfig = await sysConfigReader.getImageConfig();
     return {
       localRoot: imageConfig.local?.rootPath,
       aws: imageConfig.s3
@@ -20,12 +20,11 @@ const mediaManagerModule = MediaManagerModule.registerAsync({
         : undefined,
     };
   },
-  inject: [ImageConfigService],
+  inject: [SysConfigReaderService],
 });
 
 @Module({
-  imports: [PrismaMongoModule, mediaManagerModule],
-  providers: [ImageConfigService],
+  imports: [SysConfigModule, mediaManagerModule],
   exports: [MediaManagerModule],
 })
-export class ImageConfigModule {}
+export class MediaConfigModule {}

+ 2 - 1
apps/box-app-api/src/feature/sys-params/sys-params.controller.ts

@@ -1,3 +1,4 @@
+// apps/box-app-api/src/feature/sys-params/sys-params.controller.ts
 import { Controller, Get, Query } from '@nestjs/common';
 import { ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
 import { SysParamsService } from './sys-params.service';
@@ -42,6 +43,6 @@ export class SysParamsController {
   })
   async getSysCnf() {
     const data = await this.service.getSysCnf();
-    return { code: 1, msg: 'success', data };
+    return data;
   }
 }

+ 2 - 1
apps/box-app-api/src/feature/sys-params/sys-params.module.ts

@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
 import { SysParamsService } from './sys-params.service';
 import { SysParamsController } from './sys-params.controller';
 import { PrismaMongoModule } from '../../prisma/prisma-mongo.module';
+import { SysConfigModule } from '@box/core/sys-config/sys-config.module';
 
 @Module({
-  imports: [PrismaMongoModule],
+  imports: [PrismaMongoModule, SysConfigModule],
   controllers: [SysParamsController],
   providers: [SysParamsService],
   exports: [SysParamsService],

+ 6 - 33
apps/box-app-api/src/feature/sys-params/sys-params.service.ts

@@ -10,31 +10,14 @@ import {
   AdSlot,
   HOMEPAGE_CONSTANTS,
 } from '../homepage/homepage.constants';
-
-type SysConfigDoc = {
-  _id: number;
-  appConfig?: unknown;
-};
-
-type MongoFindResult<T> = {
-  cursor: {
-    firstBatch: T[];
-  };
-};
-
-function isMongoFindResult<T>(value: unknown): value is MongoFindResult<T> {
-  if (typeof value !== 'object' || value === null) {
-    return false;
-  }
-
-  const anyValue = value as { cursor?: { firstBatch?: unknown } };
-  const firstBatch = anyValue.cursor?.firstBatch;
-  return Array.isArray(firstBatch);
-}
+import { SysConfigReaderService } from '@box/core/sys-config/sys-config-reader.service';
 
 @Injectable()
 export class SysParamsService {
-  constructor(private readonly prisma: PrismaMongoService) {}
+  constructor(
+    private readonly prisma: PrismaMongoService,
+    private readonly sysConfigReader: SysConfigReaderService,
+  ) {}
 
   getHomepageConstants() {
     return {
@@ -103,16 +86,6 @@ export class SysParamsService {
   }
 
   async getSysCnf() {
-    const raw: unknown = await this.prisma.$runCommandRaw({
-      find: 'sysConfig',
-      filter: { _id: -1 },
-      limit: 1,
-    });
-
-    const doc = isMongoFindResult<SysConfigDoc>(raw)
-      ? raw.cursor.firstBatch[0]
-      : undefined;
-
-    return (doc?.appConfig ?? {}) as Record<string, any>;
+    return this.sysConfigReader.getAppConfig();
   }
 }

+ 5 - 3
apps/box-mgnt-api/src/mgnt-backend/feature/provider-video-sync/provider-video-sync.module.ts

@@ -1,11 +1,13 @@
-import { Module } from '@nestjs/common';
 import { HttpModule } from '@nestjs/axios';
-import { ProviderVideoSyncService } from './provider-video-sync.service';
+import { Module } from '@nestjs/common';
+
 import { ProviderVideoSyncController } from './provider-video-sync.controller';
+import { ProviderVideoSyncService } from './provider-video-sync.service';
 import { PrismaModule } from '@box/db/prisma/prisma.module';
+import { SysConfigModule } from '@box/core/sys-config/sys-config.module';
 
 @Module({
-  imports: [PrismaModule, HttpModule],
+  imports: [PrismaModule, HttpModule, SysConfigModule],
   providers: [ProviderVideoSyncService],
   controllers: [ProviderVideoSyncController],
   exports: [ProviderVideoSyncService],

+ 38 - 21
apps/box-mgnt-api/src/mgnt-backend/feature/provider-video-sync/provider-video-sync.service.ts

@@ -2,6 +2,7 @@
 import { Injectable, Logger } from '@nestjs/common';
 import { HttpService } from '@nestjs/axios';
 import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
+import { SysConfigReaderService } from '@box/core/sys-config/sys-config-reader.service';
 import { firstValueFrom } from 'rxjs';
 import { EntityType } from '@prisma/mongo/client';
 
@@ -136,10 +137,6 @@ export class ProviderVideoSyncService {
 
   private lastSyncSummary: ProviderVideoSyncResult | null = null;
 
-  private readonly PROVIDER_API_URL =
-    'https://vm.rvakc.xyz/api/web/mediafile/search';
-
-  private readonly DEFAULT_PROVIDER_CODE = 'RVAKC';
   private readonly MAX_PAGE_SIZE = 500;
   private readonly DEFAULT_PAGE_SIZE = 500;
   private readonly BATCH_SIZE = 100;
@@ -148,14 +145,23 @@ export class ProviderVideoSyncService {
   constructor(
     private readonly mongo: MongoPrismaService,
     private readonly httpService: HttpService,
+    private readonly sysConfigReader: SysConfigReaderService,
   ) {}
 
   async syncFromProvider(
     options: ProviderVideoSyncOptions = {},
   ): Promise<ProviderVideoSyncResult> {
-    const providerCode = options.providerCode ?? this.DEFAULT_PROVIDER_CODE;
+    const providerConfig = await this.sysConfigReader.getProviderConfig();
+    const providerCode = options.providerCode ?? providerConfig.providerCode;
+    const providerApiUrl = providerConfig.apiUrl;
+    if (!providerApiUrl) {
+      throw new Error(
+        'sysConfig.provider.apiUrl is required for provider sync',
+      );
+    }
 
-    const requestedPageSize = options.pageSize ?? this.DEFAULT_PAGE_SIZE;
+    const defaultPageSize = providerConfig.itemsLimit ?? this.DEFAULT_PAGE_SIZE;
+    const requestedPageSize = options.pageSize ?? defaultPageSize;
     const pageSize = this.clampInt(requestedPageSize, 1, this.MAX_PAGE_SIZE);
 
     const fullSync = options.fullSync ?? false;
@@ -215,6 +221,7 @@ export class ProviderVideoSyncService {
           cursor: initialCursor,
           paramStatus,
           optionsParam: options.param,
+          apiUrl: providerApiUrl,
         });
         if (baselineResult) {
           this.lastSyncSummary = baselineResult;
@@ -236,7 +243,7 @@ export class ProviderVideoSyncService {
           `[syncFromProvider] POST pageNum=${pageNum} pageSize=${cursor.pageSize} status=${paramStatus} updatedAt=${fullSync ? '(none)' : (body.param.updatedAt ?? '(none)')}`,
         );
 
-        const rawList = await this.fetchPage(body);
+        const rawList = await this.fetchPage(providerApiUrl, body);
         if (!rawList.length) {
           this.logger.log(
             `[syncFromProvider] No more records (pageNum=${pageNum}). Stop.`,
@@ -325,6 +332,7 @@ export class ProviderVideoSyncService {
     entity: EntityType;
     cursor: SyncCursor;
     paramStatus: string;
+    apiUrl: string;
     optionsParam?: ProviderVideoSyncOptions['param'];
   }): Promise<ProviderVideoSyncResult | null> {
     if (!args.cursor || args.cursor.updatedAtCursor !== undefined) {
@@ -339,7 +347,10 @@ export class ProviderVideoSyncService {
       extraParam: args.optionsParam,
     });
 
-    const pagination = await this.probeProviderForPaging(probeBody);
+    const pagination = await this.probeProviderForPaging(
+      args.apiUrl,
+      probeBody,
+    );
 
     let totalPages = pagination.totalPages;
     if (totalPages === undefined && pagination.total !== undefined) {
@@ -389,7 +400,7 @@ export class ProviderVideoSyncService {
 
       this.logger.log(`[syncFromProvider] param body ${JSON.stringify(body)} `);
 
-      const rawList = await this.fetchPage(body);
+      const rawList = await this.fetchPage(args.apiUrl, body);
       if (!rawList.length) {
         this.logger.log(
           `[syncFromProvider] Baseline partial page ${page} returned 0 records; continuing.`,
@@ -425,14 +436,17 @@ export class ProviderVideoSyncService {
     };
   }
 
-  private async probeProviderForPaging(body: {
-    pageNum: number;
-    pageSize: number;
-    param: Record<string, any>;
-  }): Promise<ProviderPagingInfo> {
+  private async probeProviderForPaging(
+    apiUrl: string,
+    body: {
+      pageNum: number;
+      pageSize: number;
+      param: Record<string, any>;
+    },
+  ): Promise<ProviderPagingInfo> {
     try {
       const response = await firstValueFrom(
-        this.httpService.post(this.PROVIDER_API_URL, body, {
+        this.httpService.post(apiUrl, body, {
           headers: { 'Content-Type': 'application/json' },
           timeout: 30000,
         }),
@@ -519,11 +533,14 @@ export class ProviderVideoSyncService {
       param,
     };
   }
-  private async fetchPage(body: {
-    pageNum: number;
-    pageSize: number;
-    param: Record<string, any>;
-  }): Promise<RawProviderVideo[]> {
+  private async fetchPage(
+    apiUrl: string,
+    body: {
+      pageNum: number;
+      pageSize: number;
+      param: Record<string, any>;
+    },
+  ): Promise<RawProviderVideo[]> {
     try {
       // Provider expects { data: "<json string>" } (based on code=400 Field=data expecting string)
       const wrappedBody = {
@@ -535,7 +552,7 @@ export class ProviderVideoSyncService {
       };
 
       const response = await firstValueFrom(
-        this.httpService.post(this.PROVIDER_API_URL, wrappedBody, {
+        this.httpService.post(apiUrl, wrappedBody, {
           headers: { 'Content-Type': 'application/json' },
           timeout: 30_000,
         }),

+ 2 - 1
apps/box-mgnt-api/src/mgnt-backend/feature/s3/s3.module.ts

@@ -1,9 +1,10 @@
 import { Module } from '@nestjs/common';
 import { S3Service } from './s3.service';
 import { S3Controller } from './s3.controller';
+import { SysConfigModule } from '@box/core/sys-config/sys-config.module';
 
 @Module({
-  imports: [],
+  imports: [SysConfigModule],
   providers: [S3Service],
   controllers: [S3Controller],
   exports: [S3Service],

+ 16 - 15
apps/box-mgnt-api/src/mgnt-backend/feature/s3/s3.service.ts

@@ -10,13 +10,17 @@ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
 import { Logger } from 'nestjs-pino';
 import { appendFile, mkdir } from 'fs/promises';
 import * as path from 'path';
+import { SysConfigReaderService } from '@box/core/sys-config/sys-config-reader.service';
 
 @Injectable()
 export class S3Service {
   private readonly s3: S3Client;
   private readonly bucket: string;
 
-  constructor(private readonly logger: Logger) {
+  constructor(
+    private readonly logger: Logger,
+    private readonly sysConfigReader: SysConfigReaderService,
+  ) {
     this.s3 = new S3Client({
       region: process.env.AWS_S3_REGION_NAME,
       credentials: {
@@ -62,7 +66,7 @@ export class S3Service {
     const ext = filename.split('.').pop();
     const key = `${folder}/${Date.now()}_${Math.random().toString(36).slice(2, 8)}.${ext}`;
 
-    const maxSize = this.resolveMaxSize(contentType);
+    const maxSize = await this.resolveMaxSize(contentType);
     this.logger.log(`maxSize for upload: ${maxSize} bytes`);
 
     const { url, fields } = await createPresignedPost(this.s3, {
@@ -81,34 +85,31 @@ export class S3Service {
     return { url, fields, key };
   }
 
-  private resolveMaxSize(contentType: string): number {
+  private async resolveMaxSize(contentType: string): Promise<number> {
     this.logger.log(`Resolving max size for content type: ${contentType}`);
     const BASE64_OVERHEAD_RATIO = 4 / 3; // ≈1.333
     const SAFETY_MARGIN = 1.05; // extra 5% for headers / future changes
 
-    const fromEnvMB = (env: string, fallbackMB: number) => {
-      const val = Number(process.env[env]);
-      return (isNaN(val) ? fallbackMB : val) * 1024 * 1024;
-    };
+    const imageConfig = await this.sysConfigReader.getImageConfig();
+    const limitsMb = imageConfig.limitsMb ?? {};
+    const imageLimitMb = limitsMb.image ?? 10;
+    const videoLimitMb = limitsMb.video ?? 100;
 
-    // ✅ your image payload is base64 text
     if (contentType === 'text/plain') {
-      const rawMb = Number(process.env.UPLOAD_LIMIT_IMAGE ?? 10);
-      const effectiveMb = rawMb * (4 / 3) * 1.05;
+      const effectiveMb = imageLimitMb * BASE64_OVERHEAD_RATIO * SAFETY_MARGIN;
       return Math.ceil(effectiveMb * 1024 * 1024);
     }
     if (contentType.startsWith('image/')) {
-      const rawMb = Number(process.env.UPLOAD_LIMIT_IMAGE ?? 10);
-      const effectiveMb = rawMb * BASE64_OVERHEAD_RATIO * SAFETY_MARGIN;
+      const effectiveMb = imageLimitMb * BASE64_OVERHEAD_RATIO * SAFETY_MARGIN;
       return Math.ceil(effectiveMb * 1024 * 1024);
     }
     if (contentType === 'video/mp4') {
-      return fromEnvMB('UPLOAD_LIMIT_VIDEO', 100);
+      return videoLimitMb * 1024 * 1024;
     }
     if (contentType === 'application/pdf') {
-      return fromEnvMB('UPLOAD_LIMIT_PDF', 10);
+      return 10 * 1024 * 1024;
     }
-    return fromEnvMB('UPLOAD_LIMIT_DEFAULT', 10);
+    return 10 * 1024 * 1024;
   }
 
   /**

+ 89 - 0
libs/core/src/sys-config/sys-config-reader.service.ts

@@ -0,0 +1,89 @@
+import { Injectable } from '@nestjs/common';
+import { MongoPrismaService } from '@box/db/prisma/mongo-prisma.service';
+
+type SysConfigDoc = {
+  _id: number;
+  appConfig?: Record<string, unknown>;
+  imageConfig?: ImageConfig;
+  provider?: ProviderConfig;
+};
+
+type ImageConfig = {
+  s3Enabled?: boolean;
+  storageStrategy?: string;
+  local?: {
+    rootPath?: string;
+    baseUrl?: string;
+  };
+  limitsMb?: {
+    image?: number;
+    video?: number;
+  };
+  s3?: {
+    accessKeyId?: string;
+    secretAccessKey?: string;
+    bucket?: string;
+    region?: string;
+    endpointUrl?: string;
+    imageBaseUrl?: string;
+  };
+};
+
+type ProviderConfig = {
+  providerCode?: string;
+  apiUrl?: string;
+  itemsLimit?: number;
+};
+
+type MongoFindResult<T> = {
+  cursor: {
+    firstBatch: T[];
+  };
+};
+
+function isMongoFindResult<T>(value: unknown): value is MongoFindResult<T> {
+  if (typeof value !== 'object' || value === null) {
+    return false;
+  }
+
+  const anyValue = value as { cursor?: { firstBatch?: unknown } };
+  return Array.isArray(anyValue.cursor?.firstBatch);
+}
+
+/**
+ * This is the only allowed access point for the `sysConfig` document.
+ * No other modules or services should read `sysConfig` directly.
+ */
+@Injectable()
+export class SysConfigReaderService {
+  constructor(private readonly prisma: MongoPrismaService) {}
+
+  async getAppConfig(): Promise<Record<string, unknown>> {
+    const doc = await this.fetchDoc();
+    return doc?.appConfig ?? {};
+  }
+
+  async getImageConfig(): Promise<ImageConfig> {
+    const doc = await this.fetchDoc();
+    return doc?.imageConfig ?? {};
+  }
+
+  async getProviderConfig(): Promise<ProviderConfig> {
+    const doc = await this.fetchDoc();
+    return doc?.provider ?? {};
+  }
+
+  private async fetchDoc(): Promise<SysConfigDoc | undefined> {
+    const raw: unknown = await this.prisma.$runCommandRaw({
+      find: 'sysConfig',
+      filter: { _id: -1 },
+      limit: 1,
+    });
+
+    if (!isMongoFindResult<SysConfigDoc>(raw)) {
+      return undefined;
+    }
+
+    return raw.cursor.firstBatch[0];
+  }
+}

+ 10 - 0
libs/core/src/sys-config/sys-config.module.ts

@@ -0,0 +1,10 @@
+import { Module } from '@nestjs/common';
+import { PrismaModule } from '@box/db/prisma/prisma.module';
+import { SysConfigReaderService } from './sys-config-reader.service';
+
+@Module({
+  imports: [PrismaModule],
+  providers: [SysConfigReaderService],
+  exports: [SysConfigReaderService],
+})
+export class SysConfigModule {}

+ 41 - 3
prisma/mongo/README.md

@@ -1,9 +1,47 @@
 # Mongo seeds
 
 ## sysConfig helper
-Run `pnpm prisma:seed:box-admin:sys-config` from the repo root to upsert the singleton Mongo document `{ _id: -1 }` with `appConfig.imageCdn`. That script uses `ts-node -P tsconfig.seed.json prisma/mongo/seed-sys-config.ts`.
 
-As an alternative you can do:
+`pnpm prisma:seed:box-admin:sys-config` upserts the singleton `{ _id: -1 }` document so that `appConfig.imageCdn`, `imageConfig`, and `provider` are populated for `box-app-api`. The ts-node script is `ts-node -P tsconfig.seed.json prisma/mongo/seed-sys-config.ts`.
+
+Generated document shape:
+
+```json
+{
+  "_id": -1,
+  "appConfig": {
+    "imageCdn": {
+      "s3": "...",
+      "local": "..."
+    }
+  },
+  "imageConfig": {
+    "s3Enabled": true,
+    "storageStrategy": "S3_AND_LOCAL",
+    "local": {
+      "rootPath": "something (e.g. /opt/app/node/ww-images)",
+      "baseUrl": "https://man.boxt3yk.com/images"
+    },
+    "limitsMb": { "image": 10, "video": 100 },
+    "s3": {
+      "accessKeyId": "...",
+      "secretAccessKey": "...",
+      "bucket": "...",
+      "region": "ap-east-1",
+      "endpointUrl": "https://s3.ap-east-1.amazonaws.com",
+      "imageBaseUrl": "https://s3.ap-east-1.amazonaws.com/mybucket-imgs"
+    }
+  },
+  "provider": {
+    "providerCode": "PARTNER",
+    "apiUrl": "https://wwapi.hxc1t.com",
+    "itemsLimit": 100
+  }
+}
+```
+
+Equivalent `mongosh` call:
+
 ```
-mongosh --eval "db.sysConfig.updateOne({ _id: -1 }, { $set: { appConfig: { imageCdn: { s3: 'https://s3.ap-east-1.amazonaws.com/mybucket-imgs', local: 'https://ww.xczox.xyz/images' } } } }, { upsert: true, multi: false })"
+mongosh --eval "db.sysConfig.updateOne({ _id: -1 }, { $set: { appConfig: { imageCdn: { s3: 'https://s3.ap-east-1.amazonaws.com/mybucket-imgs', local: 'https://man.boxt3yk.com/' } }, imageConfig: { s3Enabled: true, storageStrategy: 'S3_AND_LOCAL', local: { rootPath: '/opt/app/node/ww-images', baseUrl: 'https://man.boxt3yk.com/images' }, limitsMb: { image: 10, video: 100 }, s3: { accessKeyId: 'AKIA6GSNGR5PISMIKCJ4', secretAccessKey: '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', bucket: 'mybucket-imgs', region: 'ap-east-1', endpointUrl: 'https://s3.ap-east-1.amazonaws.com', imageBaseUrl: 'https://s3.ap-east-1.amazonaws.com/mybucket-imgs' } }, provider: { providerCode: 'PARTNER', apiUrl: 'https://wwapi.hxc1t.com', itemsLimit: 100 } } }, { upsert: true, multi: false })"
 ```

+ 2 - 2
prisma/mongo/seed-admin.ts

@@ -38,8 +38,8 @@ async function seedSysConfig(): Promise<void> {
               storageStrategy: 'LOCAL_ONLY' as ImgSource,
               local: {
                 rootPath: '/opt/app/node/ww-images',
-                baseUrl: 'https://mgnt.cqf.wang/images',
-                chatUpload: 'https://mgnt.cqf.wang/api/chat/upload',
+                baseUrl: 'https://man.boxt3yk.com/images',
+                chatUpload: 'https://man.boxt3yk.com/api/chat/upload',
               },
               limitsMb: { image: 10, video: 100 },
               s3: {

+ 27 - 1
prisma/mongo/seed-sys-config.ts

@@ -14,9 +14,35 @@ async function main() {
             appConfig: {
               imageCdn: {
                 s3: 'https://s3.ap-east-1.amazonaws.com/mybucket-imgs',
-                local: 'https://ww.xczox.xyz/images',
+                local: 'https://man.boxt3yk.com/',
               },
             },
+            imageConfig: {
+              s3Enabled: true,
+              storageStrategy: 'S3_AND_LOCAL',
+              local: {
+                rootPath: '/opt/app/node/ww-images',
+                baseUrl: 'https://man.boxt3yk.com/images',
+              },
+              limitsMb: {
+                image: 10,
+                video: 100,
+              },
+              s3: {
+                accessKeyId: 'AKIA6GSNGR5PISMIKCJ4',
+                secretAccessKey: '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',
+                bucket: 'mybucket-imgs',
+                region: 'ap-east-1',
+                endpointUrl: 'https://s3.ap-east-1.amazonaws.com',
+                imageBaseUrl:
+                  'https://s3.ap-east-1.amazonaws.com/mybucket-imgs',
+              },
+            },
+            provider: {
+              providerCode: 'PARTNER',
+              apiUrl: 'https://wwapi.hxc1t.com',
+              itemsLimit: 100,
+            },
           },
         },
         upsert: true,