Pārlūkot izejas kodu

✨ feat(file): 实现文件URL访问功能

- 添加fullUrl属性到File实体,自动生成完整文件访问链接
- 修改文件预览组件,使用fullUrl替代path显示图片
- 调整MinioService,在数据库初始化时设置bucket访问策略

✨ feat(api): 优化HomeIcons API交互

- 修复创建图标成功状态码判断(200→201)
- 修复删除图标成功状态码判断(200→204)
- 从antd App中获取message实例,优化消息提示

♻️ refactor(config): 统一文件存储配置

- 更新全局OSS_BASE_URL配置,使用MinIO环境变量
- 移除MinioService中重复的bucket策略设置代码
yourname 7 mēneši atpakaļ
vecāks
revīzija
bb576173a1

+ 1 - 1
src/client/admin/components/FileSelector.tsx

@@ -109,7 +109,7 @@ const FileSelector: React.FC<FileSelectorProps> = ({
               }}
             >
               <Image
-                src={file.path}
+                src={file.fullUrl}
                 alt={file.name}
                 style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4, marginRight: 12 }}
                 preview={false}

+ 2 - 2
src/client/admin/components/SelectedFilePreview.tsx

@@ -20,7 +20,7 @@ const SelectedFilePreview: React.FC<SelectedFilePreviewProps> = ({ fileId }) =>
     enabled: !!fileId
   });
 
-  const file = fileData?.data;
+  const file = fileData;
 
   if (!file) return null;
 
@@ -36,7 +36,7 @@ const SelectedFilePreview: React.FC<SelectedFilePreviewProps> = ({ fileId }) =>
       gap: '12px'
     }}>
       <Image
-        src={file.path}
+        src={file.fullUrl}
         alt={file.name}
         style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4 }}
       />

+ 5 - 5
src/client/admin/pages/HomeIcons.tsx

@@ -1,5 +1,5 @@
 import React, { useState } from 'react';
-import { Table, Card, Tabs, Button, Space, Tag, Switch, Popconfirm, message, Modal, Form, Input, Image } from 'antd';
+import { Table, Card, Tabs, Button, Space, Tag, Switch, Popconfirm, Modal, Form, Input, App } from 'antd';
 import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined, LinkOutlined } from '@ant-design/icons';
 import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
 import { homeIconClient } from '@/client/api';
@@ -17,6 +17,7 @@ type CreateHomeIconRequest = InferRequestType<typeof homeIconClient.$post>['json
 type UpdateHomeIconRequest = InferRequestType<typeof homeIconClient[":id"]["$put"]>['json'];
 
 const HomeIconsPage: React.FC = () => {
+  const { message } = App.useApp();
   const [activeTab, setActiveTab] = useState<'banner' | 'category'>('banner');
   const [modalOpen, setModalOpen] = useState(false);
   const [editingRecord, setEditingRecord] = useState<HomeIcon | null>(null);
@@ -43,7 +44,7 @@ const HomeIconsPage: React.FC = () => {
   const createMutation = useMutation({
     mutationFn: async (data: CreateHomeIconRequest) => {
       const response = await homeIconClient.$post({ json: data });
-      if (response.status !== 200) throw new Error('创建图标失败');
+      if (response.status !== 201) throw new Error('创建图标失败');
       return response.json();
     },
     onSuccess: () => {
@@ -84,8 +85,7 @@ const HomeIconsPage: React.FC = () => {
       const response = await homeIconClient[":id"].$delete({
         param: { id: id.toString() }
       });
-      if (response.status !== 200) throw new Error('删除图标失败');
-      return response.json();
+      if (response.status !== 204) throw new Error('删除图标失败');
     },
     onSuccess: () => {
       message.success('删除成功');
@@ -175,7 +175,7 @@ const HomeIconsPage: React.FC = () => {
       width: 80,
       render: (file: FileType) => (
         <img
-          src={file.path}
+          src={file.fullUrl}
           alt={file.name}
           style={{ width: 60, height: 60, objectFit: 'cover', borderRadius: 4 }}
         />

+ 3 - 0
src/server/api.ts

@@ -25,6 +25,7 @@ import { AppDataSource } from './data-source'
 import silverJobRoutes from './api/silver-jobs/index'
 import companyCertificationRoutes from './api/company-certification/index'
 import homeIconRoutes from './api/home-icons/index'
+import { MinioService } from './modules/files/minio.service'
 
 const api = new OpenAPIHono<AuthContext>()
 
@@ -41,6 +42,8 @@ api.use('/api/v1/*', async (c, next) => {
 api.use('/api/v1/*', async (c, next) => {
   if(!AppDataSource.isInitialized) {
     await AppDataSource.initialize();
+    const minioService = new MinioService();
+    await minioService.setPublicReadPolicy()
   }
   await next();
 })

+ 14 - 0
src/server/modules/files/file.entity.ts

@@ -1,6 +1,7 @@
 import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne, JoinColumn } from 'typeorm';
 import { z } from '@hono/zod-openapi';
 import { UserEntity, UserSchema } from '@/server/modules/users/user.entity';
+import process from 'node:process';
 
 @Entity('file')
 export class File {
@@ -19,6 +20,15 @@ export class File {
   @Column({ name: 'path', type: 'varchar', length: 512, comment: '文件存储路径' })
   path!: string;
 
+  // 获取完整的文件URL(包含MINIO_HOST前缀)
+  get fullUrl(): string {
+    const protocol = process.env.MINIO_USE_SSL !== 'false' ? 'https' : 'http';
+    const port = process.env.MINIO_PORT ? `:${process.env.MINIO_PORT}` : '';
+    const host = process.env.MINIO_HOST || 'localhost';
+    const bucketName = process.env.MINIO_BUCKET_NAME || 'd8dai';
+    return `${protocol}://${host}${port}/${bucketName}/${this.path}`;
+  }
+
   @Column({ name: 'description', type: 'text', nullable: true, comment: '文件描述' })
   description!: string | null;
 
@@ -68,6 +78,10 @@ export const FileSchema = z.object({
     description: '文件存储路径',
     example: '/uploads/documents/2023/project-plan.pdf'
   }),
+  fullUrl: z.string().url().openapi({
+    description: '完整文件访问URL',
+    example: 'https://minio.example.com/d8dai/uploads/documents/2023/project-plan.pdf'
+  }),
   description: z.string().nullable().openapi({
     description: '文件描述',
     example: '2023年度项目计划书'

+ 0 - 1
src/server/modules/files/minio.service.ts

@@ -52,7 +52,6 @@ export class MinioService {
       const exists = await this.client.bucketExists(bucketName);
       if (!exists) {
         await this.client.makeBucket(bucketName);
-        await this.setPublicReadPolicy(bucketName);
         logger.db(`Created new bucket: ${bucketName}`);
       }
       return true;

+ 1 - 1
src/server/renderer.tsx

@@ -5,7 +5,7 @@ import process from 'node:process'
 
 // 全局配置常量
 const GLOBAL_CONFIG: GlobalConfig = {
-  OSS_BASE_URL: process.env.OSS_BASE_URL || 'https://oss.d8d.fun',
+  OSS_BASE_URL: `https://${process.env.MINIO_HOST}/d8dai/` || 'https://oss.d8d.fun/',
   APP_NAME: process.env.APP_NAME || '多八多Aider',
 }