浏览代码

✨ feat(disability): 新增照片详情和列表查询接口

- 新增获取单张照片详情的 API 端点 `/photos/:id`,返回包含文件 URL 等元数据的完整信息
- 新增获取残疾人所有照片的 API 端点 `/persons/:personId/photos`,返回包含文件 URL 的列表
- 在服务层新增 `getPhotoWithFile` 和 `getPersonPhotosWithFiles` 方法,通过 `relations` 加载关联的 File 实体
- 移除实体中冗余的计算属性 `photoUrl`,改为通过关联实体动态获取文件 URL
- 更新文档,补充性能优化策略(关联查询、分页)和错误处理说明
yourname 1 周之前
父节点
当前提交
c1e1182d9d
共有 1 个文件被更改,包括 66 次插入8 次删除
  1. 66 8
      docs/epic-007.md

+ 66 - 8
docs/epic-007.md

@@ -530,11 +530,6 @@ export type CreateChannelDto = z.infer<typeof CreateChannelSchema>;
      @CreateDateColumn({ name: 'upload_time' })
      uploadTime!: Date;
 
-     // 计算属性:获取文件URL
-     get photoUrl(): Promise<string> {
-       return this.file?.fullUrl || Promise.resolve('');
-     }
-
      // 关联残疾人实体
      @ManyToOne(() => DisabledPerson, person => person.photos)
      @JoinColumn({ name: 'person_id' })
@@ -559,6 +554,41 @@ export type CreateChannelDto = z.infer<typeof CreateChannelSchema>;
      const photo = await disabilityService.createPhoto(data);
      return c.json(photo, 201);
    });
+
+   // 获取照片详情(包含文件URL)
+   channelRoutes.get('/photos/:id', async (c) => {
+     const { id } = c.req.param();
+     const photo = await disabilityService.getPhotoWithFile(Number(id));
+
+     // 构建响应数据,包含文件URL
+     const response = {
+       ...photo,
+       photoUrl: await photo.file.fullUrl, // 通过关联实体获取文件URL
+       fileName: photo.file.name,
+       fileSize: photo.file.size,
+       fileType: photo.file.type
+     };
+
+     return c.json(response);
+   });
+
+   // 获取残疾人的所有照片(包含文件信息)
+   channelRoutes.get('/persons/:personId/photos', async (c) => {
+     const { personId } = c.req.param();
+     const photos = await disabilityService.getPersonPhotosWithFiles(Number(personId));
+
+     // 为每张照片添加文件URL
+     const photosWithUrls = await Promise.all(
+       photos.map(async (photo) => ({
+         ...photo,
+         photoUrl: await photo.file.fullUrl,
+         fileName: photo.file.name,
+         fileSize: photo.file.size
+       }))
+     );
+
+     return c.json(photosWithUrls);
+   });
    ```
 
 4. **服务层实现**:
@@ -585,7 +615,8 @@ export type CreateChannelDto = z.infer<typeof CreateChannelSchema>;
        return this.photoRepository.save(photo);
      }
 
-     async getPhotoWithUrl(photoId: number): Promise<DisabledPhoto> {
+     async getPhotoWithFile(photoId: number): Promise<DisabledPhoto> {
+       // 查询照片时加载关联的File实体
        const photo = await this.photoRepository.findOne({
          where: { photoId },
          relations: ['file'] // 加载关联的File实体
@@ -597,6 +628,23 @@ export type CreateChannelDto = z.infer<typeof CreateChannelSchema>;
 
        return photo;
      }
+
+     async getPhotoUrl(photoId: number): Promise<string> {
+       const photo = await this.getPhotoWithFile(photoId);
+
+       // 通过关联的file实体访问fullUrl属性
+       // file.fullUrl是File实体中的Promise属性
+       return await photo.file.fullUrl;
+     }
+
+     async getPersonPhotosWithFiles(personId: number): Promise<DisabledPhoto[]> {
+       // 查询某个残疾人的所有照片,并加载关联的File实体
+       return this.photoRepository.find({
+         where: { personId },
+         relations: ['file'], // 加载关联的File实体
+         order: { uploadTime: 'DESC' }
+       });
+     }
    }
    ```
 
@@ -677,8 +725,18 @@ API层(disability-module):
 
 1. **数据迁移**:需要制定现有URL数据的迁移方案
 2. **兼容性**:保持API兼容,逐步迁移
-3. **性能**:注意N+1查询问题,合理使用数据加载策略
-4. **错误处理**:处理文件服务不可用的情况
+3. **性能优化**:
+   - **关联查询**:使用`relations: ['file']`加载关联实体,避免N+1查询
+   - **选择性加载**:根据需要选择加载的关联字段
+   - **分页查询**:大数据量时使用分页,避免一次性加载所有关联数据
+4. **查询模式**:
+   - 通过关联的`file`实体访问文件属性(`file.fullUrl`、`file.name`等)
+   - 不需要在业务实体中定义重复的文件URL属性
+   - 使用TypeORM的`relations`选项加载关联数据
+5. **错误处理**:
+   - 验证`fileId`对应的文件是否存在
+   - 处理文件服务不可用的情况
+   - 处理关联实体加载失败的情况
 
 ## 兼容性要求