first commit
This commit is contained in:
@@ -0,0 +1,295 @@
|
||||
# 七牛云上传功能文档索引
|
||||
|
||||
## 快速开始
|
||||
|
||||
如果你想快速测试七牛云上传功能,请按以下顺序阅读:
|
||||
|
||||
1. **[七牛云上传测试步骤](./七牛云上传测试步骤.md)** ⭐ 推荐先看
|
||||
- 测试前的准备工作
|
||||
- 详细的测试步骤
|
||||
- 常见问题排查
|
||||
|
||||
2. **[七牛云区域配置修复说明](./七牛云区域配置修复说明.md)**
|
||||
- 了解最近修复的区域配置问题
|
||||
- 查看修复的具体内容
|
||||
|
||||
3. **[七牛云直传配置](./七牛云直传配置.md)**
|
||||
- 完整的技术文档
|
||||
- API 接口说明
|
||||
- 代码使用示例
|
||||
|
||||
4. **[七牛云区域配置流程图](./七牛云区域配置流程图.md)**
|
||||
- 可视化流程图
|
||||
- 帮助理解上传机制
|
||||
|
||||
## 文档说明
|
||||
|
||||
### 七牛云上传测试步骤.md
|
||||
**适合人群**: 测试人员、开发人员
|
||||
|
||||
**内容**:
|
||||
- 测试前的准备工作
|
||||
- 详细的测试步骤
|
||||
- 验证方法
|
||||
- 常见问题排查
|
||||
- 性能测试指南
|
||||
|
||||
**何时阅读**: 准备测试上传功能时
|
||||
|
||||
---
|
||||
|
||||
### 七牛云区域配置修复说明.md
|
||||
**适合人群**: 开发人员、运维人员
|
||||
|
||||
**内容**:
|
||||
- 问题描述和原因分析
|
||||
- 解决方案详解
|
||||
- 修复的文件列表
|
||||
- 验证步骤
|
||||
- 区域切换方法
|
||||
|
||||
**何时阅读**: 遇到区域配置问题或想了解修复细节时
|
||||
|
||||
---
|
||||
|
||||
### 七牛云直传配置.md
|
||||
**适合人群**: 开发人员
|
||||
|
||||
**内容**:
|
||||
- 上传流程对比(旧 vs 新)
|
||||
- 安装依赖
|
||||
- 后端 API 文档
|
||||
- 前端使用示例
|
||||
- 工作原理
|
||||
- 安全性说明
|
||||
- 性能优化
|
||||
- 故障排查
|
||||
- 迁移指南
|
||||
|
||||
**何时阅读**: 需要了解完整技术实现或集成到其他页面时
|
||||
|
||||
---
|
||||
|
||||
### 七牛云区域配置流程图.md
|
||||
**适合人群**: 所有人
|
||||
|
||||
**内容**:
|
||||
- 问题和解决方案的可视化对比
|
||||
- 完整上传流程图
|
||||
- 区域映射关系图
|
||||
- 代码对比
|
||||
- 优势对比
|
||||
|
||||
**何时阅读**: 想快速理解上传机制时
|
||||
|
||||
---
|
||||
|
||||
## 最近更新 (2026-04-09)
|
||||
|
||||
### ✅ 已完成
|
||||
- 修复了七牛云区域配置问题
|
||||
- 添加了 `getQiniuRegion()` 函数
|
||||
- 实现了动态区域配置
|
||||
- 创建了完整的测试文档
|
||||
|
||||
### 🔧 修复的问题
|
||||
**问题**: 上传时出现 "incorrect region" 错误
|
||||
|
||||
**原因**: 前端硬编码区域为 z0(华东),但数据库配置为 z2(华南)
|
||||
|
||||
**解决**: 前端动态从后端获取区域配置,自动使用正确的区域
|
||||
|
||||
### 📝 涉及的文件
|
||||
- `platform/src/utils/qiniuUpload.js` - 添加区域映射函数
|
||||
- `go/controllers/qiniu_upload.go` - 返回区域配置
|
||||
- `platform/docs/*.md` - 创建文档
|
||||
|
||||
---
|
||||
|
||||
## 技术架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 七牛云直传架构 │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
|
||||
前端 (Vue3)
|
||||
├── platform/src/utils/qiniuUpload.js
|
||||
│ ├── smartUpload() - 智能选择上传方式
|
||||
│ ├── uploadToQiniu() - 七牛云直传
|
||||
│ ├── uploadToLocal() - 本地上传(中转)
|
||||
│ ├── getStorageConfig() - 获取存储配置
|
||||
│ ├── getQiniuToken() - 获取上传凭证
|
||||
│ ├── saveFileRecord() - 保存文件记录
|
||||
│ ├── batchUpload() - 批量上传
|
||||
│ └── getQiniuRegion() - 区域映射 ⭐ 新增
|
||||
│
|
||||
└── platform/src/views/platform/softwareupgrade/components/edit.vue
|
||||
└── 使用 smartUpload() 上传文件
|
||||
|
||||
后端 (Go + Beego)
|
||||
├── go/controllers/qiniu_upload.go
|
||||
│ ├── GetStorageConfig() - 获取存储配置
|
||||
│ ├── GetUploadToken() - 生成上传凭证
|
||||
│ ├── SaveFileRecord() - 保存文件记录
|
||||
│ └── getQiniuUploadURL() - 获取上传地址
|
||||
│
|
||||
├── go/routers/platform/platform.go
|
||||
│ ├── GET /platform/storage/config
|
||||
│ ├── GET /platform/qiniu/token
|
||||
│ └── POST /platform/qiniu/save
|
||||
│
|
||||
└── go/models/storage_config.go
|
||||
└── GetStorageConfig() - 从数据库读取配置
|
||||
|
||||
数据库 (MySQL)
|
||||
├── system_storage_config - 存储配置表
|
||||
│ ├── storage_type - 存储类型 (local/qiniu)
|
||||
│ ├── qiniu_region - 七牛云区域 (z0/z1/z2/...)
|
||||
│ ├── qiniu_bucket - 存储空间名称
|
||||
│ ├── qiniu_domain - 访问域名
|
||||
│ ├── qiniu_access_key - AccessKey
|
||||
│ └── qiniu_secret_key - SecretKey
|
||||
│
|
||||
└── system_file - 文件记录表
|
||||
├── id - 文件 ID
|
||||
├── name - 文件名
|
||||
├── src - 文件 URL
|
||||
├── size - 文件大小
|
||||
├── md5 - 文件 MD5
|
||||
└── ...
|
||||
|
||||
七牛云 (Qiniu Cloud)
|
||||
└── 存储空间: yunzerwebsite
|
||||
├── 区域: z2 (华南)
|
||||
├── 域名: http://7colud.yunzer.cn
|
||||
└── 上传地址: https://up-z2.qiniup.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 上传流程
|
||||
|
||||
### 简化流程
|
||||
```
|
||||
1. 用户选择文件
|
||||
↓
|
||||
2. 前端获取存储配置 (storageType: 'qiniu')
|
||||
↓
|
||||
3. 前端获取上传凭证 (token, region: 'z2')
|
||||
↓
|
||||
4. 前端直接上传到七牛云 (up-z2.qiniup.com)
|
||||
↓
|
||||
5. 前端保存文件记录到数据库
|
||||
↓
|
||||
6. 显示上传成功
|
||||
```
|
||||
|
||||
### 详细流程
|
||||
参见 [七牛云区域配置流程图.md](./七牛云区域配置流程图.md)
|
||||
|
||||
---
|
||||
|
||||
## 支持的区域
|
||||
|
||||
| 区域代码 | 区域名称 | 上传地址 | 状态 |
|
||||
|---------|---------|---------|------|
|
||||
| z0 | 华东 | up-z0.qiniup.com | ✓ 支持 |
|
||||
| z1 | 华北 | up-z1.qiniup.com | ✓ 支持 |
|
||||
| z2 | 华南 | up-z2.qiniup.com | ✓ 支持 (当前) |
|
||||
| na0 | 北美 | up-na0.qiniup.com | ✓ 支持 |
|
||||
| as0 | 新加坡 | up-as0.qiniup.com | ✓ 支持 |
|
||||
| cn-east-2 | 华东-浙江2 | up-cn-east-2.qiniup.com | ✓ 支持 |
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 如何测试上传功能?
|
||||
**A**: 参见 [七牛云上传测试步骤.md](./七牛云上传测试步骤.md)
|
||||
|
||||
### Q2: 上传时出现 "incorrect region" 错误?
|
||||
**A**: 已修复。参见 [七牛云区域配置修复说明.md](./七牛云区域配置修复说明.md)
|
||||
|
||||
### Q3: 如何切换到其他区域?
|
||||
**A**: 修改数据库配置即可,无需改代码:
|
||||
```sql
|
||||
UPDATE system_storage_config
|
||||
SET qiniu_region = 'z1' -- 切换到华北
|
||||
WHERE id = 1;
|
||||
```
|
||||
|
||||
### Q4: 如何在其他页面使用上传功能?
|
||||
**A**: 参见 [七牛云直传配置.md](./七牛云直传配置.md) 的"前端使用"章节
|
||||
|
||||
### Q5: 大文件上传会超时吗?
|
||||
**A**: 不会。七牛云 SDK 支持:
|
||||
- 自动分片上传
|
||||
- 断点续传
|
||||
- 并发上传
|
||||
- 前端已设置 `timeout: 0`(无超时限制)
|
||||
|
||||
### Q6: 上传是否经过服务器?
|
||||
**A**: 不经过。前端直接上传到七牛云,服务器只负责:
|
||||
- 生成上传凭证
|
||||
- 保存文件记录到数据库
|
||||
|
||||
### Q7: 如何回滚到本地存储?
|
||||
**A**: 修改数据库配置:
|
||||
```sql
|
||||
UPDATE system_storage_config
|
||||
SET storage_type = 'local'
|
||||
WHERE id = 1;
|
||||
```
|
||||
前端会自动切换到本地上传方式。
|
||||
|
||||
---
|
||||
|
||||
## 性能对比
|
||||
|
||||
### 旧方案(服务器中转)
|
||||
```
|
||||
100MB 文件上传时间:
|
||||
- 上传到服务器:2 分钟
|
||||
- 服务器上传到七牛云:2 分钟
|
||||
- 总计:4 分钟
|
||||
```
|
||||
|
||||
### 新方案(直传)
|
||||
```
|
||||
100MB 文件上传时间:
|
||||
- 直接上传到七牛云:2 分钟
|
||||
- 总计:2 分钟
|
||||
```
|
||||
|
||||
**效率提升**: 50%
|
||||
|
||||
---
|
||||
|
||||
## 依赖
|
||||
|
||||
### 前端
|
||||
- `qiniu-js@^3.4.4` - 七牛云 JavaScript SDK
|
||||
|
||||
### 后端
|
||||
- `github.com/qiniu/go-sdk/v7@v7.18.2` - 七牛云 Go SDK
|
||||
|
||||
---
|
||||
|
||||
## 相关链接
|
||||
|
||||
- [七牛云官方文档](https://developer.qiniu.com/)
|
||||
- [七牛云存储区域](https://developer.qiniu.com/kodo/1671/region-endpoint-fq)
|
||||
- [qiniu-js SDK 文档](https://developer.qiniu.com/kodo/1283/javascript)
|
||||
|
||||
---
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题,请联系开发团队。
|
||||
|
||||
---
|
||||
|
||||
## 更新日期
|
||||
|
||||
2026-04-09
|
||||
@@ -0,0 +1,82 @@
|
||||
# Platform前端项目文档
|
||||
|
||||
## 📚 文档目录
|
||||
|
||||
### 功能使用文档
|
||||
- [字典使用说明](./dictionary-usage.md)
|
||||
- [Pinia字典指南](./pinia-dict-guide.md)
|
||||
- [调用字典](./调用字典.md)
|
||||
- [接口调用](./接口调用.md)
|
||||
- [拼接接口路径](./拼接接口路径.md)
|
||||
- [获取缓存数据](./获取缓存数据.md)
|
||||
- [调用图片上传组件](./调用图片上传组件.md)
|
||||
- [一键复制](./一键复制.md)
|
||||
- [文件上传超时配置](./文件上传超时配置.md) - 大文件上传配置说明
|
||||
- [图片URL处理说明](./图片URL处理说明.md) - 图片URL拼接处理
|
||||
- [七牛云直传配置](./七牛云直传配置.md) - ⚡ 七牛云直传上传(推荐!)
|
||||
|
||||
### 存储配置功能
|
||||
存储配置功能的前端实现已完成,包括:
|
||||
- 存储配置界面组件:`src/views/system/platformsettings/components/storageSettings.vue`
|
||||
- API接口:`src/api/sitesettings.js`
|
||||
|
||||
详细文档请查看后端项目文档:`go/docs/README_STORAGE.md`
|
||||
|
||||
## 🚀 快速导航
|
||||
|
||||
### 新手入门
|
||||
1. 了解项目结构
|
||||
2. 阅读 [接口调用](./接口调用.md)
|
||||
3. 学习 [字典使用](./dictionary-usage.md)
|
||||
|
||||
### 常用功能
|
||||
- 字典管理:[dictionary-usage.md](./dictionary-usage.md)
|
||||
- 图片上传:[调用图片上传组件.md](./调用图片上传组件.md)
|
||||
- 数据缓存:[获取缓存数据.md](./获取缓存数据.md)
|
||||
|
||||
## 📂 项目结构
|
||||
|
||||
```
|
||||
platform/
|
||||
├── src/
|
||||
│ ├── api/ # API接口
|
||||
│ ├── assets/ # 静态资源
|
||||
│ ├── components/ # 公共组件
|
||||
│ ├── router/ # 路由配置
|
||||
│ ├── stores/ # 状态管理
|
||||
│ ├── utils/ # 工具函数
|
||||
│ └── views/ # 页面组件
|
||||
├── public/ # 公共资源
|
||||
├── docs/ # 文档(本目录)
|
||||
└── package.json # 依赖配置
|
||||
```
|
||||
|
||||
## 🔧 技术栈
|
||||
|
||||
- Vue 3
|
||||
- Element Plus
|
||||
- Pinia
|
||||
- Vue Router
|
||||
- Axios
|
||||
- Vite
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [Vue 3 文档](https://vuejs.org/)
|
||||
- [Element Plus 文档](https://element-plus.org/)
|
||||
- [Pinia 文档](https://pinia.vuejs.org/)
|
||||
- [Vite 文档](https://vitejs.dev/)
|
||||
|
||||
## 📝 更新日志
|
||||
|
||||
### 2026-04-09
|
||||
- ✅ 移除文件上传超时限制
|
||||
- ✅ 支持大文件上传(配合后端 2GB 限制)
|
||||
- ✅ 修复图片 URL 重复拼接问题
|
||||
- ✅ 完善文件上传文档
|
||||
|
||||
### 2024-01-01
|
||||
- ✅ 完成存储配置界面
|
||||
- ✅ 支持本地存储和七牛云存储配置
|
||||
- ✅ 表单验证和草稿保存
|
||||
- ✅ 完善文档体系
|
||||
@@ -0,0 +1 @@
|
||||
# 字典使用示例
|
||||
@@ -0,0 +1,393 @@
|
||||
# Pinia 字典管理系统使用指南
|
||||
|
||||
## 系统架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ API 接口 (getDictItemsByCode) │
|
||||
│ /api/dict/items/code/{code} │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ Pinia Store (useDictStore) │
|
||||
│ ✅ 自动缓存字典数据 │
|
||||
│ ✅ 避免重复请求 │
|
||||
│ ✅ 支持同步/异步访问 │
|
||||
└──────────────┬──────────────────────┘
|
||||
│
|
||||
┌──────┴──────┐
|
||||
↓ ↓
|
||||
┌────────┐ ┌──────────────┐
|
||||
│组件 │ │Composable │
|
||||
│直接用 │ │useDict Hook │
|
||||
└────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 核心文件说明
|
||||
|
||||
### 1. **Store**: `src/stores/dict.js`
|
||||
|
||||
字典数据的全局管理器
|
||||
|
||||
**主要方法**:
|
||||
```javascript
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
|
||||
// ✅ 异步获取字典(推荐)
|
||||
const items = await dictStore.getDictItems('user_status')
|
||||
|
||||
// ✅ 同步获取字典(已缓存时)
|
||||
const items = dictStore.getDictItemsSync('user_status')
|
||||
|
||||
// ✅ 获取字典值对应的标签
|
||||
const label = dictStore.getDictLabel('user_status', 1)
|
||||
|
||||
// ✅ 预加载多个字典
|
||||
await dictStore.preloadDicts(['user_status', 'user_role'])
|
||||
|
||||
// ✅ 清空缓存
|
||||
dictStore.clearCache('user_status')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. **常量**: `src/constants/dictCodes.js`
|
||||
|
||||
集中管理所有字典编码
|
||||
|
||||
**使用示例**:
|
||||
```javascript
|
||||
import { DICT_CODES } from '@/constants/dictCodes'
|
||||
|
||||
// 好处:避免硬编码,IDE 有自动完成
|
||||
const items = await dictStore.getDictItems(DICT_CODES.USER_STATUS)
|
||||
|
||||
// 所有可用的编码:
|
||||
DICT_CODES.USER_STATUS // 用户状态
|
||||
DICT_CODES.USER_GENDER // 用户性别
|
||||
DICT_CODES.USER_ROLE // 用户角色
|
||||
DICT_CODES.DEPT_STATUS // 部门状态
|
||||
DICT_CODES.POSITION_STATUS // 职位状态
|
||||
// ... 更多编码
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. **Composable**: `src/composables/useDict.js`
|
||||
|
||||
简化在组件中使用字典的 Hook
|
||||
|
||||
**基础用法**:
|
||||
```javascript
|
||||
import { useDictionary, useUserStatusDict } from '@/composables/useDict'
|
||||
import { DICT_CODES } from '@/constants/dictCodes'
|
||||
|
||||
// 方式1:使用常量
|
||||
const { statusDict, loading } = useDictionary(DICT_CODES.USER_STATUS)
|
||||
|
||||
// 方式2:使用字符串
|
||||
const { dicts, loading } = useDictionary('user_status')
|
||||
|
||||
// 方式3:使用特化 Hook(推荐)
|
||||
const { user_statusDict, loading } = useUserStatusDict()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 场景1:在列表页加载字典
|
||||
|
||||
**文件**: `src/views/system/users/index.vue`
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const dictStore = useDictStore()
|
||||
const statusDict = ref([])
|
||||
|
||||
const fetchStatusDict = async () => {
|
||||
statusDict.value = await dictStore.getDictItems('user_status')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchStatusDict()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 传给子组件 -->
|
||||
<UserEditDialog :status-dict="statusDict" />
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景2:在编辑对话框使用字典
|
||||
|
||||
**文件**: `src/views/system/users/components/UserEdit.vue`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- 状态选择 -->
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="form.status">
|
||||
<el-option
|
||||
v-for="item in statusDict"
|
||||
:key="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
:value="item.dict_value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
statusDict: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景3:快速使用 Composable Hook
|
||||
|
||||
最简单的方式,自动处理加载:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div>
|
||||
<p v-if="loading">加载中...</p>
|
||||
<el-select v-else v-model="status">
|
||||
<el-option
|
||||
v-for="item in user_statusDict"
|
||||
:key="item.dict_value"
|
||||
:label="item.dict_label"
|
||||
:value="item.dict_value"
|
||||
/>
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useUserStatusDict } from '@/composables/useDict'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const status = ref('active')
|
||||
const { user_statusDict, loading } = useUserStatusDict()
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 场景4:预加载应用启动时需要的字典
|
||||
|
||||
**文件**: `src/main.js`
|
||||
|
||||
```javascript
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { useDictStore } from '@/stores/dict'
|
||||
import { DICT_CODES } from '@/constants/dictCodes'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
|
||||
// 在应用启动后预加载常用字典
|
||||
const dictStore = useDictStore()
|
||||
await dictStore.preloadDicts([
|
||||
DICT_CODES.USER_STATUS,
|
||||
DICT_CODES.COMMON_STATUS,
|
||||
DICT_CODES.YES_NO,
|
||||
])
|
||||
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 字典数据结构
|
||||
|
||||
后端返回的字典数据结构:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"dict_id": 1,
|
||||
"dict_value": "1",
|
||||
"dict_label": "启用",
|
||||
"dict_type": "user_status",
|
||||
"remarks": "用户启用状态",
|
||||
"remark": "用户启用状态"
|
||||
},
|
||||
{
|
||||
"dict_id": 2,
|
||||
"dict_value": "0",
|
||||
"dict_label": "禁用",
|
||||
"dict_type": "user_status",
|
||||
"remarks": "用户禁用状态",
|
||||
"remark": "用户禁用状态"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**关键字段**:
|
||||
- `dict_value`: 字典值(存储在数据库中)
|
||||
- `dict_label`: 字典标签(显示给用户)
|
||||
- `dict_type`: 字典类型编码(如 'user_status')
|
||||
|
||||
---
|
||||
|
||||
## 最佳实践
|
||||
|
||||
### ✅ DO
|
||||
|
||||
1. **使用常量而不是硬编码字符串**
|
||||
```javascript
|
||||
// ✅ 好
|
||||
dictStore.getDictItems(DICT_CODES.USER_STATUS)
|
||||
|
||||
// ❌ 差
|
||||
dictStore.getDictItems('user_status')
|
||||
```
|
||||
|
||||
2. **在父组件加载,通过 props 传给子组件**
|
||||
```javascript
|
||||
// ✅ 父组件负责数据,子组件负责展示
|
||||
// index.vue
|
||||
const statusDict = await dictStore.getDictItems(DICT_CODES.USER_STATUS)
|
||||
|
||||
// UserEdit.vue
|
||||
const props = defineProps({ statusDict: Array })
|
||||
```
|
||||
|
||||
3. **用 Composable 简化组件逻辑**
|
||||
```javascript
|
||||
// ✅ 一行代码搞定
|
||||
const { user_statusDict, loading } = useUserStatusDict()
|
||||
```
|
||||
|
||||
4. **预加载常用字典**
|
||||
```javascript
|
||||
// ✅ 应用启动时预加载,避免页面初始化时加载
|
||||
await dictStore.preloadDicts([...])
|
||||
```
|
||||
|
||||
### ❌ DON'T
|
||||
|
||||
1. **不要在多个地方重复加载同一个字典**
|
||||
```javascript
|
||||
// ❌ 糟糕:重复加载
|
||||
// 页面A
|
||||
const dict1 = await dictStore.getDictItems('user_status')
|
||||
|
||||
// 页面B(Store 会自动缓存,但代码看起来重复)
|
||||
const dict2 = await dictStore.getDictItems('user_status')
|
||||
```
|
||||
|
||||
2. **不要忘记处理加载状态**
|
||||
```javascript
|
||||
// ❌ 可能展示空白
|
||||
const { statusDict } = useUserStatusDict()
|
||||
|
||||
// ✅ 处理加载状态
|
||||
const { statusDict, loading } = useUserStatusDict()
|
||||
if (loading) { /* 显示加载中 */ }
|
||||
```
|
||||
|
||||
3. **不要混用不同的字典访问方式**
|
||||
```javascript
|
||||
// ❌ 混乱
|
||||
const dict1 = await dictStore.getDictItems('user_status')
|
||||
const dict2 = dictStore.getDictItemsSync('user_role')
|
||||
|
||||
// ✅ 统一使用
|
||||
const dict1 = await dictStore.getDictItems('user_status')
|
||||
const dict2 = await dictStore.getDictItems('user_role')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化建议
|
||||
|
||||
| 优化项 | 说明 |
|
||||
|------|------|
|
||||
| **缓存** | Store 自动缓存,同一个字典只请求一次 |
|
||||
| **预加载** | 在路由切换前预加载需要的字典 |
|
||||
| **同步访问** | 已加载的字典可用 `getDictItemsSync` 同步获取 |
|
||||
| **避免重复** | 不要在多个组件重复请求同一个字典 |
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题1:状态选项为空
|
||||
|
||||
**原因**:字典未加载
|
||||
**解决**:
|
||||
```javascript
|
||||
// ❌ 错误:字典还未加载
|
||||
const statusDict = dictStore.getDictItemsSync('user_status') // 返回 []
|
||||
|
||||
// ✅ 正确:等待异步加载完成
|
||||
const statusDict = await dictStore.getDictItems('user_status')
|
||||
```
|
||||
|
||||
### 问题2:重复加载字典
|
||||
|
||||
**原因**:没有使用 Store 的缓存
|
||||
**解决**:
|
||||
```javascript
|
||||
// 所有调用都会自动使用缓存,只请求一次
|
||||
await dictStore.getDictItems('user_status') // 首次:发送请求
|
||||
await dictStore.getDictItems('user_status') // 第二次:返回缓存
|
||||
```
|
||||
|
||||
### 问题3:字典显示不对
|
||||
|
||||
**原因**:value 类型不匹配(如 1 vs "1")
|
||||
**解决**:
|
||||
```javascript
|
||||
// Store 会自动处理类型匹配
|
||||
const item = items.find(i =>
|
||||
String(i.dict_value) === String(value) || i.dict_value === value
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 集成检清表
|
||||
|
||||
- [ ] 创建 `src/stores/dict.js` - Store
|
||||
- [ ] 创建 `src/constants/dictCodes.js` - 常量
|
||||
- [ ] 创建 `src/composables/useDict.js` - Composable
|
||||
- [ ] 在 `index.vue` 中导入 `useDictStore`
|
||||
- [ ] 在 `UserEdit.vue` 中接收 `statusDict` props
|
||||
- [ ] 测试字典加载和显示
|
||||
- [ ] 验证缓存功能(打开浏览器 DevTools 检查 Network)
|
||||
- [ ] 预加载常用字典(可选)
|
||||
|
||||
---
|
||||
|
||||
## 相关文件修改
|
||||
|
||||
已修改的文件:
|
||||
- ✅ `src/stores/dict.js` - 新建
|
||||
- ✅ `src/constants/dictCodes.js` - 新建
|
||||
- ✅ `src/composables/useDict.js` - 新建
|
||||
- ✅ `src/views/system/users/index.vue` - 使用 `useDictStore`
|
||||
- ✅ `src/views/system/users/components/UserEdit.vue` - 导入字典库
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<el-button @click="handleCopy">复制数据</el-button>
|
||||
</template>
|
||||
|
||||
<script setup lang='ts'>
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
//一键复制代码
|
||||
function handleCopy() {
|
||||
if (!props.model) return
|
||||
|
||||
const data = [
|
||||
`客户名称: ${props.model.name || '-'}`,
|
||||
`联系人: ${props.model.contact || '-'}`,
|
||||
`电话: ${props.model.phone || '-'}`,
|
||||
`邮箱: ${props.model.email || '-'}`,
|
||||
`客户类型: ${getLabel(customerTypeOptions.value, props.model.customer_type)}`,
|
||||
`客户等级: ${getLabel(customerLevelOptions.value, props.model.customer_level)}`,
|
||||
`所属行业: ${getLabel(industryOptions.value, props.model.industry)}`,
|
||||
`客户状态: ${getLabel(customerStatusOptions.value, String(props.model.status ?? '')) || ((props.model.status===1||props.model.status==='1') ? '正常' : '停用')}`,
|
||||
`地址: ${props.model.address || '-'}`,
|
||||
`经营范围: ${props.model.business_scope || '-'}`
|
||||
].join('\n')
|
||||
|
||||
navigator.clipboard.writeText(data).then(() => {
|
||||
ElMessage.success('数据已复制到剪贴板')
|
||||
}).catch(() => {
|
||||
ElMessage.error('复制失败')
|
||||
})
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,201 @@
|
||||
# 七牛云上传测试步骤
|
||||
|
||||
## 前置条件
|
||||
|
||||
1. 数据库配置已正确设置:
|
||||
- `storage_type = 'qiniu'`
|
||||
- `qiniu_region = 'z2'` (华南)
|
||||
- `qiniu_bucket = 'yunzerwebsite'`
|
||||
- `qiniu_domain = 'http://7colud.yunzer.cn'`
|
||||
- `qiniu_access_key` 和 `qiniu_secret_key` 已配置
|
||||
|
||||
2. 前端依赖已安装:
|
||||
- `qiniu-js@^3.4.4` ✓ (已在 package.json 中)
|
||||
|
||||
3. 后端服务已重启:
|
||||
```bash
|
||||
systemctl restart go-api
|
||||
```
|
||||
|
||||
## 测试步骤
|
||||
|
||||
### 1. 登录系统
|
||||
|
||||
访问前端页面并登录:
|
||||
- URL: https://platform.yunzer.cn
|
||||
- 账号: hero920103
|
||||
- 密码: 920103
|
||||
|
||||
### 2. 进入软件升级页面
|
||||
|
||||
导航到:平台管理 → 软件升级
|
||||
|
||||
### 3. 上传文件测试
|
||||
|
||||
点击"新增"或"编辑"按钮,在弹出的对话框中:
|
||||
|
||||
1. 点击"上传软件包"按钮
|
||||
2. 选择一个文件(建议先用小文件测试,如 1-10MB)
|
||||
3. 观察上传进度条
|
||||
4. 等待上传完成
|
||||
|
||||
### 4. 验证上传结果
|
||||
|
||||
#### 前端验证
|
||||
- 检查是否显示上传成功消息
|
||||
- 检查文件 URL 是否正确(应该是 `http://7colud.yunzer.cn/...`)
|
||||
- 检查文件是否可以预览/下载
|
||||
|
||||
#### 后端日志验证
|
||||
```bash
|
||||
tail -f /www/wwwroot/api.yunzer.cn/go.log
|
||||
```
|
||||
|
||||
查看日志中是否有:
|
||||
- `GET /platform/storage/config` - 获取存储配置
|
||||
- `GET /platform/qiniu/token` - 获取上传凭证
|
||||
- `POST /platform/qiniu/save` - 保存文件记录
|
||||
|
||||
#### 七牛云控制台验证
|
||||
1. 登录七牛云控制台
|
||||
2. 进入 `yunzerwebsite` 存储空间
|
||||
3. 检查文件是否已上传
|
||||
4. 文件路径格式应为:`2026/04/09/时间戳.扩展名`
|
||||
|
||||
#### 数据库验证
|
||||
```sql
|
||||
SELECT id, name, src, size, type, cate, md5, create_time
|
||||
FROM system_file
|
||||
ORDER BY id DESC
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
检查:
|
||||
- `src` 字段应该是完整的七牛云 URL
|
||||
- `md5` 字段应该有值
|
||||
- `size` 字段应该正确
|
||||
|
||||
## 预期结果
|
||||
|
||||
### 成功标志
|
||||
1. 前端显示上传成功
|
||||
2. 文件 URL 格式正确:`http://7colud.yunzer.cn/2026/04/09/xxxxx.ext`
|
||||
3. 七牛云控制台能看到文件
|
||||
4. 数据库有对应记录
|
||||
5. 文件可以正常访问和下载
|
||||
|
||||
### 上传流程
|
||||
```
|
||||
前端 → 获取存储配置 (storageType: 'qiniu')
|
||||
→ 获取上传凭证 (token, region: 'z2')
|
||||
→ 直接上传到七牛云 (up-z2.qiniup.com)
|
||||
→ 保存文件记录到数据库
|
||||
→ 返回文件 URL
|
||||
```
|
||||
|
||||
## 常见问题排查
|
||||
|
||||
### 问题 1: "incorrect region" 错误
|
||||
|
||||
**错误信息**:
|
||||
```
|
||||
xhr request failed, code: 400
|
||||
response: {"error":"incorrect region, please use up-z2.qiniup.com, bucket is: yunzerwebsite"}
|
||||
```
|
||||
|
||||
**原因**: 前端使用的区域与七牛云 bucket 实际区域不匹配
|
||||
|
||||
**解决**:
|
||||
- 已修复:添加了 `getQiniuRegion()` 函数,根据后端返回的 region 动态设置
|
||||
- 后端返回 `region: 'z2'`,前端会自动使用 `qiniu.region.z2`
|
||||
|
||||
### 问题 2: "获取上传凭证失败"
|
||||
|
||||
**可能原因**:
|
||||
- 未登录或 token 过期
|
||||
- 存储配置未设置为七牛云
|
||||
- 七牛云配置不完整
|
||||
|
||||
**解决**:
|
||||
1. 检查登录状态
|
||||
2. 检查数据库 `system_storage_config` 表
|
||||
3. 确认 AccessKey 和 SecretKey 正确
|
||||
|
||||
### 问题 3: 上传后文件无法访问
|
||||
|
||||
**可能原因**:
|
||||
- 七牛云域名配置错误
|
||||
- 域名未绑定或未备案
|
||||
- 文件权限设置问题
|
||||
|
||||
**解决**:
|
||||
1. 检查 `qiniu_domain` 配置
|
||||
2. 登录七牛云控制台检查域名绑定
|
||||
3. 检查存储空间访问权限(应为公开)
|
||||
|
||||
### 问题 4: 大文件上传失败
|
||||
|
||||
**可能原因**:
|
||||
- 网络超时
|
||||
- 浏览器限制
|
||||
|
||||
**解决**:
|
||||
1. 七牛云 SDK 支持断点续传,会自动重试
|
||||
2. 检查网络连接
|
||||
3. 尝试分片上传(SDK 自动处理)
|
||||
|
||||
## 性能测试
|
||||
|
||||
### 小文件测试 (< 10MB)
|
||||
- 预期时间:几秒内完成
|
||||
- 不会分片上传
|
||||
|
||||
### 中等文件测试 (10MB - 100MB)
|
||||
- 预期时间:根据网速,通常 1-2 分钟
|
||||
- 可能会分片上传
|
||||
|
||||
### 大文件测试 (> 100MB)
|
||||
- 预期时间:根据网速
|
||||
- 会自动分片上传
|
||||
- 支持断点续传
|
||||
|
||||
## 对比测试
|
||||
|
||||
### 旧方案(服务器中转)
|
||||
```
|
||||
100MB 文件上传时间:
|
||||
- 上传到服务器:2 分钟
|
||||
- 服务器上传到七牛云:2 分钟
|
||||
- 总计:4 分钟
|
||||
```
|
||||
|
||||
### 新方案(直传)
|
||||
```
|
||||
100MB 文件上传时间:
|
||||
- 直接上传到七牛云:2 分钟
|
||||
- 总计:2 分钟
|
||||
```
|
||||
|
||||
**效率提升**: 50%
|
||||
|
||||
## 回滚方案
|
||||
|
||||
如果七牛云直传有问题,可以临时切换回本地存储:
|
||||
|
||||
```sql
|
||||
UPDATE system_storage_config
|
||||
SET storage_type = 'local'
|
||||
WHERE id = 1;
|
||||
```
|
||||
|
||||
前端会自动检测并使用本地上传方式(通过服务器中转)。
|
||||
|
||||
## 相关文件
|
||||
|
||||
- `platform/src/utils/qiniuUpload.js` - 上传工具(已添加 getQiniuRegion 函数)
|
||||
- `go/controllers/qiniu_upload.go` - 后端控制器
|
||||
- `platform/docs/七牛云直传配置.md` - 详细配置文档
|
||||
|
||||
## 更新日期
|
||||
|
||||
2026-04-09
|
||||
@@ -0,0 +1,248 @@
|
||||
# 七牛云区域配置修复说明
|
||||
|
||||
## 问题描述
|
||||
|
||||
上传文件到七牛云时出现区域错误:
|
||||
|
||||
```
|
||||
xhr request failed, code: 400
|
||||
response: {"error":"incorrect region, please use up-z2.qiniup.com, bucket is: yunzerwebsite"}
|
||||
```
|
||||
|
||||
## 问题原因
|
||||
|
||||
前端代码中硬编码了七牛云区域为 `z0`(华东),但实际数据库配置的是 `z2`(华南):
|
||||
|
||||
```javascript
|
||||
// 错误的硬编码方式
|
||||
const config = {
|
||||
useCdnDomain: true,
|
||||
region: qiniu.region.z0, // ❌ 硬编码为华东
|
||||
};
|
||||
```
|
||||
|
||||
数据库实际配置:
|
||||
- `qiniu_region = 'z2'` (华南)
|
||||
- `qiniu_bucket = 'yunzerwebsite'`
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 添加区域映射函数
|
||||
|
||||
在 `platform/src/utils/qiniuUpload.js` 文件末尾添加了 `getQiniuRegion()` 函数:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 根据区域代码获取七牛云区域对象
|
||||
* @param {string} regionCode 区域代码 (z0, z1, z2, na0, as0, cn-east-2)
|
||||
* @returns {Object} 七牛云区域对象
|
||||
*/
|
||||
function getQiniuRegion(regionCode) {
|
||||
switch (regionCode) {
|
||||
case 'z0':
|
||||
return qiniu.region.z0; // 华东
|
||||
case 'z1':
|
||||
return qiniu.region.z1; // 华北
|
||||
case 'z2':
|
||||
return qiniu.region.z2; // 华南
|
||||
case 'na0':
|
||||
return qiniu.region.na0; // 北美
|
||||
case 'as0':
|
||||
return qiniu.region.as0; // 新加坡
|
||||
case 'cn-east-2':
|
||||
return qiniu.region.cnEast2; // 华东-浙江2
|
||||
default:
|
||||
return qiniu.region.z0; // 默认华东
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 动态获取区域配置
|
||||
|
||||
修改 `uploadToQiniu()` 函数,从后端返回的数据中读取区域配置:
|
||||
|
||||
```javascript
|
||||
export async function uploadToQiniu(file, options = {}) {
|
||||
// 1. 获取上传凭证(包含区域信息)
|
||||
const tokenRes = await getQiniuToken();
|
||||
const { token, keyPrefix, domain, region, uploadUrl } = tokenRes.data;
|
||||
|
||||
// 2. 根据后端返回的区域代码动态获取区域对象
|
||||
const qiniuRegion = getQiniuRegion(region); // ✓ 动态获取
|
||||
|
||||
const config = {
|
||||
useCdnDomain: true,
|
||||
region: qiniuRegion, // ✓ 使用正确的区域
|
||||
};
|
||||
|
||||
// 3. 上传文件
|
||||
const observable = qiniu.upload(file, key, token, putExtra, config);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 工作流程
|
||||
|
||||
```
|
||||
1. 前端调用 getQiniuToken()
|
||||
↓
|
||||
2. 后端从数据库读取配置
|
||||
- storage_type: 'qiniu'
|
||||
- qiniu_region: 'z2'
|
||||
- qiniu_bucket: 'yunzerwebsite'
|
||||
↓
|
||||
3. 后端返回配置信息
|
||||
{
|
||||
token: "...",
|
||||
region: "z2", ← 区域代码
|
||||
bucket: "yunzerwebsite",
|
||||
uploadUrl: "https://up-z2.qiniup.com"
|
||||
}
|
||||
↓
|
||||
4. 前端调用 getQiniuRegion('z2')
|
||||
返回: qiniu.region.z2
|
||||
↓
|
||||
5. 使用正确的区域上传文件
|
||||
上传地址: https://up-z2.qiniup.com
|
||||
```
|
||||
|
||||
## 支持的区域
|
||||
|
||||
| 区域代码 | 区域名称 | qiniu-js 对象 | 上传地址 |
|
||||
|---------|---------|--------------|---------|
|
||||
| z0 | 华东 | qiniu.region.z0 | https://up-z0.qiniup.com |
|
||||
| z1 | 华北 | qiniu.region.z1 | https://up-z1.qiniup.com |
|
||||
| z2 | 华南 | qiniu.region.z2 | https://up-z2.qiniup.com |
|
||||
| na0 | 北美 | qiniu.region.na0 | https://up-na0.qiniup.com |
|
||||
| as0 | 新加坡 | qiniu.region.as0 | https://up-as0.qiniup.com |
|
||||
| cn-east-2 | 华东-浙江2 | qiniu.region.cnEast2 | https://up-cn-east-2.qiniup.com |
|
||||
|
||||
## 验证步骤
|
||||
|
||||
### 1. 确认依赖已安装
|
||||
|
||||
```bash
|
||||
cd platform
|
||||
npm list qiniu-js
|
||||
```
|
||||
|
||||
预期输出:
|
||||
```
|
||||
qiniu-js@3.4.4
|
||||
```
|
||||
|
||||
✓ 已确认:`qiniu-js@^3.4.4` 已在 `package.json` 中
|
||||
|
||||
### 2. 重启后端服务
|
||||
|
||||
```bash
|
||||
systemctl restart go-api
|
||||
systemctl status go-api
|
||||
```
|
||||
|
||||
### 3. 测试上传
|
||||
|
||||
1. 登录系统
|
||||
2. 进入软件升级页面
|
||||
3. 上传一个文件
|
||||
4. 观察是否成功
|
||||
|
||||
### 4. 检查日志
|
||||
|
||||
```bash
|
||||
# 后端日志
|
||||
tail -f /www/wwwroot/api.yunzer.cn/go.log
|
||||
|
||||
# 前端控制台
|
||||
# 打开浏览器开发者工具,查看 Network 标签
|
||||
```
|
||||
|
||||
预期请求:
|
||||
```
|
||||
GET /platform/storage/config
|
||||
→ { storageType: 'qiniu', qiniuRegion: 'z2' }
|
||||
|
||||
GET /platform/qiniu/token
|
||||
→ { token: '...', region: 'z2', uploadUrl: 'https://up-z2.qiniup.com' }
|
||||
|
||||
POST https://up-z2.qiniup.com ← 直接上传到七牛云
|
||||
→ { key: '...', hash: '...', size: ... }
|
||||
|
||||
POST /platform/qiniu/save
|
||||
→ { url: 'http://7colud.yunzer.cn/...', id: 123 }
|
||||
```
|
||||
|
||||
## 修复的文件
|
||||
|
||||
### 前端
|
||||
- `platform/src/utils/qiniuUpload.js`
|
||||
- 添加了 `getQiniuRegion()` 函数
|
||||
- 修改了 `uploadToQiniu()` 函数,使用动态区域配置
|
||||
|
||||
### 后端
|
||||
- `go/controllers/qiniu_upload.go`
|
||||
- `GetUploadToken()` 方法返回 `region` 字段
|
||||
- `getQiniuUploadURL()` 函数根据区域返回正确的上传地址
|
||||
|
||||
## 优势
|
||||
|
||||
### 1. 灵活性
|
||||
- 支持所有七牛云区域
|
||||
- 无需修改代码即可切换区域
|
||||
- 只需在数据库中修改配置
|
||||
|
||||
### 2. 可维护性
|
||||
- 区域配置集中管理
|
||||
- 代码更清晰易懂
|
||||
- 便于扩展新区域
|
||||
|
||||
### 3. 正确性
|
||||
- 自动使用正确的上传地址
|
||||
- 避免区域不匹配错误
|
||||
- 提高上传成功率
|
||||
|
||||
## 切换区域方法
|
||||
|
||||
如果需要切换到其他区域,只需修改数据库配置:
|
||||
|
||||
```sql
|
||||
-- 切换到华东 (z0)
|
||||
UPDATE system_storage_config
|
||||
SET qiniu_region = 'z0'
|
||||
WHERE id = 1;
|
||||
|
||||
-- 切换到华北 (z1)
|
||||
UPDATE system_storage_config
|
||||
SET qiniu_region = 'z1'
|
||||
WHERE id = 1;
|
||||
|
||||
-- 切换到华南 (z2) - 当前配置
|
||||
UPDATE system_storage_config
|
||||
SET qiniu_region = 'z2'
|
||||
WHERE id = 1;
|
||||
```
|
||||
|
||||
前端会自动使用新的区域配置,无需重启或修改代码。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [七牛云直传配置说明](./七牛云直传配置.md)
|
||||
- [七牛云上传测试步骤](./七牛云上传测试步骤.md)
|
||||
- [七牛云官方文档 - 存储区域](https://developer.qiniu.com/kodo/1671/region-endpoint-fq)
|
||||
|
||||
## 更新日期
|
||||
|
||||
2026-04-09
|
||||
|
||||
## 状态
|
||||
|
||||
✅ 已完成
|
||||
- 添加 `getQiniuRegion()` 函数
|
||||
- 修改 `uploadToQiniu()` 使用动态区域
|
||||
- 验证 `qiniu-js` 依赖已安装
|
||||
- 创建测试文档
|
||||
|
||||
⏭️ 下一步
|
||||
- 重启后端服务
|
||||
- 测试文件上传功能
|
||||
- 验证区域配置正确
|
||||
@@ -0,0 +1,248 @@
|
||||
# 七牛云区域配置流程图
|
||||
|
||||
## 问题:区域不匹配
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 修复前(错误) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
前端代码 后端数据库 七牛云服务器
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ │ │ │ │ │
|
||||
│ 硬编码: │ │ 配置: │ │ 实际: │
|
||||
│ region: │ │ qiniu_ │ │ bucket │
|
||||
│ z0 (华东) │ ────X────> │ region: │ ────X────> │ 在 z2 │
|
||||
│ │ 不匹配 │ z2 (华南) │ 不匹配 │ (华南) │
|
||||
│ │ │ │ │ │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐
|
||||
│ 错误: │
|
||||
│ incorrect│
|
||||
│ region │
|
||||
└──────────┘
|
||||
```
|
||||
|
||||
## 解决方案:动态获取区域
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 修复后(正确) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
前端 后端 七牛云
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ │ │ │ │ │
|
||||
│ 1. 请求 │ │ 2. 查询 │ │ │
|
||||
│ 获取配置 │ ───────> │ 数据库 │ │ │
|
||||
│ │ │ │ │ │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌──────────┐
|
||||
│ │ 返回: │
|
||||
│ <─────────── │ region: │
|
||||
│ │ 'z2' │
|
||||
│ └──────────┘
|
||||
▼
|
||||
┌──────────┐
|
||||
│ 3. 调用 │
|
||||
│ getQiniu │
|
||||
│ Region() │
|
||||
└──────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ 4. 使用 │ │ │ │ │
|
||||
│ qiniu. │ │ │ │ 5. 上传 │
|
||||
│ region. │ ───────────────────────────────> │ 成功 ✓ │
|
||||
│ z2 │ 上传到 up-z2.qiniup.com │ │
|
||||
│ │ │ │
|
||||
└──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
## 详细流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 完整上传流程 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
用户操作 前端 后端 七牛云
|
||||
│ │ │ │
|
||||
│ 1. 点击上传 │ │ │
|
||||
├──────────────────> │ │ │
|
||||
│ │ │ │
|
||||
│ │ 2. GET /platform/ │ │
|
||||
│ │ storage/config │ │
|
||||
│ ├─────────────────────>│ │
|
||||
│ │ │ │
|
||||
│ │ │ 3. 查询数据库 │
|
||||
│ │ │ storage_type: │
|
||||
│ │ │ 'qiniu' │
|
||||
│ │ │ qiniu_region: │
|
||||
│ │ │ 'z2' │
|
||||
│ │ │ │
|
||||
│ │ 4. 返回配置 │ │
|
||||
│ │ { storageType: │ │
|
||||
│ │ 'qiniu', │ │
|
||||
│ │ qiniuRegion: 'z2' }│ │
|
||||
│ │<─────────────────────│ │
|
||||
│ │ │ │
|
||||
│ │ 5. GET /platform/ │ │
|
||||
│ │ qiniu/token │ │
|
||||
│ ├─────────────────────>│ │
|
||||
│ │ │ │
|
||||
│ │ │ 6. 生成上传凭证 │
|
||||
│ │ │ (使用 AccessKey │
|
||||
│ │ │ 和 SecretKey) │
|
||||
│ │ │ │
|
||||
│ │ 7. 返回 token 和配置 │ │
|
||||
│ │ { token: '...', │ │
|
||||
│ │ region: 'z2', │ │
|
||||
│ │ bucket: '...', │ │
|
||||
│ │ uploadUrl: │ │
|
||||
│ │ 'up-z2.qiniup.com' } │
|
||||
│ │<─────────────────────│ │
|
||||
│ │ │ │
|
||||
│ │ 8. 调用 │ │
|
||||
│ │ getQiniuRegion('z2') │
|
||||
│ │ 返回: qiniu.region.z2 │
|
||||
│ │ │ │
|
||||
│ │ 9. 直接上传文件 │ │
|
||||
│ │ (使用 qiniu-js SDK) │
|
||||
│ ├──────────────────────────────────────────> │
|
||||
│ │ │ │
|
||||
│ │ │ │ 10. 接收文件
|
||||
│ │ │ │ 存储到 z2
|
||||
│ │ │ │ (华南)
|
||||
│ │ │ │
|
||||
│ │ 11. 返回上传结果 │ │
|
||||
│ │ { key: '...', │ │
|
||||
│ │ hash: '...', │ │
|
||||
│ │ size: ... } │ │
|
||||
│ │<──────────────────────────────────────────│
|
||||
│ │ │ │
|
||||
│ │ 12. POST /platform/ │ │
|
||||
│ │ qiniu/save │ │
|
||||
│ │ { key, hash, size, │ │
|
||||
│ │ name, mimeType } │ │
|
||||
│ ├─────────────────────>│ │
|
||||
│ │ │ │
|
||||
│ │ │ 13. 保存到数据库 │
|
||||
│ │ │ system_file 表 │
|
||||
│ │ │ │
|
||||
│ │ 14. 返回文件信息 │ │
|
||||
│ │ { url: '...', │ │
|
||||
│ │ id: 123, │ │
|
||||
│ │ name: '...' } │ │
|
||||
│ │<─────────────────────│ │
|
||||
│ │ │ │
|
||||
│ 15. 显示上传成功 │ │ │
|
||||
│<──────────────────│ │ │
|
||||
│ │ │ │
|
||||
```
|
||||
|
||||
## 区域映射关系
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ getQiniuRegion() 函数 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
数据库配置 函数输入 函数输出 上传地址
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
|
||||
│ qiniu_ │ │ │ │ │ │ │
|
||||
│ region: │ ───> │ 'z0' │ ───> │ qiniu. │ ───> │ up-z0.qiniup.com │
|
||||
│ 'z0' │ │ │ │ region.z0│ │ (华东) │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────────────┘
|
||||
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
|
||||
│ qiniu_ │ │ │ │ │ │ │
|
||||
│ region: │ ───> │ 'z1' │ ───> │ qiniu. │ ───> │ up-z1.qiniup.com │
|
||||
│ 'z1' │ │ │ │ region.z1│ │ (华北) │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────────────┘
|
||||
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
|
||||
│ qiniu_ │ │ │ │ │ │ │
|
||||
│ region: │ ───> │ 'z2' │ ───> │ qiniu. │ ───> │ up-z2.qiniup.com │
|
||||
│ 'z2' │ │ │ │ region.z2│ │ (华南) ✓ 当前 │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────────────┘
|
||||
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
|
||||
│ qiniu_ │ │ │ │ │ │ │
|
||||
│ region: │ ───> │ 'na0' │ ───> │ qiniu. │ ───> │ up-na0.qiniup.com│
|
||||
│ 'na0' │ │ │ │ region. │ │ (北美) │
|
||||
│ │ │ │ │ na0 │ │ │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────────────┘
|
||||
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────┐
|
||||
│ qiniu_ │ │ │ │ │ │ │
|
||||
│ region: │ ───> │ 'as0' │ ───> │ qiniu. │ ───> │ up-as0.qiniup.com│
|
||||
│ 'as0' │ │ │ │ region. │ │ (新加坡) │
|
||||
│ │ │ │ │ as0 │ │ │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────────────┘
|
||||
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────────────────┐
|
||||
│ qiniu_ │ │ │ │ │ │ │
|
||||
│ region: │ ───> │'cn-east-2'──────>│ qiniu. │ ───> │up-cn-east-2.qiniup. │
|
||||
│'cn-east-2' │ │ │ region. │ │com (华东-浙江2) │
|
||||
│ │ │ │ │ cnEast2 │ │ │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
## 代码对比
|
||||
|
||||
### 修复前(硬编码)
|
||||
|
||||
```javascript
|
||||
// ❌ 错误:硬编码区域
|
||||
const config = {
|
||||
useCdnDomain: true,
|
||||
region: qiniu.region.z0, // 总是使用华东
|
||||
};
|
||||
```
|
||||
|
||||
### 修复后(动态配置)
|
||||
|
||||
```javascript
|
||||
// ✓ 正确:动态获取区域
|
||||
const { region } = tokenRes.data; // 从后端获取: 'z2'
|
||||
const qiniuRegion = getQiniuRegion(region); // 转换为: qiniu.region.z2
|
||||
|
||||
const config = {
|
||||
useCdnDomain: true,
|
||||
region: qiniuRegion, // 使用正确的区域
|
||||
};
|
||||
```
|
||||
|
||||
## 优势对比
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 硬编码方式 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
缺点:
|
||||
❌ 区域固定,无法灵活切换
|
||||
❌ 修改区域需要改代码、重新部署
|
||||
❌ 容易出现区域不匹配错误
|
||||
❌ 不同环境需要不同的代码版本
|
||||
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 动态配置方式 │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
|
||||
优点:
|
||||
✓ 区域灵活,可随时切换
|
||||
✓ 修改区域只需改数据库配置
|
||||
✓ 自动使用正确的区域
|
||||
✓ 所有环境使用同一套代码
|
||||
✓ 支持多租户不同区域配置
|
||||
```
|
||||
|
||||
## 更新日期
|
||||
|
||||
2026-04-09
|
||||
@@ -0,0 +1,443 @@
|
||||
# 七牛云直传配置说明
|
||||
|
||||
## 概述
|
||||
|
||||
新的上传机制实现了前端直接上传到七牛云,不再通过后端中转,大幅提升大文件上传效率。
|
||||
|
||||
## 上传流程对比
|
||||
|
||||
### 旧流程(低效)
|
||||
```
|
||||
前端 → 后端服务器 → 七牛云
|
||||
(中转暂存)
|
||||
```
|
||||
|
||||
问题:
|
||||
- 大文件需要先上传到服务器,再由服务器上传到七牛云
|
||||
- 占用服务器带宽和磁盘空间
|
||||
- 上传时间翻倍
|
||||
- 服务器压力大
|
||||
|
||||
### 新流程(高效)
|
||||
```
|
||||
前端 → 七牛云(直传)
|
||||
后端 → 数据库(仅保存记录)
|
||||
```
|
||||
|
||||
优势:
|
||||
- 前端直接上传到七牛云,不经过服务器
|
||||
- 节省服务器资源
|
||||
- 上传速度快
|
||||
- 支持断点续传
|
||||
|
||||
## 安装依赖
|
||||
|
||||
### 前端安装七牛云 SDK
|
||||
|
||||
```bash
|
||||
cd platform
|
||||
npm install qiniu-js
|
||||
```
|
||||
|
||||
或使用 yarn:
|
||||
|
||||
```bash
|
||||
yarn add qiniu-js
|
||||
```
|
||||
|
||||
## 后端 API
|
||||
|
||||
### 1. 获取存储配置
|
||||
|
||||
**接口**: `GET /platform/storage/config`
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"storageType": "qiniu", // 或 "local"
|
||||
"qiniuDomain": "http://7colud.yunzer.cn",
|
||||
"qiniuRegion": "z0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取上传凭证
|
||||
|
||||
**接口**: `GET /platform/qiniu/token`
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"token": "七牛云上传token",
|
||||
"domain": "http://7colud.yunzer.cn",
|
||||
"bucket": "your-bucket",
|
||||
"region": "z0",
|
||||
"keyPrefix": "2026/04/09/1775722615052606500",
|
||||
"expires": 1712654400,
|
||||
"uploadUrl": "https://up-z0.qiniup.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 保存文件记录
|
||||
|
||||
**接口**: `POST /platform/qiniu/save`
|
||||
|
||||
**请求**:
|
||||
```json
|
||||
{
|
||||
"key": "2026/04/09/1775722615052606500.png",
|
||||
"hash": "FhGxwBzoLwO_RGws...",
|
||||
"size": 1024000,
|
||||
"name": "screenshot.png",
|
||||
"mimeType": "image/png",
|
||||
"cate": 0
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"url": "http://7colud.yunzer.cn/2026/04/09/1775722615052606500.png",
|
||||
"id": 123,
|
||||
"name": "screenshot.png",
|
||||
"key": "2026/04/09/1775722615052606500.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 前端使用
|
||||
|
||||
### 基础用法
|
||||
|
||||
```javascript
|
||||
import { smartUpload } from '@/utils/qiniuUpload';
|
||||
|
||||
// 自动选择上传方式(本地或七牛云)
|
||||
const result = await smartUpload(file, {
|
||||
cate: 0, // 文件分类
|
||||
onProgress: (progress) => {
|
||||
console.log('上传进度:', progress.percent + '%');
|
||||
console.log('已上传:', progress.loaded);
|
||||
console.log('总大小:', progress.total);
|
||||
},
|
||||
});
|
||||
|
||||
console.log('上传成功:', result);
|
||||
// { url: '...', id: 123, name: '...', key: '...' }
|
||||
```
|
||||
|
||||
### 在组件中使用
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<el-upload
|
||||
:http-request="handleUpload"
|
||||
:on-progress="handleProgress"
|
||||
>
|
||||
<el-button>上传文件</el-button>
|
||||
</el-upload>
|
||||
<el-progress v-if="uploading" :percentage="uploadPercent" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { smartUpload } from '@/utils/qiniuUpload';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
const uploading = ref(false);
|
||||
const uploadPercent = ref(0);
|
||||
|
||||
async function handleUpload(options) {
|
||||
const file = options.file?.raw || options.file;
|
||||
uploading.value = true;
|
||||
|
||||
try {
|
||||
const result = await smartUpload(file, {
|
||||
cate: 0,
|
||||
onProgress: (progress) => {
|
||||
uploadPercent.value = progress.percent;
|
||||
},
|
||||
});
|
||||
|
||||
ElMessage.success('上传成功');
|
||||
options.onSuccess?.(result);
|
||||
} catch (error) {
|
||||
ElMessage.error(error.message || '上传失败');
|
||||
options.onError?.(error);
|
||||
} finally {
|
||||
uploading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
```
|
||||
|
||||
### 批量上传
|
||||
|
||||
```javascript
|
||||
import { batchUpload } from '@/utils/qiniuUpload';
|
||||
|
||||
const files = [file1, file2, file3];
|
||||
|
||||
const results = await batchUpload(files, {
|
||||
cate: 0,
|
||||
onFileProgress: (file, progress) => {
|
||||
console.log(`${file.name}: ${progress.percent}%`);
|
||||
},
|
||||
onFileComplete: (file, result) => {
|
||||
console.log(`${file.name} 上传成功:`, result);
|
||||
},
|
||||
onFileError: (file, error) => {
|
||||
console.error(`${file.name} 上传失败:`, error);
|
||||
},
|
||||
});
|
||||
|
||||
console.log('所有文件上传完成:', results);
|
||||
```
|
||||
|
||||
## 工作原理
|
||||
|
||||
### 1. 智能选择上传方式
|
||||
|
||||
`smartUpload` 函数会自动检测后端配置:
|
||||
|
||||
```javascript
|
||||
// 1. 获取存储配置
|
||||
const config = await getStorageConfig();
|
||||
|
||||
// 2. 根据配置选择上传方式
|
||||
if (config.storageType === 'qiniu') {
|
||||
// 七牛云直传
|
||||
return uploadToQiniu(file, options);
|
||||
} else {
|
||||
// 本地上传(通过后端)
|
||||
return uploadToLocal(file, options);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 七牛云直传流程
|
||||
|
||||
```javascript
|
||||
// 1. 获取上传凭证
|
||||
const tokenRes = await getQiniuToken();
|
||||
const { token, keyPrefix } = tokenRes.data;
|
||||
|
||||
// 2. 生成文件 key
|
||||
const key = `${keyPrefix}.${ext}`;
|
||||
|
||||
// 3. 使用七牛云 SDK 直接上传
|
||||
const observable = qiniu.upload(file, key, token);
|
||||
|
||||
// 4. 监听上传进度
|
||||
observable.subscribe({
|
||||
next(res) {
|
||||
// 进度回调
|
||||
onProgress(res.total.percent);
|
||||
},
|
||||
complete(res) {
|
||||
// 上传完成,保存记录到数据库
|
||||
await saveFileRecord({
|
||||
key: res.key,
|
||||
hash: res.hash,
|
||||
size: file.size,
|
||||
name: file.name,
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 本地上传流程
|
||||
|
||||
```javascript
|
||||
// 通过后端中转(兼容本地存储)
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const res = await request({
|
||||
url: '/platform/uploadfile',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
onUploadProgress: (e) => {
|
||||
onProgress(e.loaded / e.total * 100);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 七牛云区域配置
|
||||
|
||||
| 区域代码 | 区域名称 | 上传地址 |
|
||||
|---------|---------|---------|
|
||||
| z0 | 华东 | https://up-z0.qiniup.com |
|
||||
| z1 | 华北 | https://up-z1.qiniup.com |
|
||||
| z2 | 华南 | https://up-z2.qiniup.com |
|
||||
| na0 | 北美 | https://up-na0.qiniup.com |
|
||||
| as0 | 新加坡 | https://up-as0.qiniup.com |
|
||||
| cn-east-2 | 华东-浙江2 | https://up-cn-east-2.qiniup.com |
|
||||
|
||||
### 上传策略配置
|
||||
|
||||
后端生成 token 时的策略:
|
||||
|
||||
```go
|
||||
putPolicy := storage.PutPolicy{
|
||||
Scope: cfg.QiniuBucket,
|
||||
ReturnBody: `{"key":"$(key)","hash":"$(etag)","size":$(fsize),"mimeType":"$(mimeType)"}`,
|
||||
Expires: 3600, // 1小时有效期
|
||||
}
|
||||
```
|
||||
|
||||
## 安全性
|
||||
|
||||
### 1. Token 有效期
|
||||
|
||||
上传 token 有效期为 1 小时,过期后需要重新获取。
|
||||
|
||||
### 2. 权限验证
|
||||
|
||||
- 获取 token 需要登录认证
|
||||
- 保存文件记录需要登录认证
|
||||
- 文件记录关联到当前用户和租户
|
||||
|
||||
### 3. 文件去重
|
||||
|
||||
通过 MD5 检查文件是否已存在,避免重复上传。
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 断点续传
|
||||
|
||||
七牛云 SDK 支持断点续传(大文件自动分片):
|
||||
|
||||
```javascript
|
||||
const config = {
|
||||
useCdnDomain: true,
|
||||
region: qiniu.region.z0,
|
||||
chunkSize: 4, // 分片大小(MB)
|
||||
concurrentRequestLimit: 3, // 并发上传数
|
||||
};
|
||||
|
||||
const observable = qiniu.upload(file, key, token, putExtra, config);
|
||||
```
|
||||
|
||||
### 2. CDN 加速
|
||||
|
||||
启用 CDN 域名加速上传:
|
||||
|
||||
```javascript
|
||||
const config = {
|
||||
useCdnDomain: true, // 使用 CDN 加速
|
||||
};
|
||||
```
|
||||
|
||||
### 3. 并发上传
|
||||
|
||||
批量上传时可以控制并发数:
|
||||
|
||||
```javascript
|
||||
// 限制同时上传 3 个文件
|
||||
const concurrency = 3;
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < files.length; i += concurrency) {
|
||||
const batch = files.slice(i, i + concurrency);
|
||||
const batchResults = await Promise.all(
|
||||
batch.map(file => smartUpload(file, options))
|
||||
);
|
||||
results.push(...batchResults);
|
||||
}
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 1. 上传失败:获取 token 失败
|
||||
|
||||
**错误**: "当前未配置七牛云存储"
|
||||
|
||||
**解决**:
|
||||
- 检查数据库存储配置
|
||||
- 确认 `storage_type` 为 `'qiniu'`
|
||||
- 确认七牛云配置完整
|
||||
|
||||
### 2. 上传失败:token 无效
|
||||
|
||||
**错误**: "401 Unauthorized"
|
||||
|
||||
**解决**:
|
||||
- 检查 AccessKey 和 SecretKey 是否正确
|
||||
- 检查 token 是否过期(1小时有效期)
|
||||
- 重新获取 token
|
||||
|
||||
### 3. 上传失败:bucket 不存在
|
||||
|
||||
**错误**: "no such bucket"
|
||||
|
||||
**解决**:
|
||||
- 检查 bucket 名称是否正确
|
||||
- 检查 bucket 是否在对应区域
|
||||
- 登录七牛云控制台确认
|
||||
|
||||
### 4. 保存记录失败
|
||||
|
||||
**错误**: "保存文件记录失败"
|
||||
|
||||
**解决**:
|
||||
- 检查数据库连接
|
||||
- 检查文件信息是否完整
|
||||
- 查看后端日志
|
||||
|
||||
## 迁移指南
|
||||
|
||||
### 从旧版本迁移
|
||||
|
||||
1. **安装依赖**:
|
||||
```bash
|
||||
npm install qiniu-js
|
||||
```
|
||||
|
||||
2. **更新导入**:
|
||||
```javascript
|
||||
// 旧版本
|
||||
import { uploadFile } from '@/api/file';
|
||||
|
||||
// 新版本
|
||||
import { smartUpload } from '@/utils/qiniuUpload';
|
||||
```
|
||||
|
||||
3. **更新上传代码**:
|
||||
```javascript
|
||||
// 旧版本
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await uploadFile(formData, { cate: 0 });
|
||||
|
||||
// 新版本
|
||||
const result = await smartUpload(file, { cate: 0 });
|
||||
```
|
||||
|
||||
4. **兼容性**:
|
||||
- `smartUpload` 会自动检测存储配置
|
||||
- 如果配置为本地存储,会自动使用旧的上传方式
|
||||
- 无需修改其他代码
|
||||
|
||||
## 相关文件
|
||||
|
||||
### 后端
|
||||
- `go/controllers/qiniu_upload.go` - 七牛云上传控制器
|
||||
- `go/routers/platform/platform.go` - 路由配置
|
||||
- `go/services/storage_service.go` - 存储服务
|
||||
|
||||
### 前端
|
||||
- `platform/src/utils/qiniuUpload.js` - 七牛云上传工具
|
||||
- `platform/src/views/platform/softwareupgrade/components/edit.vue` - 软件升级组件(示例)
|
||||
|
||||
## 更新日期
|
||||
|
||||
2026-04-09
|
||||
@@ -0,0 +1,101 @@
|
||||
# 图片URL处理说明
|
||||
|
||||
## 问题描述
|
||||
|
||||
在使用七牛云存储时,上传的图片URL会出现重复拼接的问题:
|
||||
|
||||
```
|
||||
错误: http://localhost:8081http://7cloud.yunzer.cn/2026/04/09/xxx.png
|
||||
正确: http://7cloud.yunzer.cn/2026/04/09/xxx.png
|
||||
```
|
||||
|
||||
## 原因分析
|
||||
|
||||
1. **本地存储**返回的URL是相对路径:`/uploads/2026/04/09/xxx.png`
|
||||
2. **七牛云存储**返回的URL是完整URL:`http://7cloud.yunzer.cn/2026/04/09/xxx.png`
|
||||
3. 前端的 `getFileUrl` 方法会自动拼接 `VITE_API_BASE_URL`
|
||||
4. 导致七牛云URL被重复拼接
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 1. 创建通用工具函数
|
||||
|
||||
文件:`platform/src/utils/url.js`
|
||||
|
||||
```javascript
|
||||
export function getFileUrl(url) {
|
||||
if (!url) return '';
|
||||
|
||||
// 如果URL已经是完整的URL,直接返回
|
||||
if (url.startsWith('http://') || url.startsWith('https://')) {
|
||||
return url;
|
||||
}
|
||||
|
||||
// 否则拼接API基础URL
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '';
|
||||
return `${API_BASE_URL}${url}`;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 在组件中使用
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
import { getFileUrl } from '@/utils/url';
|
||||
|
||||
// 使用工具函数
|
||||
const imageUrl = getFileUrl(file.url);
|
||||
</script>
|
||||
```
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 本地存储
|
||||
|
||||
```javascript
|
||||
const url = '/uploads/2026/04/09/xxx.png';
|
||||
const fullUrl = getFileUrl(url);
|
||||
// 结果: http://localhost:8081/uploads/2026/04/09/xxx.png
|
||||
```
|
||||
|
||||
### 七牛云存储
|
||||
|
||||
```javascript
|
||||
const url = 'http://7cloud.yunzer.cn/2026/04/09/xxx.png';
|
||||
const fullUrl = getFileUrl(url);
|
||||
// 结果: http://7cloud.yunzer.cn/2026/04/09/xxx.png
|
||||
```
|
||||
|
||||
## 需要修改的文件
|
||||
|
||||
所有使用 `getFileUrl` 或 `getEnvUrl` 的组件都应该使用统一的工具函数:
|
||||
|
||||
- ✅ `platform/src/views/system/fileManager/index.vue`
|
||||
- ⏳ `platform/src/views/moduleshop/center/index.vue`
|
||||
- ⏳ `platform/src/views/apps/babyhealth/babys/index.vue`
|
||||
- ⏳ `platform/src/views/apps/babyhealth/babys/components/edit.vue`
|
||||
- ⏳ 其他使用图片URL的组件
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **统一使用工具函数**:不要在组件中重复定义 `getFileUrl`
|
||||
2. **判断完整URL**:始终检查URL是否已经是完整URL
|
||||
3. **兼容两种存储**:确保本地存储和七牛云存储都能正常工作
|
||||
|
||||
## 测试清单
|
||||
|
||||
- [x] 本地存储图片显示正常
|
||||
- [x] 七牛云存储图片显示正常
|
||||
- [x] 图片预览功能正常
|
||||
- [ ] 视频文件显示正常
|
||||
- [ ] 文档下载功能正常
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [存储配置功能](../go/docs/README_STORAGE.md)
|
||||
- [七牛云配置指南](../go/docs/storage-config-guide.md)
|
||||
|
||||
---
|
||||
|
||||
**修复时间**: 2024-01-01
|
||||
**修复人员**: AI Assistant
|
||||
@@ -0,0 +1,11 @@
|
||||
//拼接接口路径
|
||||
const getEnvUrl = (path: string) => {
|
||||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL;
|
||||
return `${API_BASE_URL}${path}`;
|
||||
};
|
||||
|
||||
用例:
|
||||
<img :src="getEnvUrl(module.thumb)" :alt="module.title" />
|
||||
|
||||
|
||||
const url = getEnvUrl('/platform/moduleCenter/modules');
|
||||
@@ -0,0 +1,12 @@
|
||||
import { onMounted } from "vue";
|
||||
import { getMenus } from "@/api/menu";
|
||||
|
||||
onMounted(async () => {
|
||||
try{
|
||||
const response = await getMenus();
|
||||
} catch (error) {
|
||||
console.error('获取菜单数据失败:', error);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
# 文件上传超时配置说明
|
||||
|
||||
## 修改内容
|
||||
|
||||
移除了文件上传的超时限制,并增加了文件大小限制,允许大文件(如软件安装包)上传完成而不会因超时或大小限制中断。
|
||||
|
||||
## 前端修改
|
||||
|
||||
### 修改位置
|
||||
`platform/src/api/file.js` 中的 `uploadFile` 函数
|
||||
|
||||
### 修改详情
|
||||
|
||||
#### 修改前
|
||||
```javascript
|
||||
const config = {
|
||||
url: "/platform/uploadfile",
|
||||
method: "post",
|
||||
data: formData,
|
||||
timeout: 2 * 60 * 60 * 1000, // 2小时超时
|
||||
};
|
||||
```
|
||||
|
||||
#### 修改后
|
||||
```javascript
|
||||
const config = {
|
||||
url: "/platform/uploadfile",
|
||||
method: "post",
|
||||
data: formData,
|
||||
timeout: 0, // 不设置超时时间,等待文件上传完毕
|
||||
};
|
||||
```
|
||||
|
||||
## 后端修改
|
||||
|
||||
### 1. 文件大小限制调整
|
||||
|
||||
#### 修改位置
|
||||
`go/controllers/platform_file.go`
|
||||
|
||||
#### 修改详情
|
||||
|
||||
##### 修改前
|
||||
```go
|
||||
const fileUploadMaxMB = 200 // 200MB
|
||||
const fileUploadMaxBytes = fileUploadMaxMB * 1024 * 1024
|
||||
```
|
||||
|
||||
##### 修改后
|
||||
```go
|
||||
const fileUploadMaxMB = 2048 // 2GB,适用于大型软件安装包
|
||||
const fileUploadMaxBytes = fileUploadMaxMB * 1024 * 1024
|
||||
```
|
||||
|
||||
### 2. 服务器超时配置
|
||||
|
||||
#### 修改位置
|
||||
`go/conf/app.conf`
|
||||
|
||||
#### 新增配置
|
||||
```ini
|
||||
# 服务器超时配置(支持大文件上传)
|
||||
# 0 表示不设置超时限制
|
||||
ServerTimeOut = 0
|
||||
# 最大请求体大小(字节),0 表示不限制
|
||||
MaxMemory = 0
|
||||
```
|
||||
|
||||
## 影响范围
|
||||
|
||||
此修改影响所有使用 `uploadFile` 函数的文件上传功能,包括但不限于:
|
||||
|
||||
- 软件升级安装包上传 (`platform/src/views/platform/softwareupgrade/components/edit.vue`)
|
||||
- 文件管理器上传 (`platform/src/views/system/fileManager/index.vue`)
|
||||
- 其他调用 `uploadFile` API 的组件
|
||||
|
||||
## 技术说明
|
||||
|
||||
### 前端
|
||||
- `timeout: 0` 在 axios 中表示不设置超时限制
|
||||
- 上传进度仍然会正常显示(通过 `onUploadProgress` 回调)
|
||||
- 用户可以随时取消上传操作
|
||||
|
||||
### 后端
|
||||
- 文件大小限制从 200MB 提升到 2GB
|
||||
- `ServerTimeOut = 0` 表示服务器不设置请求超时
|
||||
- `MaxMemory = 0` 表示不限制请求体大小
|
||||
- 适用于大文件(如软件安装包)的上传场景
|
||||
|
||||
## CORS 问题说明
|
||||
|
||||
如果遇到 CORS 错误,请确认:
|
||||
|
||||
1. 后端 `go/routers/router.go` 中已配置 CORS 中间件
|
||||
2. 允许的请求头包含 `Authorization`
|
||||
3. 生产环境中,需要将 `Access-Control-Allow-Origin` 设置为具体的前端域名
|
||||
|
||||
当前配置:
|
||||
```go
|
||||
ctx.Output.Header("Access-Control-Allow-Origin", "*")
|
||||
ctx.Output.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS")
|
||||
ctx.Output.Header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization")
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 修改后需要重启 Go 后端服务才能生效
|
||||
2. 对于超大文件(>2GB),建议考虑使用分片上传或断点续传机制
|
||||
3. 生产环境建议配置 Nginx 等反向代理的超时和大小限制
|
||||
4. 监控服务器磁盘空间,确保有足够空间存储大文件
|
||||
|
||||
## 相关文件
|
||||
|
||||
### 前端
|
||||
- `platform/src/api/file.js` - 文件上传 API
|
||||
- `platform/src/utils/request.js` - axios 请求配置
|
||||
- `platform/src/views/platform/softwareupgrade/components/edit.vue` - 软件升级上传组件
|
||||
|
||||
### 后端
|
||||
- `go/controllers/platform_file.go` - 文件上传控制器
|
||||
- `go/conf/app.conf` - 服务器配置
|
||||
- `go/routers/router.go` - CORS 配置
|
||||
|
||||
## 修改日期
|
||||
|
||||
2026-04-09
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { onMounted } from 'vue';
|
||||
|
||||
// 使用 auth store 获取用户信息
|
||||
const authStore = useAuthStore();
|
||||
|
||||
// 获取租户ID
|
||||
const tenantId = (authStore.user as any)?.tid;
|
||||
|
||||
// 获取用户信息
|
||||
const userInfo = authStore.user;
|
||||
if (userInfo && userInfo.id) {
|
||||
console.log('用户名:', userInfo.username || userInfo.nickname);
|
||||
console.log('用户ID:', userInfo.id);
|
||||
console.log('角色:', userInfo.role);
|
||||
} else {
|
||||
console.log('未找到用户信息或用户未登录');
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserInfo();
|
||||
});
|
||||
@@ -0,0 +1,90 @@
|
||||
<el-form-item label="默认图片" prop="image">
|
||||
<div class="uploads">
|
||||
<el-upload v-model:file-list="fileList" :action="uploadUrl" list-type="picture-card"
|
||||
:headers="uploadHeaders" :limit="1" :auto-upload="false" :before-upload="beforeImgUpload"
|
||||
:on-success="handleImgSuccess">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
|
||||
<template #file="{ file }">
|
||||
<div>
|
||||
<img class="el-upload-list__item-thumbnail" :src="file.url" alt="" />
|
||||
<span class="el-upload-list__item-actions">
|
||||
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||
<el-icon>
|
||||
<ZoomIn />
|
||||
</el-icon>
|
||||
</span>
|
||||
<span class="el-upload-list__item-delete" @click="handleRemove(file)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
<el-dialog v-model="dialogVisible">
|
||||
<img w-full :src="dialogImageUrl" alt="Preview Image" />
|
||||
</el-dialog>
|
||||
|
||||
<div class="upload-tip">
|
||||
<span>建议尺寸:250px × 140px</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
import { uploadFile } from '@/api/file.js';
|
||||
import { ElMessage, ElUpload } from 'element-plus'
|
||||
|
||||
// 上传相关
|
||||
const fileList = ref<any[]>([])
|
||||
const dialogVisible = ref(false)
|
||||
const dialogImageUrl = ref('')
|
||||
|
||||
function beforeImgUpload(file: File) {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
if (!isImage) ElMessage.error('仅支持图片格式')
|
||||
if (!isLt10M) ElMessage.error('图片大小不能超过10MB')
|
||||
return isImage && isLt10M
|
||||
}
|
||||
|
||||
function handleImgUpload(file: File) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
formData.append('cate', 'article')
|
||||
|
||||
uploadFile(formData).then((res: any) => {
|
||||
if (res?.url) {
|
||||
formData.image = res.url
|
||||
fileList.value = [{
|
||||
name: file.name,
|
||||
url: res.url
|
||||
}]
|
||||
}
|
||||
}).catch((error: any) => {
|
||||
ElMessage.error('上传失败:' + (error.msg || '未知错误'))
|
||||
})
|
||||
}
|
||||
|
||||
function handlePictureCardPreview(file: any) {
|
||||
dialogImageUrl.value = file.url
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function handleRemove(file: any) {
|
||||
fileList.value = []
|
||||
formData.image = ''
|
||||
}
|
||||
|
||||
|
||||
.uploads{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.upload-tip {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
````
|
||||
<template>
|
||||
{{ (xxxxxxxxDict.find(item => item.dict_value == String(model?.status)) || {}).dict_label || '-' }}
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { onMounted } from 'vue';
|
||||
import { useDictStore } from '@/stores/dict';
|
||||
|
||||
// 字典store
|
||||
const dictStore = useDictStore();
|
||||
const xxxxxxxxDict = ref<any[]>([]);
|
||||
|
||||
// 获取审核状态字典
|
||||
const fetchxxxxxxxxDict = async () => {
|
||||
try {
|
||||
xxxxxxxxDict.value = await dictStore.getDictItems('article_status');
|
||||
console.log(xxxxxxxxDict.value);
|
||||
} catch (err) {
|
||||
console.error('获取文章状态字典失败:', err);
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchxxxxxxxxDict();
|
||||
});
|
||||
</script>
|
||||
````
|
||||
Reference in New Issue
Block a user