Преглед изворни кода

✨ feat(policy-news): 重构政策资讯文件管理系统

- 移除旧的images字段,采用files关联关系管理图片
- 添加relationFields配置支持关联字段处理
- 扩展通用CRUD服务以支持关联字段的创建和更新
- 在政策资讯模块中实现fileIds关联字段,支持多文件管理
- 优化relations配置,包含files.uploadUser关联信息
yourname пре 9 месеци
родитељ
комит
adae029515

+ 8 - 1
src/server/api/policy-news/index.ts

@@ -1,5 +1,6 @@
 import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
 import { PolicyNews, PolicyNewsSchema, CreatePolicyNewsDto, UpdatePolicyNewsDto } from '@/server/modules/silver-users/policy-news.entity';
+import { File } from '@/server/modules/files/file.entity';
 import { authMiddleware } from '@/server/middleware/auth.middleware';
 
 const policyNewsRoutes = createCrudRoutes({
@@ -10,10 +11,16 @@ const policyNewsRoutes = createCrudRoutes({
   listSchema: PolicyNewsSchema,
   searchFields: ['newsTitle', 'newsContent', 'summary', 'source', 'category'],
   middleware: [authMiddleware],
-  relations: ['files'],
+  relations: ['files.uploadUser'],
   userTracking: {
     createdByField: 'createdBy',
     updatedByField: 'updatedBy'
+  },
+  relationFields: {
+    fileIds: {
+      relationName: 'files',
+      targetEntity: File
+    }
   }
 });
 

+ 0 - 6
src/server/modules/silver-users/policy-news.entity.ts

@@ -19,9 +19,6 @@ export class PolicyNews {
   @Column({ name: 'view_count', type: 'int', unsigned: true, default: 0, comment: '阅读量' })
   viewCount!: number;
 
-  @Column({ name: 'images', type: 'text', nullable: true, comment: '图片URL,多个用逗号分隔' })
-  images!: string | null;
-
   @Column({ name: 'summary', type: 'text', nullable: true, comment: '资讯摘要' })
   summary!: string | null;
 
@@ -72,7 +69,6 @@ export const PolicyNewsSchema = z.object({
   newsContent: z.string().openapi({ description: '资讯内容', example: '国务院办公厅近日印发《关于积极应对人口老龄化的实施意见》...' }),
   publishTime: z.date().openapi({ description: '发布时间', example: '2024-01-15T08:00:00Z' }),
   viewCount: z.coerce.number().int().min(0).openapi({ description: '阅读量', example: 1234 }),
-  images: z.string().nullable().openapi({ description: '图片URL,多个用逗号分隔', example: 'https://example.com/news1.jpg,https://example.com/news2.jpg' }),
   summary: z.string().nullable().openapi({ description: '资讯摘要', example: '国务院办公厅近日印发《关于积极应对人口老龄化的实施意见》...' }),
   source: z.string().max(255).nullable().openapi({ description: '资讯来源', example: '中国政府网' }),
   category: z.string().max(100).nullable().openapi({ description: '资讯分类', example: '政策法规' }),
@@ -90,7 +86,6 @@ export const CreatePolicyNewsDto = z.object({
   newsTitle: z.string().max(255).openapi({ description: '资讯标题', example: '关于积极应对人口老龄化的实施意见' }),
   newsContent: z.string().openapi({ description: '资讯内容', example: '国务院办公厅近日印发《关于积极应对人口老龄化的实施意见》...' }),
   publishTime: z.coerce.date().openapi({ description: '发布时间', example: '2024-01-15' }),
-  images: z.string().nullable().optional().openapi({ description: '图片URL,多个用逗号分隔', example: 'https://example.com/news1.jpg' }),
   summary: z.string().nullable().optional().openapi({ description: '资讯摘要', example: '国务院办公厅近日印发《关于积极应对人口老龄化的实施意见》...' }),
   source: z.string().max(255).nullable().optional().openapi({ description: '资讯来源', example: '中国政府网' }),
   category: z.string().max(100).nullable().optional().openapi({ description: '资讯分类', example: '政策法规' }),
@@ -103,7 +98,6 @@ export const UpdatePolicyNewsDto = z.object({
   newsTitle: z.string().max(255).optional().openapi({ description: '资讯标题', example: '关于积极应对人口老龄化的实施意见' }),
   newsContent: z.string().optional().openapi({ description: '资讯内容', example: '国务院办公厅近日印发《关于积极应对人口老龄化的实施意见》...' }),
   publishTime: z.coerce.date().optional().openapi({ description: '发布时间', example: '2024-01-15' }),
-  images: z.string().nullable().optional().openapi({ description: '图片URL,多个用逗号分隔', example: 'https://example.com/news1.jpg' }),
   summary: z.string().nullable().optional().openapi({ description: '资讯摘要', example: '国务院办公厅近日印发《关于积极应对人口老龄化的实施意见》...' }),
   source: z.string().max(255).nullable().optional().openapi({ description: '资讯来源', example: '中国政府网' }),
   category: z.string().max(100).nullable().optional().openapi({ description: '资讯分类', example: '政策法规' }),

+ 2 - 2
src/server/utils/generic-crud.routes.ts

@@ -13,13 +13,13 @@ export function createCrudRoutes<
   GetSchema extends z.ZodSchema = z.ZodSchema,
   ListSchema extends z.ZodSchema = z.ZodSchema
 >(options: CrudOptions<T, CreateSchema, UpdateSchema, GetSchema, ListSchema>) {
-  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking } = options;
+  const { entity, createSchema, updateSchema, getSchema, listSchema, searchFields, relations, middleware = [], userTracking, relationFields } = options;
   
   // 创建CRUD服务实例
   // 抽象类不能直接实例化,需要创建具体实现类
   class ConcreteCrudService extends GenericCrudService<T> {
     constructor() {
-      super(AppDataSource, entity, { userTracking });
+      super(AppDataSource, entity, { userTracking, relationFields });
     }
   }
   const crudService = new ConcreteCrudService();

+ 79 - 2
src/server/utils/generic-crud.service.ts

@@ -1,19 +1,23 @@
-import { DataSource, Repository, ObjectLiteral, DeepPartial } from 'typeorm';
+import { DataSource, Repository, ObjectLiteral, DeepPartial, In } from 'typeorm';
 import { z } from '@hono/zod-openapi';
 
 export abstract class GenericCrudService<T extends ObjectLiteral> {
   protected repository: Repository<T>;
   private userTrackingOptions?: UserTrackingOptions;
 
+  protected relationFields?: RelationFieldOptions;
+
   constructor(
     protected dataSource: DataSource,
     protected entity: new () => T,
     options?: {
       userTracking?: UserTrackingOptions;
+      relationFields?: RelationFieldOptions;
     }
   ) {
     this.repository = this.dataSource.getRepository(entity);
     this.userTrackingOptions = options?.userTracking;
+    this.relationFields = options?.relationFields;
   }
 
   /**
@@ -154,13 +158,56 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
     }
   }
 
+  /**
+   * 创建实体
+   */
+  /**
+   * 处理关联字段
+   */
+  private async handleRelationFields(data: any, entity: T, isUpdate: boolean = false): Promise<void> {
+    if (!this.relationFields) return;
+
+    for (const [fieldName, config] of Object.entries(this.relationFields)) {
+      if (data[fieldName] !== undefined) {
+        const ids = data[fieldName];
+        const relationRepository = this.dataSource.getRepository(config.targetEntity);
+        
+        if (ids && Array.isArray(ids) && ids.length > 0) {
+          const relatedEntities = await relationRepository.findBy({ id: In(ids) });
+          (entity as any)[config.relationName] = relatedEntities;
+        } else {
+          (entity as any)[config.relationName] = [];
+        }
+        
+        // 清理原始数据中的关联字段
+        delete data[fieldName];
+      }
+    }
+  }
+
   /**
    * 创建实体
    */
   async create(data: DeepPartial<T>, userId?: string | number): Promise<T> {
     const entityData = { ...data };
     this.setUserFields(entityData, userId, true);
+    
+    // 分离关联字段数据
+    const relationData: any = {};
+    if (this.relationFields) {
+      for (const fieldName of Object.keys(this.relationFields)) {
+        if (fieldName in entityData) {
+          relationData[fieldName] = (entityData as any)[fieldName];
+          delete (entityData as any)[fieldName];
+        }
+      }
+    }
+
     const entity = this.repository.create(entityData as DeepPartial<T>);
+    
+    // 处理关联字段
+    await this.handleRelationFields(relationData, entity);
+    
     return this.repository.save(entity);
   }
 
@@ -170,8 +217,29 @@ export abstract class GenericCrudService<T extends ObjectLiteral> {
   async update(id: number, data: Partial<T>, userId?: string | number): Promise<T | null> {
     const updateData = { ...data };
     this.setUserFields(updateData, userId, false);
+    
+    // 分离关联字段数据
+    const relationData: any = {};
+    if (this.relationFields) {
+      for (const fieldName of Object.keys(this.relationFields)) {
+        if (fieldName in updateData) {
+          relationData[fieldName] = (updateData as any)[fieldName];
+          delete (updateData as any)[fieldName];
+        }
+      }
+    }
+
+    // 先更新基础字段
     await this.repository.update(id, updateData);
-    return this.getById(id);
+    
+    // 获取完整实体并处理关联字段
+    const entity = await this.getById(id);
+    if (!entity) return null;
+    
+    // 处理关联字段
+    await this.handleRelationFields(relationData, entity, true);
+    
+    return this.repository.save(entity);
   }
 
   /**
@@ -196,6 +264,14 @@ export interface UserTrackingOptions {
   userIdField?: string;
 }
 
+export interface RelationFieldOptions {
+  [fieldName: string]: {
+    relationName: string;
+    targetEntity: new () => any;
+    joinTableName?: string;
+  };
+}
+
 export type CrudOptions<
   T extends ObjectLiteral,
   CreateSchema extends z.ZodSchema = z.ZodSchema,
@@ -212,4 +288,5 @@ export type CrudOptions<
   relations?: string[];
   middleware?: any[];
   userTracking?: UserTrackingOptions;
+  relationFields?: RelationFieldOptions;
 };