实现上传功能

This commit is contained in:
李志强 2025-10-28 17:22:27 +08:00
parent 1062bcfb70
commit 74df0e539c
15 changed files with 1126 additions and 24 deletions

172
EDITOR_UPLOAD_FIX.md Normal file
View File

@ -0,0 +1,172 @@
# 编辑器图片上传问题修复
## 问题描述
编辑器里上传的图片无法显示。
## 根本原因
文件保存路径和静态文件URL映射不匹配。
## 已完成的修改
### 1. 后端文件保存路径 (`server/controllers/file.go`)
**修改前:**
```go
uploadDir := path.Join("uploads", datePath)
```
文件保存到:`server/uploads/`
**修改后:**
```go
uploadDir := path.Join("..", "uploads", datePath)
```
文件保存到:`项目根目录/uploads/`
### 2. 静态文件映射 (`server/conf/app.conf`)
**修改前:**
```conf
StaticDir = /uploads:uploads
```
**修改后:**
```conf
StaticDir = /uploads:../uploads
```
现在 `/uploads` URL 正确映射到项目根目录的 `uploads` 文件夹。
### 3. 前端URL拼接 (`front/src/components/WangEditor.vue`)
添加了更健壮的URL拼接逻辑
```typescript
const fileUrl = response.data.data.file_url; // /uploads/2024/01/15/xxx.jpg
const baseUrl = getUploadUrl() ;
let fullUrl = fileUrl;
if (!fileUrl.startsWith('http')) {
const base = baseUrl.replace(/\/$/, '');
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
fullUrl = `${base}${url}`;
}
```
这样可以确保URL正确拼接为`http://localhost:8080/uploads/2024/01/15/xxx.jpg`
## 目录结构
上传后文件将保存在:
```
yunzer_go/
├── server/
├── front/
└── uploads/ ← 文件保存位置
├── 2024/
│ └── 01/
│ └── 15/
│ └── 20240115143045_example.jpg
```
## 访问URL
上传后可以通过以下URL访问
```
http://localhost:8080/uploads/2024/01/15/20240115143045_example.jpg
```
## 如何验证
### 1. 上传一个图片
在编辑器中使用图片上传功能上传一张图片。
### 2. 查看控制台日志
打开浏览器开发者工具F12查看 Console 标签,应该看到:
```
图片上传成功URL: http://localhost:8080/uploads/2024/01/15/xxx.jpg
```
### 3. 检查文件是否保存
查看项目根目录的 `uploads` 文件夹:
```bash
ls uploads/2024/01/15/
```
### 4. 测试URL访问
在浏览器中直接访问图片URL应该能看到图片。
### 5. 检查数据库
查看 `yz_files` 表中的记录:
```sql
SELECT file_path, file_url FROM yz_files ORDER BY upload_time DESC LIMIT 1;
```
应该看到:
- `file_path`: `uploads/2024/01/15/xxx.jpg`
- `file_url`: `/uploads/2024/01/15/xxx.jpg`
## 重启服务器
修改配置后需要重启服务器才能生效:
```bash
cd server
go run main.go
```
或者如果使用编译后的可执行文件:
```bash
./server.exe
```
## 如果还是看不到图片
### 检查清单
1. ✅ 服务器是否重启
2. ✅ 文件是否保存到 `uploads` 目录
3. ✅ 浏览器控制台是否有错误
4. ✅ URL是否正确拼接
5. ✅ 静态文件映射是否正确
### 常见问题
**Q: 上传后文件保存在哪里?**
A: 项目根目录的 `uploads` 文件夹
**Q: 图片URL是什么格式**
A: `http://localhost:8080/uploads/2024/01/15/filename.jpg`
**Q: 为什么看不到图片?**
A: 检查:
- 文件是否正确保存
- URL是否可访问
- 浏览器控制台错误信息
- 服务器是否重启
**Q: 如何自定义上传目录?**
A: 修改 `server/controllers/file.go``server/conf/app.conf` 中的路径配置
## 调试建议
如果图片仍不显示,检查以下内容:
1. **查看浏览器网络请求**
- F12 → Network 标签
- 查看图片请求是否返回 200
- 如果返回 404说明路径不对
2. **查看浏览器控制台**
- 查看是否有 CORS 错误
- 查看上传成功后的 URL 是什么
3. **查看服务器日志**
- 确认文件是否正确上传
- 确认路径是否正确
4. **检查文件权限**
- 确保应用有创建目录和写入文件的权限

View File

@ -18,7 +18,14 @@ export const fileAPI = {
},
// 上传文件
uploadFile: (formData: FormData) => {
uploadFile: (formData: FormData, options?: { category?: string; tenantId?: string }) => {
// 如果提供了额外参数,添加到 formData 中
if (options?.category) {
formData.append('category', options.category)
}
if (options?.tenantId) {
formData.append('tenant_id', options.tenantId)
}
return api.post('/api/files', formData, {
headers: {
'Content-Type': 'multipart/form-data'

View File

@ -0,0 +1,204 @@
# WangEditor 文件上传功能说明
## 功能概述
WangEditor 组件现已集成文件上传功能,支持:
- **图片上传**5MB 以内
- **视频上传**100MB 以内
- **附件上传**50MB 以内
所有文件都会上传到服务器并保存在 `front/uploads` 目录下,按照日期自动分类。
## 已实现的功能
### 1. 图片上传
- 通过工具栏的图片按钮上传
- 自动插入到编辑器中
- 文件保存在服务器并生成可访问的 URL
### 2. 视频上传
- 通过工具栏的视频按钮上传
- 自动插入视频播放器
- 支持常见的视频格式
### 3. 附件上传
- 支持所有文件类型
- 自动生成下载链接
- 文件名自动保留
## 技术实现
### 上传配置
```typescript
MENU_CONF: {
// 图片上传
uploadImage: {
server: '/api/files',
fieldName: 'file',
headers: getAuthHeaders(),
customUpload: async (file, insertFn) => {
await handleUploadImage(file, insertFn);
},
allowedFileTypes: ['image/*'],
maxFileSize: 5 * 1024 * 1024,
},
// 视频上传
uploadVideo: {
server: '/api/files',
fieldName: 'file',
headers: getAuthHeaders(),
customUpload: async (file, insertFn) => {
await handleUploadVideo(file, insertFn);
},
allowedFileTypes: ['video/*'],
maxFileSize: 100 * 1024 * 1024,
},
// 附件上传
uploadAttachment: {
server: '/api/files',
fieldName: 'file',
headers: getAuthHeaders(),
customUpload: async (file, insertFn) => {
await handleUploadAttachment(file, insertFn);
},
allowedFileTypes: ['*'],
maxFileSize: 50 * 1024 * 1024,
},
}
```
### 上传流程
1. **用户选择文件** → wangEditor 触发上传
2. **创建 FormData** → 包装文件数据
3. **调用 fileAPI** → 发送到后端 `/api/files`
4. **后端保存** → 文件保存到 `front/uploads/年/月/日/`
5. **返回 URL** → 后端返回文件访问 URL
6. **插入编辑器** → 将 URL 插入到编辑器内容中
### 后端响应格式
```json
{
"success": true,
"message": "文件上传成功",
"data": {
"id": 1,
"file_url": "/uploads/2024/01/15/20240115143045_example.jpg",
"file_path": "uploads/2024/01/15/20240115143045_example.jpg",
"file_name": "example",
"original_name": "example.jpg",
"file_size": 102400,
"file_type": "image",
"file_ext": ".jpg",
"category": "编辑器"
}
}
```
## 使用方式
### 基础使用
```vue
<template>
<WangEditor v-model="content" />
</template>
<script setup>
import { ref } from 'vue'
import WangEditor from '@/components/WangEditor.vue'
const content = ref('')
// 编辑器会自动支持上传功能
</script>
```
### 自定义配置
如果需要修改上传限制,可以编辑 `WangEditor.vue` 中的配置:
```typescript
// 修改文件大小限制
maxFileSize: 10 * 1024 * 1024, // 改为 10MB
// 修改允许的文件类型
allowedFileTypes: ['image/jpeg', 'image/png'],
// 修改分类
category: '自定义分类',
```
## 文件存储位置
所有上传的文件按日期分类存储在:
```
front/uploads/
├── 2024/
│ ├── 01/
│ │ ├── 15/
│ │ │ ├── 20240115143045_image.jpg
│ │ │ └── 20240115143046_video.mp4
```
## 访问已上传的文件
上传后的文件可以通过以下 URL 访问:
```
http://localhost:8080/uploads/2024/01/15/20240115143045_image.jpg
```
## 注意事项
1. **文件大小限制**
- 图片5MB
- 视频100MB
- 附件50MB
2. **文件类型验证**
- 图片:所有图片格式
- 视频:所有视频格式
- 附件:所有文件格式
3. **认证要求**
- 需要用户登录才能上传
- 自动携带 JWT Token
4. **错误处理**
- 上传失败会显示错误消息
- 成功后会显示成功提示
## 故障排除
### 上传失败
1. 检查网络连接
2. 确认用户已登录
3. 检查文件大小是否超限
4. 查看浏览器控制台错误信息
### 图片不显示
1. 确认文件已成功上传
2. 检查文件 URL 是否正确
3. 确认服务器已配置静态文件访问
4. 检查 CORS 配置
### 上传按钮不显示
1. 确认使用了正确的组件
2. 检查编辑器初始化是否成功
3. 查看浏览器控制台是否有错误
## 下一步改进建议
1. 添加上传进度条
2. 支持拖拽上传
3. 添加图片裁剪功能
4. 支持粘贴图片上传
5. 添加文件管理器功能

View File

@ -7,7 +7,9 @@
<script setup lang="ts">
import { ref, watch, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import '@wangeditor/editor/dist/css/style.css';
import { fileAPI } from '@/api/file';
interface Props {
modelValue: string;
@ -26,6 +28,119 @@ const editorRef = ref<HTMLDivElement>();
let editorInstance: any = null;
let isDestroyed = false;
// URL
const getUploadUrl = (): string => {
return import.meta.env.VITE_API_BASE_URL;
};
// Authorization Header
const getAuthHeaders = () => {
const token = localStorage.getItem('token');
return token ? { Authorization: `Bearer ${token}` } : {};
};
//
const handleUploadImage = async (file: File, insertFn: (url: string, alt?: string, href?: string) => void) => {
try {
const formData = new FormData();
formData.append('file', file);
const response: any = await fileAPI.uploadFile(formData, {
category: '编辑器',
});
// axios response.data使 response
if (response?.success) {
const fileUrl = response.data.file_url; // : /uploads/2024/01/15/xxx.jpg
const baseUrl = getUploadUrl() || window.location.origin;
// URLURL使baseUrl
let fullUrl = fileUrl;
if (!fileUrl.startsWith('http')) {
// URL/baseUrl/
const base = baseUrl.replace(/\/$/, '');
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
fullUrl = `${base}${url}`;
}
console.log('图片上传成功URL:', fullUrl);
insertFn(fullUrl, file.name, fullUrl);
ElMessage.success('图片上传成功');
} else {
ElMessage.error('上传失败:' + (response?.message || '未知错误'));
}
} catch (error: any) {
console.error('Upload error:', error);
ElMessage.error('上传失败:' + (error.message || '未知错误'));
}
};
//
const handleUploadVideo = async (file: File, insertFn: (url: string, poster?: string) => void) => {
try {
const formData = new FormData();
formData.append('file', file);
const response: any = await fileAPI.uploadFile(formData, {
category: '编辑器',
});
if (response?.success) {
const fileUrl = response.data.file_url;
const baseUrl = getUploadUrl() || window.location.origin;
let fullUrl = fileUrl;
if (!fileUrl.startsWith('http')) {
const base = baseUrl.replace(/\/$/, '');
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
fullUrl = `${base}${url}`;
}
console.log('视频上传成功URL:', fullUrl);
insertFn(fullUrl, '');
ElMessage.success('视频上传成功');
} else {
ElMessage.error('上传失败:' + (response?.message || '未知错误'));
}
} catch (error: any) {
console.error('Upload error:', error);
ElMessage.error('上传失败:' + (error.message || '未知错误'));
}
};
//
const handleUploadAttachment = async (file: File, insertFn: (url: string, text?: string) => void) => {
try {
const formData = new FormData();
formData.append('file', file);
const response: any = await fileAPI.uploadFile(formData, {
category: '编辑器',
});
if (response?.success) {
const fileUrl = response.data.file_url;
const baseUrl = getUploadUrl() || window.location.origin;
let fullUrl = fileUrl;
if (!fileUrl.startsWith('http')) {
const base = baseUrl.replace(/\/$/, '');
const url = fileUrl.startsWith('/') ? fileUrl : '/' + fileUrl;
fullUrl = `${base}${url}`;
}
console.log('附件上传成功URL:', fullUrl);
insertFn(fullUrl, file.name);
ElMessage.success('附件上传成功');
} else {
ElMessage.error('上传失败:' + (response?.message || '未知错误'));
}
} catch (error: any) {
console.error('Upload error:', error);
ElMessage.error('上传失败:' + (error.message || '未知错误'));
}
};
//
const initEditor = async () => {
if (!editorRef.value || !toolbarRef.value || isDestroyed) return;
@ -42,6 +157,42 @@ const initEditor = async () => {
emit('update:modelValue', html);
}
},
//
MENU_CONF: {
//
uploadImage: {
server: '/api/files',
fieldName: 'file',
headers: getAuthHeaders(),
customUpload: async (file: File, insertFn: (url: string, alt?: string, href?: string) => void) => {
await handleUploadImage(file, insertFn);
},
allowedFileTypes: ['image/*'],
maxFileSize: 5 * 1024 * 1024, // 5MB
},
//
uploadVideo: {
server: '/api/files',
fieldName: 'file',
headers: getAuthHeaders(),
customUpload: async (file: File, insertFn: (url: string, poster?: string) => void) => {
await handleUploadVideo(file, insertFn);
},
allowedFileTypes: ['video/*'],
maxFileSize: 100 * 1024 * 1024, // 100MB
},
//
uploadAttachment: {
server: '/api/files',
fieldName: 'file',
headers: getAuthHeaders(),
customUpload: async (file: File, insertFn: (url: string, text?: string) => void) => {
await handleUploadAttachment(file, insertFn);
},
allowedFileTypes: ['*'],
maxFileSize: 50 * 1024 * 1024, // 50MB
},
},
};
//

View File

@ -43,7 +43,7 @@
<span>{{ formData.updateTime }}</span>
</el-form-item>
</el-form>
<el-form label-width="80px" size="small" align="center">
<el-form label-width="80px" align="center">
<el-divider />
<el-button type="primary" @click="handleEdit"
><i class="fas fa-edit"></i> 编辑</el-button

View File

@ -160,11 +160,12 @@
<!-- 空状态 -->
<div class="empty-state" v-if="repoList.length === 0">
<div class="empty-icon">
<i class="el-icon-folder-opened"></i>
<i class="fa-solid fa-folder-open"></i>
</div>
<h3>暂无知识库</h3>
<p>点击下方按钮创建您的第一个知识库</p>
<el-button type="primary" icon="el-icon-plus" @click="handleCreate">
<el-button type="primary" @click="handleCreate">
<i class="fa-solid fa-plus"></i>
新建知识库
</el-button>
</div>

View File

@ -227,6 +227,7 @@
drag
:action="uploadUrl"
:headers="uploadHeaders"
:data="{ category: uploadForm.category }"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:before-upload="beforeUpload"
@ -360,8 +361,9 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import {
Download,
Delete,
@ -369,6 +371,7 @@ import {
Document,
View,
Link,
UploadFilled,
} from "@element-plus/icons-vue";
const router = useRouter();
@ -429,8 +432,20 @@ const pageSize = ref(10);
const currentPage = ref(1);
const totalFiles = ref(0);
const showUploadDialog = ref(false);
const uploadUrl = "";
const uploadHeaders = {};
//
const uploadUrl = computed(() => {
const baseUrl = import.meta.env.VITE_API_BASE_URL ;
return `${baseUrl}/api/files`;
});
const uploadHeaders = computed(() => {
const token = localStorage.getItem('token');
return {
'Authorization': `Bearer ${token}`
};
});
const uploadForm = ref({
category: "",
isPublic: false,
@ -442,11 +457,37 @@ const handleSearch = () => {};
const handleFilter = () => {};
const handleSizeChange = () => {};
const handleCurrentChange = () => {};
const handleUploadClose = () => {};
const handleUploadSuccess = () => {};
const handleUploadError = () => {};
const beforeUpload = () => {};
const submitUpload = () => {};
const handleUploadClose = () => {
showUploadDialog.value = false;
uploadForm.value.category = "";
uploadForm.value.isPublic = false;
};
const handleUploadSuccess = (response: any, file: any) => {
ElMessage.success('文件上传成功!');
//
showUploadDialog.value = false;
//
// loadFiles();
};
const handleUploadError = (error: Error, file: any) => {
ElMessage.error('文件上传失败:' + error.message);
};
const beforeUpload = (file: File) => {
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
ElMessage.error('文件大小不能超过 10MB');
return false;
}
return true;
};
const submitUpload = () => {
// Element Plus el-upload
ElMessage.info('开始上传文件...');
};
const viewFile = (row: any) => {};
const formatDate = (val: string | number) => val;
const copyFileUrl = (file: any) => {};

View File

@ -155,7 +155,7 @@ const uploadForm = ref({
//
const uploadUrl = computed(() => {
const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
const baseUrl = import.meta.env.VITE_API_BASE_URL
return `${baseUrl}/api/files`
})

View File

@ -15,5 +15,6 @@ mysqldb = gotest
# ORM配置
orm = mysql
# 配置静态文件目录(指向前端 dist 目录)
# 配置静态文件目录
StaticDir = /static:../front/dist # 映射 /static 路径到前端 dist 目录
StaticDir = /uploads:../uploads # 映射 /uploads 路径到上传文件目录(项目根目录下的 uploads 文件夹)

View File

@ -2,8 +2,14 @@ package controllers
import (
"encoding/json"
"strconv"
"os"
"path"
"path/filepath"
"server/models"
"strconv"
"strings"
"time"
beego "github.com/beego/beego/v2/server/web"
)
@ -404,7 +410,7 @@ func (c *FileController) GetMyFiles() {
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "获取成功",
"data": files,
"data": files,
}
}
c.ServeJSON()
@ -430,3 +436,147 @@ func (c *FileController) GetFilesPublic() {
c.ServeJSON()
}
// Post 处理文件上传
func (c *FileController) Post() {
// 从JWT中间件获取用户信息
userID, ok := c.Ctx.Input.GetData("userId").(int)
if !ok || userID <= 0 {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "用户未登录或登录已过期",
}
c.ServeJSON()
return
}
username, _ := c.Ctx.Input.GetData("username").(string)
if username == "" {
username = "unknown"
}
// 获取上传的文件
file, header, err := c.GetFile("file")
if err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "获取上传文件失败: " + err.Error(),
}
c.ServeJSON()
return
}
defer file.Close()
// 获取文件基本信息
originalName := header.Filename
fileSize := header.Size
fileExt := strings.ToLower(filepath.Ext(originalName))
fileName := strings.TrimSuffix(originalName, fileExt)
// 获取分类(可选)
category := c.GetString("category")
if category == "" {
category = "未分类"
}
// 获取租户ID可选从请求参数或中间件获取
tenantID := c.GetString("tenant_id")
if tenantID == "" {
tenantID = "default"
}
// 生成日期路径(年/月/日)
now := time.Now()
datePath := now.Format("2006/01/02")
// 目录改成 server 文件夹目录下的 uploads
uploadDir := filepath.Join("uploads", datePath)
// 清理路径
uploadDir = filepath.Clean(uploadDir)
// 确保目录存在
if err := os.MkdirAll(uploadDir, 0755); err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "创建上传目录失败: " + err.Error(),
}
c.ServeJSON()
return
}
// 生成唯一文件名(时间戳 + 原始文件名)
timestamp := now.Format("20060102150405")
uniqueFileName := timestamp + "_" + originalName
savePath := path.Join(uploadDir, uniqueFileName)
// 计算相对路径(用于存储到数据库)
relativePath := path.Join("uploads", datePath, uniqueFileName)
// 保存文件
if err := c.SaveToFile("file", savePath); err != nil {
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "保存文件失败: " + err.Error(),
}
c.ServeJSON()
return
}
// 获取文件类型
fileType := "other"
switch fileExt {
case ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp":
fileType = "image"
case ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".txt":
fileType = "document"
case ".mp4", ".avi", ".mov", ".wmv":
fileType = "video"
case ".mp3", ".wav", ".flac":
fileType = "audio"
case ".zip", ".rar", ".7z":
fileType = "archive"
}
// 构造文件URL相对路径
fileURL := "/" + relativePath
// 创建文件信息记录
fileInfo := models.FileInfo{
TenantID: tenantID,
UserID: userID,
FileName: fileName,
OriginalName: originalName,
FilePath: relativePath,
FileURL: fileURL,
FileSize: fileSize,
FileType: fileType,
FileExt: fileExt,
Category: category,
UploadBy: username,
UploadTime: now,
}
// 保存到数据库
id, err := models.AddFile(&fileInfo)
if err != nil {
// 如果数据库保存失败,删除已上传的文件
os.Remove(savePath)
c.Data["json"] = map[string]interface{}{
"success": false,
"message": "保存文件信息失败: " + err.Error(),
}
c.ServeJSON()
return
}
fileInfo.ID = id
// 返回成功响应
c.Data["json"] = map[string]interface{}{
"success": true,
"message": "文件上传成功",
"data": fileInfo,
}
c.ServeJSON()
}

203
server/docs/FILE_UPLOAD.md Normal file
View File

@ -0,0 +1,203 @@
# 文件上传功能文档
## 概述
文件上传功能已完整实现,支持将文件保存到本地文件系统并记录到数据库中。
## 功能特性
1. **自动目录管理**:按年月日自动创建目录结构(如 `front/uploads/2024/01/15/`
2. **唯一文件名**:使用时间戳生成唯一文件名,避免重名冲突
3. **文件类型识别**:自动识别文件类型(图片、文档、视频、音频、压缩包等)
4. **数据库记录**:所有文件信息保存到 `yz_files`
5. **用户关联**:自动关联当前登录用户
6. **异常处理**:文件保存失败时自动清理已上传的文件
## 文件结构
```
front/
└── uploads/
├── 2024/
│ ├── 01/
│ │ ├── 15/
│ │ │ ├── 20240115143045_example.jpg
│ │ │ └── 20240115143046_document.pdf
```
## API 接口
### 上传文件
**POST** `/api/files`
**请求头**:
```
Authorization: Bearer <token>
Content-Type: multipart/form-data
```
**请求参数**:
- `file` (File, required): 上传的文件
- `category` (String, optional): 文件分类,默认为"未分类"
- `tenant_id` (String, optional): 租户ID默认为"default"
**响应示例**:
```json
{
"success": true,
"message": "文件上传成功",
"data": {
"id": 1,
"tenant_id": "default",
"user_id": 123,
"file_name": "example",
"original_name": "example.jpg",
"file_path": "uploads/2024/01/15/20240115143045_example.jpg",
"file_url": "/uploads/2024/01/15/20240115143045_example.jpg",
"file_size": 102400,
"file_type": "image",
"file_ext": ".jpg",
"category": "未分类",
"upload_by": "username",
"upload_time": "2024-01-15T14:30:45Z"
}
}
```
## 后端实现
### 路由配置
`server/routers/router.go` 中配置:
```go
// 文件管理路由
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
beego.Router("/api/files/:id", &controllers.FileController{}, "get:GetFileById")
beego.Router("/api/files/:id", &controllers.FileController{}, "put:UpdateFile")
beego.Router("/api/files/:id", &controllers.FileController{}, "delete:DeleteFile")
```
### 控制器实现
`Post()` 方法位于 `server/controllers/file.go`:
1. 验证用户登录状态
2. 接收上传的文件
3. 生成日期路径和唯一文件名
4. 保存文件到本地
5. 记录文件信息到数据库
6. 返回文件信息
## 前端使用
### 基本用法
```typescript
import { fileAPI } from '@/api/file'
// 创建 FormData
const formData = new FormData()
formData.append('file', fileObject) // fileObject 是 File 对象
formData.append('category', '文档')
formData.append('tenant_id', 'tenant-001')
// 上传文件
try {
const response = await fileAPI.uploadFile(formData, {
category: '文档',
tenantId: 'tenant-001'
})
console.log('上传成功:', response.data)
} catch (error) {
console.error('上传失败:', error)
}
```
### Element Plus 上传组件
```vue
<el-upload
ref="uploadRef"
drag
:action="uploadUrl"
:headers="uploadHeaders"
:data="{ category: uploadForm.category }"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
:before-upload="beforeUpload"
multiple
>
<el-icon><upload-filled /></el-icon>
<div class="el-upload__text">
将文件拖到此处,或<em>点击上传</em>
</div>
</el-upload>
<script setup>
const uploadUrl = computed(() => {
const baseUrl = import.meta.env.VITE_API_BASE_URL
return `${baseUrl}/api/files`
})
const uploadHeaders = computed(() => {
const token = localStorage.getItem('token')
return {
'Authorization': `Bearer ${token}`
}
})
const handleUploadSuccess = (response) => {
console.log('上传成功:', response)
}
const beforeUpload = (file) => {
const maxSize = 10 * 1024 * 1024 // 10MB
if (file.size > maxSize) {
ElMessage.error('文件大小不能超过 10MB')
return false
}
return true
}
</script>
```
## 文件访问
上传后的文件可以通过以下URL访问
```
http://localhost:8080/uploads/2024/01/15/20240115143045_example.jpg
```
注意:需要配置 Beego 的静态文件服务来提供上传文件的访问。
`server/conf/app.conf` 中添加:
```conf
# 文件上传目录
StaticDir = /uploads:../front/uploads
```
## 安全注意事项
1. **文件大小限制**:建议在前端和后端都添加文件大小限制
2. **文件类型验证**:根据业务需求限制允许上传的文件类型
3. **文件名安全**:避免用户控制文件名造成安全问题
4. **权限控制**:确保只有授权用户可以上传文件
5. **存储位置**:考虑使用对象存储服务(如 OSS、S3替代本地存储
## 数据库字段说明
`yz_files` 表的主要字段:
- `file_path`: 相对路径,用于存储和访问文件
- `file_url`: 访问URL
- `original_name`: 用户上传时的原始文件名
- `file_name`: 去除扩展名的文件名
- `file_type`: 文件类型image, document, video, audio, archive, other
- `category`: 用户自定义分类

164
server/docs/UPLOAD_PATH.md Normal file
View File

@ -0,0 +1,164 @@
# 文件上传路径说明
## 当前配置
### 文件保存路径
文件保存在项目根目录的 `front/uploads/` 目录下,按照日期自动分类:
```
项目根目录/
├── server/
│ └── controllers/
│ └── file.go (处理上传)
├── front/
│ └── uploads/ ← 文件保存位置
│ ├── 2024/
│ │ └── 01/
│ │ └── 15/
│ │ └── 20240115143045_example.jpg
```
### 代码中的路径
`server/controllers/file.go``Post()` 方法中:
```go
// 构造保存路径:../front/uploads/年/月/日/
uploadDir := path.Join("..", "front", "uploads", datePath)
```
**说明**
- `..` 表示从 server 目录向上一级到项目根目录
- `front/uploads/` 是上传文件的根目录
- `datePath` 是按日期自动生成的子目录(如 `2024/01/15`
### 静态文件访问配置
`server/conf/app.conf` 中:
```conf
StaticDir = /uploads:../front/uploads
```
**说明**
- `/uploads` 是 URL 访问路径
- `../front/uploads` 是实际文件存储路径(相对于 server 目录)
## 目录结构
### 保存到数据库的路径
- `file_path`: `uploads/2024/01/15/20240115143045_example.jpg`(相对路径)
- `file_url`: `/uploads/2024/01/15/20240115143045_example.jpg`URL 路径)
### 实际文件系统路径
```
front/uploads/2024/01/15/20240115143045_example.jpg
```
### 访问 URL
```
http://localhost:8080/uploads/2024/01/15/20240115143045_example.jpg
```
## 为什么使用相对路径 `..`
由于项目结构是:
```
yunzer_go/
├── server/ ← 服务端代码
└── front/ ← 前端代码和上传文件
└── uploads/
```
`server` 目录运行应用时,要访问 `front/uploads`,需要使用 `../front/uploads`
## 如果路径不对怎么办?
### 方案1修改代码中的路径
如果您的项目启动目录不同,可以修改 `file.go` 中的路径:
```go
// 如果从项目根目录运行
uploadDir := path.Join("front", "uploads", datePath)
// 或者使用绝对路径
uploadDir := path.Join("/path/to/project", "front", "uploads", datePath)
```
### 方案2从配置文件读取
`app.conf` 中添加配置:
```conf
# 上传文件目录
uploadDir = ../front/uploads
```
然后在代码中读取:
```go
import "github.com/beego/beego/v2/server/web"
uploadDir := path.Join(
web.AppConfig.String("uploadDir"),
datePath,
)
```
## 验证路径是否正确
### 1. 检查文件保存位置
上传一个文件后,查看文件是否在正确的位置:
```bash
ls front/uploads/
```
应该看到按日期分类的文件夹和文件。
### 2. 检查数据库记录
查看 `yz_files` 表中的 `file_path` 字段:
```sql
SELECT file_path, file_url FROM yz_files ORDER BY upload_time DESC LIMIT 1;
```
应该看到类似:
```
file_path: uploads/2024/01/15/20240115143045_example.jpg
file_url: /uploads/2024/01/15/20240115143045_example.jpg
```
### 3. 检查 URL 访问
直接在浏览器访问:
```
http://localhost:8080/uploads/2024/01/15/文件名
```
如果能看到文件,说明路径配置正确。
## 常见问题
### Q: 文件保存在了 server/front/uploads
A: 修改代码中的路径为 `../front/uploads`(已经修改)
### Q: 文件保存在了 front/front/uploads
A: 检查当前工作目录,确保在 server 目录运行应用
### Q: 访问文件返回 404
A: 检查 `app.conf` 中的 StaticDir 配置是否正确
### Q: 权限问题?
A: 确保应用有创建目录和写入文件的权限:
```bash
chmod 755 front/uploads
```
## 建议的改进
如果需要更可靠的路径处理,可以考虑:
1. **使用绝对路径**:从配置文件或环境变量读取项目根目录
2. **路径验证**:在应用启动时检查上传目录是否存在,不存在则创建
3. **日志记录**:记录文件保存的完整路径,便于调试

View File

@ -79,8 +79,15 @@ func init() {
// 程序信息路由 - 自动映射到 /api/programinfo/*
beego.AutoRouter(&controllers.ProgramInfoController{})
// 文件管理路由 - 自动映射到 /api/file/*
beego.AutoRouter(&controllers.FileController{})
// 文件管理路由 - 手动配置以匹配前端的 /api/files 路径
beego.Router("/api/files", &controllers.FileController{}, "get:GetAllFiles")
beego.Router("/api/files", &controllers.FileController{}, "post:Post")
beego.Router("/api/files/my", &controllers.FileController{}, "get:GetMyFiles")
beego.Router("/api/files/:id", &controllers.FileController{}, "get:GetFileById")
beego.Router("/api/files/:id", &controllers.FileController{}, "put:UpdateFile")
beego.Router("/api/files/:id", &controllers.FileController{}, "delete:DeleteFile")
beego.Router("/api/files/search", &controllers.FileController{}, "get:SearchFiles")
beego.Router("/api/files/statistics", &controllers.FileController{}, "get:GetFileStatistics")
// 知识库路由
beego.Router("/api/knowledge/list", &controllers.KnowledgeController{}, "get:List")

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -0,0 +1 @@