Jelajahi Sumber

✨ feat(advertisements): add advertisement management feature

- add advertisement route in admin panel
- refactor advertisement entity to use file relationship instead of direct image URL
- update schema to replace imageUrl with imageFileId and imageFile relation
- modify create and update DTOs to use imageFileId instead of imageUrl

♻️ refactor(advertisements): optimize image storage relationship

- replace imageUrl field with imageFileId and establish ManyToOne relationship with File entity
- update database schema to support file association for advertisement images
yourname 7 bulan lalu
induk
melakukan
dcb26f05c9

+ 2 - 1
src/client/admin-shadcn/routes.tsx

@@ -13,6 +13,7 @@ import { WechatCouponStocksPage } from './pages/WechatCouponStocks';
 import { WechatCouponsPage } from './pages/WechatCoupons';
 import { RedemptionCodesPage } from './pages/RedemptionCodes';
 import { WechatPayConfigPage } from './pages/WechatPayConfig';
+import Advertisements from './pages/Advertisements';
 
 export const router = createBrowserRouter([
   {
@@ -74,12 +75,12 @@ export const router = createBrowserRouter([
         path: 'wechat-pay-config',
         element: <WechatPayConfigPage />,
         errorElement: <ErrorPage />
+      },
       {
         path: 'advertisements',
         element: <Advertisements />,
         errorElement: <ErrorPage />
       },
-      },
       {
         path: '*',
         element: <NotFoundPage />,

+ 8 - 3
src/server/modules/advertisements/advertisement.entity.ts

@@ -1,4 +1,5 @@
-import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from 'typeorm';
+import { File } from '@/server/modules/files/file.entity';
 
 @Entity('advertisements')
 export class Advertisement {
@@ -11,8 +12,12 @@ export class Advertisement {
   @Column({ name: 'link_url', type: 'text', comment: '链接地址' })
   linkUrl!: string;
 
-  @Column({ name: 'image_url', type: 'text', comment: '图片地址' })
-  imageUrl!: string;
+  @Column({ name: 'image_file_id', type: 'int', unsigned: true, nullable: true, comment: '图片文件ID' })
+  imageFileId!: number | null;
+
+  @ManyToOne(() => File, { nullable: true })
+  @JoinColumn({ name: 'image_file_id', referencedColumnName: 'id' })
+  imageFile!: File | null;
 
   @Column({ name: 'title', type: 'varchar', length: 255, comment: '广告标题' })
   title!: string;

+ 18 - 9
src/server/modules/advertisements/advertisement.schema.ts

@@ -14,9 +14,18 @@ export const AdvertisementSchema = z.object({
     description: '链接地址',
     example: 'https://example.com'
   }),
-  imageUrl: z.string().url().openapi({
-    description: '图片地址',
-    example: 'https://example.com/image.jpg'
+  imageFileId: z.number().int().positive().nullable().openapi({
+    description: '图片文件ID',
+    example: 1
+  }),
+  imageFile: z.object({
+    id: z.number().int().positive().openapi({ description: '文件ID' }),
+    name: z.string().max(255).openapi({ description: '文件名', example: 'banner.jpg' }),
+    fullUrl: z.string().openapi({ description: '文件完整URL', example: 'https://example.com/banner.jpg' }),
+    type: z.string().nullable().openapi({ description: '文件类型', example: 'image/jpeg' }),
+    size: z.number().nullable().openapi({ description: '文件大小(字节)', example: 102400 })
+  }).nullable().optional().openapi({
+    description: '图片文件信息'
   }),
   title: z.string().min(1).max(255).openapi({
     description: '广告标题',
@@ -74,9 +83,9 @@ export const CreateAdvertisementDto = z.object({
     description: '链接地址',
     example: 'https://example.com'
   }),
-  imageUrl: z.string().url().openapi({
-    description: '图片地址',
-    example: 'https://example.com/image.jpg'
+  imageFileId: z.coerce.number().int().positive().nullable().optional().openapi({
+    description: '图片文件ID',
+    example: 1
   }),
   title: z.string().min(1).max(255).openapi({
     description: '广告标题',
@@ -114,9 +123,9 @@ export const UpdateAdvertisementDto = z.object({
     description: '链接地址',
     example: 'https://example.com'
   }),
-  imageUrl: z.string().url().optional().openapi({
-    description: '图片地址',
-    example: 'https://example.com/image.jpg'
+  imageFileId: z.coerce.number().int().positive().nullable().optional().openapi({
+    description: '图片文件ID',
+    example: 1
   }),
   title: z.string().min(1).max(255).optional().openapi({
     description: '广告标题',