feat(m8): P1 MinIO 对象存储与文件管理

- 迁移 000023 gateway.file_objects(personal/system 归属隔离 + 部分索引)
- internal/platform/storage:minio-go 适配(端点 scheme 剥离、流式 PutObject/Open/Delete)
- internal/workbench/files.go:FileService(sha256 校验、PutObject-then-insert 回滚、delete 先删行再删对象)
- admin /api/v1/admin/files + portal /api/v1/portal/files 处理器(流式上传下载、Content-Disposition)
- RBAC file:read/file:manage;菜单加文件管理 + 门户文件仓库
- compose 增 minio 服务(S3_* anchor、不暴露端口);nginx client_max_body_size 32m→256m
- 管理端文件管理页 + 门户个人文件仓;集成测试 TestFileObjectLifecycle 连真 MinIO 通过
- healthz object_storage:true;README/PRODUCTION/进展文档同步

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ben
2026-08-12 14:08:38 +08:00
parent 5759c1862e
commit 6708c226a5
26 changed files with 1180 additions and 40 deletions
+27
View File
@@ -1,4 +1,6 @@
import axios from 'axios'
import request from '@/utils/http'
import { useUserStore } from '@/store/modules/user'
export interface Application { id:string;code:string;name:string;description:string;published_version?:number }
export interface Knowledge { id:string;name:string;description:string;document_count:number;chunk_count:number }
@@ -21,6 +23,31 @@ export const fetchStats=(days:number)=>request.get<Stats>({url:'/api/v1/portal/s
export const fetchLogs=(days:number,limit=50)=>request.get<{items:AuditEvent[]}>({url:'/api/v1/portal/logs',params:{days,limit}})
export const changePassword=(params:{old_password:string;new_password:string})=>request.post({url:'/api/v1/portal/password',params})
// --- 个人文件仓库(M8 对象存储)---
export interface FileObject { id:string;original_name:string;content_type:string;size_bytes:number;content_sha256:string;scope:string;created_at:string;updated_at:string }
export const fetchMyFiles=()=>request.get<FileObject[]>({url:'/api/v1/portal/files'})
export const uploadMyFile=(file:File)=>{
const form=new FormData()
form.append('file',file)
return request.post<FileObject>({url:'/api/v1/portal/files',data:form,timeout:120000})
}
export const deleteMyFile=(id:string)=>request.del({url:`/api/v1/portal/files/${id}`})
export async function downloadMyFile(id:string,filename:string){
const { accessToken } = useUserStore()
const token = accessToken.startsWith('Bearer ')?accessToken:`Bearer ${accessToken}`
const res = await axios.get(`/api/v1/portal/files/${id}/download`,{
baseURL:import.meta.env.VITE_API_URL,
headers:{Authorization:token},
responseType:'blob'
})
const url = URL.createObjectURL(res.data)
const anchor = document.createElement('a')
anchor.href = url
anchor.download = filename
anchor.click()
URL.revokeObjectURL(url)
}
// --- 资源市场 ---
export interface MarketItem { type:string;code:string;name:string;description:string;category_id?:string;category_name:string;tags:string[];department_ids:string[];updated_at:string }