feat: 将截图存储方式从文件路径更改为Base64编码的JSON数组,并更新数据库列类型为LONGTEXT以支持此变更。
This commit is contained in:
parent
f9c03cab8f
commit
feed4f84c8
@ -345,12 +345,9 @@ func main() {
|
||||
os.MkdirAll(staticDir, 0755)
|
||||
}
|
||||
|
||||
// Serve static files
|
||||
// Serve static files (包括上传的文件)
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./frontend"))))
|
||||
|
||||
// Serve uploaded files
|
||||
http.Handle("/static/uploads/", http.StripPrefix("/static/uploads/", http.FileServer(http.Dir("./frontend/uploads"))))
|
||||
|
||||
// Serve index page
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, filepath.Join("./frontend", "index.html"))
|
||||
|
||||
250
docs/screenshot-bug-fix.md
Normal file
250
docs/screenshot-bug-fix.md
Normal file
@ -0,0 +1,250 @@
|
||||
# 截图存储 Bug 修复说明
|
||||
|
||||
## 问题描述
|
||||
|
||||
在实施 Base64 截图存储方案后,发现了一个严重的 Bug:
|
||||
|
||||
```
|
||||
错误信息: GET data:image/jpeg;base64:1 net::ERR_INVALID_URL
|
||||
```
|
||||
|
||||
## 根本原因
|
||||
|
||||
### 问题分析
|
||||
|
||||
1. **数据存储方式**:使用逗号分隔的字符串存储多个截图
|
||||
|
||||
```go
|
||||
screenshots := strings.Join(customer.Screenshots, ",")
|
||||
```
|
||||
|
||||
2. **数据读取方式**:使用逗号分割字符串
|
||||
|
||||
```go
|
||||
c.Screenshots = strings.Split(screenshots.String, ",")
|
||||
```
|
||||
|
||||
3. **Base64 数据特点**:Base64 编码的字符串**本身不包含逗号**,但是...
|
||||
|
||||
4. **Data URL 格式**:`data:image/jpeg;base64,{base64_string}`
|
||||
- 注意:**逗号**是 Data URL 格式的一部分!
|
||||
- 格式:`data:{MIME类型};base64,{Base64数据}`
|
||||
|
||||
### 问题示例
|
||||
|
||||
假设有两张截图:
|
||||
|
||||
**原始数据**:
|
||||
|
||||
```json
|
||||
[
|
||||
"data:image/jpeg;base64,/9j/4AAQSkZJRg...",
|
||||
"data:image/png;base64,iVBORw0KGgoAAAA..."
|
||||
]
|
||||
```
|
||||
|
||||
**存储到数据库**(使用逗号连接):
|
||||
|
||||
```
|
||||
data:image/jpeg;base64,/9j/4AAQSkZJRg...,data:image/png;base64,iVBORw0KGgoAAAA...
|
||||
```
|
||||
|
||||
**从数据库读取**(使用逗号分割):
|
||||
|
||||
```json
|
||||
[
|
||||
"data:image/jpeg;base64", // ❌ 被截断!
|
||||
"/9j/4AAQSkZJRg...", // ❌ 不是有效的 Data URL
|
||||
"data:image/png;base64", // ❌ 被截断!
|
||||
"iVBORw0KGgoAAAA..." // ❌ 不是有效的 Data URL
|
||||
]
|
||||
```
|
||||
|
||||
**结果**:前端尝试加载 `data:image/jpeg;base64` 导致 `ERR_INVALID_URL` 错误!
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 修改存储格式
|
||||
|
||||
从**逗号分隔字符串**改为 **JSON 数组**:
|
||||
|
||||
#### 修改前(错误)
|
||||
|
||||
```go
|
||||
// 存储
|
||||
screenshots := strings.Join(customer.Screenshots, ",")
|
||||
|
||||
// 读取
|
||||
c.Screenshots = strings.Split(screenshots.String, ",")
|
||||
```
|
||||
|
||||
#### 修改后(正确)
|
||||
|
||||
```go
|
||||
// 存储
|
||||
screenshotsJSON, err := json.Marshal(customer.Screenshots)
|
||||
// 结果: ["data:image/jpeg;base64,...","data:image/png;base64,..."]
|
||||
|
||||
// 读取
|
||||
var screenshotArray []string
|
||||
json.Unmarshal([]byte(screenshots.String), &screenshotArray)
|
||||
```
|
||||
|
||||
### 向后兼容
|
||||
|
||||
为了兼容旧数据(文件路径格式),添加了降级处理:
|
||||
|
||||
```go
|
||||
if screenshots.Valid && screenshots.String != "" {
|
||||
// 尝试解析为 JSON 数组
|
||||
var screenshotArray []string
|
||||
if err := json.Unmarshal([]byte(screenshots.String), &screenshotArray); err == nil {
|
||||
c.Screenshots = screenshotArray
|
||||
} else {
|
||||
// 向后兼容:如果不是 JSON,尝试逗号分隔(旧格式)
|
||||
c.Screenshots = strings.Split(screenshots.String, ",")
|
||||
}
|
||||
} else {
|
||||
c.Screenshots = []string{}
|
||||
}
|
||||
```
|
||||
|
||||
## 修改文件清单
|
||||
|
||||
### `/internal/storage/mysql_customer_storage.go`
|
||||
|
||||
1. **添加导入**
|
||||
|
||||
```go
|
||||
import "encoding/json"
|
||||
```
|
||||
|
||||
2. **修改 `GetAllCustomers` 方法**
|
||||
- 使用 `json.Unmarshal` 解析 screenshots 字段
|
||||
- 添加向后兼容逻辑
|
||||
|
||||
3. **修改 `GetCustomerByID` 方法**
|
||||
- 使用 `json.Unmarshal` 解析 screenshots 字段
|
||||
- 添加向后兼容逻辑
|
||||
|
||||
4. **修改 `CreateCustomer` 方法**
|
||||
- 使用 `json.Marshal` 序列化 screenshots 数组
|
||||
|
||||
5. **修改 `UpdateCustomer` 方法**
|
||||
- 使用 `json.Marshal` 序列化 screenshots 数组
|
||||
|
||||
## 数据格式对比
|
||||
|
||||
### 旧格式(文件路径 - 使用逗号分隔)
|
||||
|
||||
```
|
||||
数据库存储: /static/uploads/1.jpg,/static/uploads/2.png
|
||||
解析结果: ["/static/uploads/1.jpg", "/static/uploads/2.png"]
|
||||
状态: ✅ 正常(路径中没有逗号)
|
||||
```
|
||||
|
||||
### 错误格式(Base64 - 使用逗号分隔)
|
||||
|
||||
```
|
||||
数据库存储: data:image/jpeg;base64,abc...,data:image/png;base64,xyz...
|
||||
解析结果: ["data:image/jpeg;base64", "abc...", "data:image/png;base64", "xyz..."]
|
||||
状态: ❌ 错误(Data URL 被逗号截断)
|
||||
```
|
||||
|
||||
### 新格式(JSON 数组)
|
||||
|
||||
```
|
||||
数据库存储: ["data:image/jpeg;base64,abc...","data:image/png;base64,xyz..."]
|
||||
解析结果: ["data:image/jpeg;base64,abc...", "data:image/png;base64,xyz..."]
|
||||
状态: ✅ 正常(完整的 Data URL)
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 1. 单元测试数据
|
||||
|
||||
```go
|
||||
// 测试 JSON 序列化
|
||||
screenshots := []string{
|
||||
"data:image/jpeg;base64,/9j/4AAQSkZJRg...",
|
||||
"data:image/png;base64,iVBORw0KGgoAAAA...",
|
||||
}
|
||||
|
||||
json, _ := json.Marshal(screenshots)
|
||||
fmt.Println(string(json))
|
||||
// 输出: ["data:image/jpeg;base64,/9j/4AAQSkZJRg...","data:image/png;base64,iVBORw0KGgoAAAA..."]
|
||||
|
||||
// 测试 JSON 反序列化
|
||||
var result []string
|
||||
json.Unmarshal(json, &result)
|
||||
fmt.Println(result)
|
||||
// 输出: [data:image/jpeg;base64,/9j/4AAQSkZJRg... data:image/png;base64,iVBORw0KGgoAAAA...]
|
||||
```
|
||||
|
||||
### 2. 集成测试
|
||||
|
||||
```bash
|
||||
# 1. 上传截图
|
||||
curl -X POST http://localhost:8081/api/upload \
|
||||
-F "screenshots=@test.jpg"
|
||||
|
||||
# 2. 创建客户
|
||||
curl -X POST http://localhost:8081/api/customers \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"customerName": "测试",
|
||||
"screenshots": ["data:image/jpeg;base64,/9j/4AAQ..."]
|
||||
}'
|
||||
|
||||
# 3. 查询客户
|
||||
curl http://localhost:8081/api/customers/{id}
|
||||
|
||||
# 4. 验证数据库
|
||||
mysql> SELECT screenshots FROM customers WHERE id = '{id}';
|
||||
# 应该看到: ["data:image/jpeg;base64,/9j/4AAQ..."]
|
||||
```
|
||||
|
||||
## 经验教训
|
||||
|
||||
### 1. 分隔符选择的重要性
|
||||
|
||||
当使用分隔符连接字符串时,必须确保:
|
||||
|
||||
- ✅ 分隔符不会出现在数据中
|
||||
- ✅ 或者使用转义机制
|
||||
- ✅ 或者使用结构化格式(如 JSON)
|
||||
|
||||
### 2. Data URL 格式
|
||||
|
||||
```
|
||||
data:[<mediatype>][;base64],<data>
|
||||
^ ^ ^
|
||||
| | |
|
||||
MIME类型 编码 逗号分隔符(重要!)
|
||||
```
|
||||
|
||||
逗号是 Data URL 规范的一部分,不能用作数组分隔符!
|
||||
|
||||
### 3. 最佳实践
|
||||
|
||||
对于复杂数据结构,优先使用:
|
||||
|
||||
1. **JSON** - 结构化、标准、易于解析
|
||||
2. **Protocol Buffers** - 高性能、类型安全
|
||||
3. **避免自定义分隔符** - 容易出错
|
||||
|
||||
### 4. 向后兼容
|
||||
|
||||
在修改数据格式时,务必考虑:
|
||||
|
||||
- 现有数据的迁移
|
||||
- 降级处理逻辑
|
||||
- 渐进式升级策略
|
||||
|
||||
## 总结
|
||||
|
||||
这个 Bug 的根本原因是:**使用逗号分隔包含逗号的数据**。
|
||||
|
||||
修复方案:**使用 JSON 数组格式存储和解析数据**。
|
||||
|
||||
这是一个典型的**数据格式设计问题**,提醒我们在设计数据存储格式时,必须充分考虑数据的特性和边界情况。
|
||||
257
docs/screenshot-fix-summary.md
Normal file
257
docs/screenshot-fix-summary.md
Normal file
@ -0,0 +1,257 @@
|
||||
# 截图存储方案完整修复总结
|
||||
|
||||
## 🎯 问题回顾
|
||||
|
||||
### 原始问题
|
||||
|
||||
```
|
||||
GET http://120.48.157.2:55335/static/uploads/screenshots/bd232714-58ac-472b-b3d0-18e7e768afe0.jpg 404 (Not Found)
|
||||
```
|
||||
|
||||
**原因**:文件系统和数据库数据不一致
|
||||
|
||||
### 第二个问题(修复后发现)
|
||||
|
||||
```
|
||||
GET data:image/jpeg;base64:1 net::ERR_INVALID_URL
|
||||
```
|
||||
|
||||
**原因**:使用逗号分隔 Data URL 导致数据被截断
|
||||
|
||||
## ✅ 完整解决方案
|
||||
|
||||
### 阶段一:从文件系统迁移到数据库
|
||||
|
||||
#### 1. 数据库结构升级
|
||||
|
||||
- 字段类型:`TEXT` → `LONGTEXT`
|
||||
- 支持存储:最大 4GB Base64 数据
|
||||
|
||||
#### 2. 上传逻辑重构
|
||||
|
||||
- 旧方案:保存文件 → 返回路径
|
||||
- 新方案:转换 Base64 → 返回 Data URL
|
||||
|
||||
#### 3. 静态文件服务简化
|
||||
|
||||
- 移除重复的 `/static/uploads/` 路由
|
||||
|
||||
### 阶段二:修复 Data URL 截断问题
|
||||
|
||||
#### 问题根源
|
||||
|
||||
```
|
||||
Data URL 格式: data:image/jpeg;base64,{base64_data}
|
||||
↑
|
||||
逗号!
|
||||
|
||||
使用逗号分隔多个 Data URL:
|
||||
"data:image/jpeg;base64,abc...,data:image/png;base64,xyz..."
|
||||
↑
|
||||
这个逗号会导致分割错误!
|
||||
```
|
||||
|
||||
#### 解决方案
|
||||
|
||||
- 旧方案:逗号分隔字符串
|
||||
- 新方案:JSON 数组格式
|
||||
|
||||
```go
|
||||
// 存储
|
||||
screenshotsJSON, _ := json.Marshal(customer.Screenshots)
|
||||
// 结果: ["data:image/jpeg;base64,...","data:image/png;base64,..."]
|
||||
|
||||
// 读取
|
||||
var screenshots []string
|
||||
json.Unmarshal([]byte(data), &screenshots)
|
||||
```
|
||||
|
||||
## 📊 最终架构
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 前端 │
|
||||
│ 上传图片 │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 后端 API │
|
||||
│ 1. 读取文件 │
|
||||
│ 2. 转换 Base64 │
|
||||
│ 3. 生成 Data URL │
|
||||
│ 4. 返回数组 │
|
||||
└──────┬──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 数据库 (LONGTEXT) │
|
||||
│ JSON 数组格式: │
|
||||
│ ["data:image/...", │
|
||||
│ "data:image/..."] │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
## 🔧 修改文件清单
|
||||
|
||||
### 1. `/internal/storage/db.go`
|
||||
|
||||
- ✅ 字段类型升级为 LONGTEXT
|
||||
- ✅ 自动迁移逻辑
|
||||
|
||||
### 2. `/internal/handlers/customer_handler.go`
|
||||
|
||||
- ✅ 上传接口返回 Base64 Data URL
|
||||
- ✅ 添加文件类型验证
|
||||
- ✅ 移除文件系统操作
|
||||
|
||||
### 3. `/internal/storage/mysql_customer_storage.go`
|
||||
|
||||
- ✅ 使用 JSON 序列化存储
|
||||
- ✅ 使用 JSON 反序列化读取
|
||||
- ✅ 向后兼容旧格式
|
||||
|
||||
### 4. `/cmd/server/main.go`
|
||||
|
||||
- ✅ 简化静态文件服务配置
|
||||
|
||||
## 📝 数据格式演变
|
||||
|
||||
### 格式 1.0(文件路径 - 逗号分隔)
|
||||
|
||||
```
|
||||
数据库: /static/uploads/1.jpg,/static/uploads/2.png
|
||||
解析: ["/static/uploads/1.jpg", "/static/uploads/2.png"]
|
||||
问题: ❌ 文件可能丢失,404 错误
|
||||
```
|
||||
|
||||
### 格式 2.0(Base64 - 逗号分隔)❌ 错误
|
||||
|
||||
```
|
||||
数据库: data:image/jpeg;base64,abc...,data:image/png;base64,xyz...
|
||||
解析: ["data:image/jpeg;base64", "abc...", "data:image/png;base64", "xyz..."]
|
||||
问题: ❌ Data URL 被截断,ERR_INVALID_URL
|
||||
```
|
||||
|
||||
### 格式 3.0(Base64 - JSON 数组)✅ 正确
|
||||
|
||||
```
|
||||
数据库: ["data:image/jpeg;base64,abc...","data:image/png;base64,xyz..."]
|
||||
解析: ["data:image/jpeg;base64,abc...", "data:image/png;base64,xyz..."]
|
||||
优势: ✅ 数据完整,格式正确
|
||||
```
|
||||
|
||||
## 🧪 测试验证
|
||||
|
||||
### 1. 编译测试
|
||||
|
||||
```bash
|
||||
go build -o /tmp/crm-test ./cmd/server
|
||||
# ✅ 编译成功
|
||||
```
|
||||
|
||||
### 2. 启动测试
|
||||
|
||||
```bash
|
||||
./start.sh
|
||||
# ✅ 服务器启动成功
|
||||
# ✅ 数据库连接成功
|
||||
# ✅ 表结构迁移成功
|
||||
```
|
||||
|
||||
### 3. 功能测试
|
||||
|
||||
```bash
|
||||
# 上传截图
|
||||
curl -X POST http://localhost:55335/api/upload \
|
||||
-F "screenshots=@test.jpg"
|
||||
|
||||
# 预期响应:
|
||||
{
|
||||
"filePaths": [
|
||||
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD..."
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 📈 性能影响
|
||||
|
||||
### 存储空间
|
||||
|
||||
- Base64 编码增加约 33% 大小
|
||||
- 示例:1MB 图片 → 1.33MB Base64
|
||||
|
||||
### 数据库性能
|
||||
|
||||
- LONGTEXT 字段按需加载
|
||||
- 查询其他字段不受影响
|
||||
|
||||
### 网络传输
|
||||
|
||||
- 旧方案:2 次请求(API + 图片)
|
||||
- 新方案:1 次请求(包含 Base64)
|
||||
|
||||
## 🎓 经验教训
|
||||
|
||||
### 1. 数据一致性优先
|
||||
|
||||
- 用空间换可靠性是值得的
|
||||
- 文件系统和数据库分离容易出问题
|
||||
|
||||
### 2. 分隔符选择要谨慎
|
||||
|
||||
- 确保分隔符不会出现在数据中
|
||||
- 优先使用结构化格式(JSON)
|
||||
|
||||
### 3. Data URL 格式理解
|
||||
|
||||
```
|
||||
data:[<mediatype>][;base64],<data>
|
||||
↑
|
||||
逗号是格式的一部分!
|
||||
```
|
||||
|
||||
### 4. 向后兼容很重要
|
||||
|
||||
- 新旧格式都要支持
|
||||
- 渐进式升级策略
|
||||
|
||||
## ✨ 最终效果
|
||||
|
||||
### 问题解决
|
||||
|
||||
- ✅ 不再有 404 错误
|
||||
- ✅ 不再有 ERR_INVALID_URL 错误
|
||||
- ✅ 数据完全一致
|
||||
- ✅ 部署更简单
|
||||
|
||||
### 代码质量
|
||||
|
||||
- ✅ 编译通过
|
||||
- ✅ 类型安全
|
||||
- ✅ 向后兼容
|
||||
- ✅ 文档完善
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
1. `docs/screenshot-storage-upgrade.md` - 升级说明
|
||||
2. `docs/screenshot-storage-comparison.md` - 方案对比
|
||||
3. `docs/screenshot-bug-fix.md` - Bug 修复详解
|
||||
4. `test-screenshot-upload.sh` - 测试脚本
|
||||
|
||||
## 🚀 下一步
|
||||
|
||||
1. ✅ 重启服务器(已完成)
|
||||
2. ⏳ 测试上传功能
|
||||
3. ⏳ 测试显示功能
|
||||
4. ⏳ 验证数据库存储格式
|
||||
5. ⏳ 清理旧的上传文件(可选)
|
||||
|
||||
## 总结
|
||||
|
||||
通过两个阶段的修复:
|
||||
|
||||
1. **阶段一**:解决了文件系统和数据库不一致的问题
|
||||
2. **阶段二**:解决了 Data URL 被逗号截断的问题
|
||||
|
||||
最终实现了一个**可靠、简单、高效**的截图存储方案!🎉
|
||||
228
docs/screenshot-storage-comparison.md
Normal file
228
docs/screenshot-storage-comparison.md
Normal file
@ -0,0 +1,228 @@
|
||||
# 截图存储方案对比
|
||||
|
||||
## 方案对比总览
|
||||
|
||||
| 特性 | 文件系统存储 | 数据库 Base64 存储 ✅ |
|
||||
| ---------------- | ------------------- | ------------------------- |
|
||||
| **数据一致性** | ❌ 可能不一致 | ✅ 完全一致 |
|
||||
| **备份恢复** | ❌ 需要分别备份 | ✅ 只需备份数据库 |
|
||||
| **部署复杂度** | ❌ 需要配置文件服务 | ✅ 无需额外配置 |
|
||||
| **多服务器部署** | ❌ 需要共享存储 | ✅ 无需共享存储 |
|
||||
| **维护成本** | ❌ 需要清理孤立文件 | ✅ 自动管理 |
|
||||
| **数据大小** | ✅ 原始大小 | ⚠️ 增加 33% |
|
||||
| **查询性能** | ✅ 不影响 | ✅ 不影响(LONGTEXT优化) |
|
||||
|
||||
## 架构对比
|
||||
|
||||
### 旧方案:文件系统存储
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 前端 │
|
||||
└──────┬──────┘
|
||||
│ 1. 上传图片
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 后端 API │
|
||||
│ - 保存到文件 │──────┐
|
||||
│ - 返回路径 │ │ 2. 写入文件
|
||||
└──────┬──────────┘ ▼
|
||||
│ ┌──────────────┐
|
||||
│ 3. 保存路径 │ 文件系统 │
|
||||
▼ │ /uploads/... │
|
||||
┌─────────────────┐ └──────────────┘
|
||||
│ 数据库 │
|
||||
│ screenshots: │
|
||||
│ ["/static/..."] │
|
||||
└─────────────────┘
|
||||
|
||||
问题:
|
||||
❌ 数据库和文件系统可能不一致
|
||||
❌ 删除记录时文件可能残留
|
||||
❌ 文件被删除但数据库仍有记录
|
||||
```
|
||||
|
||||
### 新方案:数据库 Base64 存储
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ 前端 │
|
||||
└──────┬──────┘
|
||||
│ 1. 上传图片
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 后端 API │
|
||||
│ - 转换为 Base64 │
|
||||
│ - 返回 Data URL │
|
||||
└──────┬──────────────┘
|
||||
│ 2. 保存 Base64
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ 数据库 │
|
||||
│ screenshots: │
|
||||
│ ["data:image/..."] │
|
||||
└─────────────────────┘
|
||||
|
||||
优势:
|
||||
✅ 数据完全一致
|
||||
✅ 删除记录时图片自动删除
|
||||
✅ 备份恢复只需处理数据库
|
||||
```
|
||||
|
||||
## 数据流对比
|
||||
|
||||
### 上传流程
|
||||
|
||||
**旧方案**:
|
||||
|
||||
```
|
||||
图片文件 → 保存到磁盘 → 生成路径 → 存入数据库
|
||||
(可能失败) (返回前端) (可能失败)
|
||||
```
|
||||
|
||||
**新方案**:
|
||||
|
||||
```
|
||||
图片文件 → 转换 Base64 → 存入数据库
|
||||
(一次性完成)
|
||||
```
|
||||
|
||||
### 读取流程
|
||||
|
||||
**旧方案**:
|
||||
|
||||
```
|
||||
查询数据库 → 获取路径 → 配置静态服务 → 前端请求文件
|
||||
(可能不存在) (需要配置) (可能 404)
|
||||
```
|
||||
|
||||
**新方案**:
|
||||
|
||||
```
|
||||
查询数据库 → 获取 Base64 → 前端直接显示
|
||||
(一定存在) (无需额外请求)
|
||||
```
|
||||
|
||||
## 实际案例
|
||||
|
||||
### 问题场景(旧方案)
|
||||
|
||||
```
|
||||
用户报告:GET /static/uploads/screenshots/bd232714-58ac-472b-b3d0-18e7e768afe0.jpg 404
|
||||
|
||||
原因分析:
|
||||
1. 数据库中有记录:screenshots: ["/static/uploads/screenshots/bd232714..."]
|
||||
2. 但文件系统中文件不存在
|
||||
3. 可能原因:
|
||||
- 手动清理了上传目录
|
||||
- 服务器迁移时未同步文件
|
||||
- 磁盘故障导致文件丢失
|
||||
```
|
||||
|
||||
### 解决方案(新方案)
|
||||
|
||||
```
|
||||
数据库记录:
|
||||
{
|
||||
"screenshots": [
|
||||
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD..."
|
||||
]
|
||||
}
|
||||
|
||||
特点:
|
||||
✅ 图片数据直接在数据库中
|
||||
✅ 查询到记录就一定能显示图片
|
||||
✅ 不会出现 404 错误
|
||||
```
|
||||
|
||||
## 性能影响分析
|
||||
|
||||
### 存储空间
|
||||
|
||||
```
|
||||
原始图片: 1 MB
|
||||
Base64 编码: 1.33 MB (+33%)
|
||||
|
||||
示例:
|
||||
- 5 张截图,每张 500 KB
|
||||
- 文件系统: 2.5 MB
|
||||
- Base64: 3.3 MB (+0.8 MB)
|
||||
```
|
||||
|
||||
### 数据库性能
|
||||
|
||||
```sql
|
||||
-- LONGTEXT 字段不影响其他字段查询
|
||||
SELECT id, customer_name, created_at
|
||||
FROM customers;
|
||||
-- ✅ 不会加载 screenshots 字段
|
||||
|
||||
-- 只有明确查询时才加载
|
||||
SELECT screenshots
|
||||
FROM customers
|
||||
WHERE id = 'xxx';
|
||||
-- ⚠️ 会加载完整的 Base64 数据
|
||||
```
|
||||
|
||||
### 网络传输
|
||||
|
||||
**旧方案**:
|
||||
|
||||
```
|
||||
1. API 请求: 返回路径 (~100 bytes)
|
||||
2. 图片请求: 下载文件 (1 MB)
|
||||
总计: 2 次请求
|
||||
```
|
||||
|
||||
**新方案**:
|
||||
|
||||
```
|
||||
1. API 请求: 返回 Base64 (1.33 MB)
|
||||
总计: 1 次请求
|
||||
```
|
||||
|
||||
## 最佳实践建议
|
||||
|
||||
### ✅ 适合使用 Base64 存储的场景
|
||||
|
||||
- 截图、缩略图等小图片
|
||||
- 需要保证数据一致性
|
||||
- 简化部署和维护
|
||||
- 单张图片 < 2 MB
|
||||
- 每条记录图片数量 < 10 张
|
||||
|
||||
### ⚠️ 不适合的场景
|
||||
|
||||
- 高清大图(> 5 MB)
|
||||
- 大量图片(> 20 张/记录)
|
||||
- 需要图片处理(缩放、裁剪)
|
||||
- 需要 CDN 加速
|
||||
|
||||
### 💡 推荐方案
|
||||
|
||||
```
|
||||
小图片(截图、头像): Base64 存储 ✅
|
||||
大图片(产品图、相册): 对象存储(OSS)+ URL 引用
|
||||
```
|
||||
|
||||
## 迁移检查清单
|
||||
|
||||
- [x] 数据库字段升级为 LONGTEXT
|
||||
- [x] 上传接口返回 Base64 Data URL
|
||||
- [x] 前端代码兼容两种格式
|
||||
- [ ] 测试上传功能
|
||||
- [ ] 测试显示功能
|
||||
- [ ] 验证数据库存储
|
||||
- [ ] 性能测试
|
||||
- [ ] 备份验证
|
||||
|
||||
## 总结
|
||||
|
||||
通过将截图存储从文件系统迁移到数据库 Base64 存储,我们:
|
||||
|
||||
1. **彻底解决了数据一致性问题** - 这是最重要的改进
|
||||
2. **简化了部署和维护** - 无需管理文件系统
|
||||
3. **提高了系统可靠性** - 不会出现文件丢失的情况
|
||||
4. **付出了可接受的代价** - 存储空间增加 33%,但换来了更好的数据完整性
|
||||
|
||||
这是一个典型的**用空间换可靠性**的工程决策。
|
||||
192
docs/screenshot-storage-upgrade.md
Normal file
192
docs/screenshot-storage-upgrade.md
Normal file
@ -0,0 +1,192 @@
|
||||
# 截图存储方案升级说明
|
||||
|
||||
## 改进概述
|
||||
|
||||
将截图存储方式从**文件系统存储**升级为**数据库 Base64 存储**,解决数据一致性问题。
|
||||
|
||||
## 主要变更
|
||||
|
||||
### 1. 数据库结构调整
|
||||
|
||||
**文件**: `internal/storage/db.go`
|
||||
|
||||
- 将 `screenshots` 字段类型从 `TEXT` 升级为 `LONGTEXT`
|
||||
- 添加自动迁移逻辑,检测并升级现有数据库的字段类型
|
||||
- `LONGTEXT` 最大支持 4GB 数据,足够存储多张 Base64 编码的图片
|
||||
|
||||
```sql
|
||||
-- 旧结构
|
||||
screenshots TEXT
|
||||
|
||||
-- 新结构
|
||||
screenshots LONGTEXT
|
||||
```
|
||||
|
||||
### 2. 上传处理逻辑重构
|
||||
|
||||
**文件**: `internal/handlers/customer_handler.go`
|
||||
|
||||
**旧方案**:
|
||||
|
||||
- 上传的图片保存到 `./frontend/uploads/screenshots/` 目录
|
||||
- 返回文件路径: `/static/uploads/screenshots/{uuid}.jpg`
|
||||
- 需要配置静态文件服务器
|
||||
|
||||
**新方案**:
|
||||
|
||||
- 上传的图片转换为 Base64 编码
|
||||
- 返回 Data URL 格式: `data:image/jpeg;base64,{base64_string}`
|
||||
- 无需文件系统存储,无需静态文件服务器配置
|
||||
|
||||
### 3. 前端兼容性
|
||||
|
||||
前端代码**无需修改**!因为:
|
||||
|
||||
- `<img>` 标签的 `src` 属性同时支持 URL 和 Data URL
|
||||
- 现有代码 `img.src = path` 可以直接使用 Base64 Data URL
|
||||
|
||||
## 优势对比
|
||||
|
||||
### 文件系统存储方案的问题
|
||||
|
||||
❌ **数据一致性问题**
|
||||
|
||||
- 数据库记录存在,但文件可能被误删
|
||||
- 文件存在,但数据库记录可能被删除
|
||||
- 备份恢复时需要同时处理数据库和文件系统
|
||||
|
||||
❌ **部署复杂性**
|
||||
|
||||
- 需要配置静态文件服务器路由
|
||||
- 需要确保上传目录权限正确
|
||||
- 多服务器部署需要共享存储或文件同步
|
||||
|
||||
❌ **维护成本**
|
||||
|
||||
- 需要定期清理孤立文件
|
||||
- 需要监控磁盘空间使用
|
||||
|
||||
### Base64 数据库存储方案的优势
|
||||
|
||||
✅ **数据一致性保证**
|
||||
|
||||
- 图片数据和业务数据在同一事务中
|
||||
- 删除记录时图片数据自动删除
|
||||
- 备份恢复只需处理数据库
|
||||
|
||||
✅ **部署简化**
|
||||
|
||||
- 无需配置文件上传目录
|
||||
- 无需配置静态文件服务器
|
||||
- 多服务器部署无需共享存储
|
||||
|
||||
✅ **维护简化**
|
||||
|
||||
- 无需清理孤立文件
|
||||
- 无需监控文件系统空间
|
||||
|
||||
## 性能考虑
|
||||
|
||||
### Base64 编码开销
|
||||
|
||||
- Base64 编码会增加约 33% 的数据大小
|
||||
- 例如: 1MB 图片 → 约 1.33MB Base64 字符串
|
||||
|
||||
### 数据库存储
|
||||
|
||||
- `LONGTEXT` 最大 4GB,足够存储多张高清截图
|
||||
- MySQL 对 LONGTEXT 字段有优化,不会影响其他字段查询性能
|
||||
|
||||
### 建议
|
||||
|
||||
- 单张图片建议不超过 2MB
|
||||
- 每条记录建议不超过 5 张截图
|
||||
- 如需存储大量高清图片,可考虑对象存储服务(OSS)
|
||||
|
||||
## 迁移说明
|
||||
|
||||
### 自动迁移
|
||||
|
||||
启动服务器时会自动执行以下操作:
|
||||
|
||||
1. 检测 `screenshots` 字段类型
|
||||
2. 如果是 `TEXT`,自动升级为 `LONGTEXT`
|
||||
3. 输出迁移日志
|
||||
|
||||
### 现有数据处理
|
||||
|
||||
**旧数据格式**(文件路径):
|
||||
|
||||
```json
|
||||
{
|
||||
"screenshots": [
|
||||
"/static/uploads/screenshots/uuid1.jpg",
|
||||
"/static/uploads/screenshots/uuid2.png"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**新数据格式**(Base64 Data URL):
|
||||
|
||||
```json
|
||||
{
|
||||
"screenshots": [
|
||||
"data:image/jpeg;base64,/9j/4AAQSkZJRg...",
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUh..."
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**兼容性**: 前端代码同时支持两种格式,旧数据仍可正常显示(如果文件未被删除)
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 1. 上传测试
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8081/api/upload \
|
||||
-F "screenshots=@test1.jpg" \
|
||||
-F "screenshots=@test2.png"
|
||||
```
|
||||
|
||||
预期响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"filePaths": [
|
||||
"data:image/jpeg;base64,/9j/4AAQ...",
|
||||
"data:image/png;base64,iVBORw0KG..."
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建客户测试
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8081/api/customers \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"customerName": "测试客户",
|
||||
"screenshots": ["data:image/jpeg;base64,/9j/4AAQ..."]
|
||||
}'
|
||||
```
|
||||
|
||||
### 3. 查询验证
|
||||
|
||||
```bash
|
||||
curl http://localhost:8081/api/customers/{id}
|
||||
```
|
||||
|
||||
验证返回的 `screenshots` 字段包含完整的 Base64 Data URL
|
||||
|
||||
## 回滚方案
|
||||
|
||||
如需回滚到文件系统存储方案:
|
||||
|
||||
1. 恢复 `customer_handler.go` 中的上传逻辑
|
||||
2. 恢复 `main.go` 中的静态文件服务配置
|
||||
3. 数据库字段保持 `LONGTEXT`(向下兼容 `TEXT`)
|
||||
|
||||
## 总结
|
||||
|
||||
这次升级从根本上解决了文件系统和数据库数据不一致的问题,简化了部署和维护流程,提高了系统的可靠性和可维护性。
|
||||
@ -2,14 +2,13 @@ package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@ -17,7 +16,6 @@ import (
|
||||
"crm-go/internal/storage"
|
||||
"crm-go/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
@ -203,46 +201,42 @@ func (h *CustomerHandler) UploadScreenshots(w http.ResponseWriter, r *http.Reque
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure upload directory exists
|
||||
uploadDir := "./frontend/uploads/screenshots"
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var filePaths []string
|
||||
var base64Images []string
|
||||
for _, fileHeader := range files {
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
log.Printf("Failed to open file: %v", err)
|
||||
continue
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Generate unique filename
|
||||
ext := filepath.Ext(fileHeader.Filename)
|
||||
newFilename := uuid.New().String() + ext
|
||||
filePath := filepath.Join(uploadDir, newFilename)
|
||||
|
||||
// Create file on disk
|
||||
dst, err := os.Create(filePath)
|
||||
// 读取文件内容
|
||||
fileBytes, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// Copy file content
|
||||
if _, err := io.Copy(dst, file); err != nil {
|
||||
log.Printf("Failed to read file: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Save relative path for frontend
|
||||
relativePath := "/static/uploads/screenshots/" + newFilename
|
||||
filePaths = append(filePaths, relativePath)
|
||||
// 检测文件类型
|
||||
contentType := http.DetectContentType(fileBytes)
|
||||
|
||||
// 验证是否为图片
|
||||
if !strings.HasPrefix(contentType, "image/") {
|
||||
log.Printf("Invalid file type: %s", contentType)
|
||||
continue
|
||||
}
|
||||
|
||||
// 转换为 Base64
|
||||
base64Str := fmt.Sprintf("data:%s;base64,%s",
|
||||
contentType,
|
||||
base64.StdEncoding.EncodeToString(fileBytes))
|
||||
|
||||
base64Images = append(base64Images, base64Str)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"filePaths": filePaths,
|
||||
"filePaths": base64Images,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ func autoMigrate() error {
|
||||
module VARCHAR(100),
|
||||
status_progress VARCHAR(100),
|
||||
reporter VARCHAR(255),
|
||||
screenshots TEXT,
|
||||
screenshots LONGTEXT,
|
||||
INDEX idx_customer_name (customer_name),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@ -118,27 +118,33 @@ func autoMigrate() error {
|
||||
return fmt.Errorf("failed to create trial_periods table: %v", err)
|
||||
}
|
||||
|
||||
// 检查并添加 screenshots 列(如果不存在)
|
||||
// MySQL 不支持 IF NOT EXISTS,所以我们需要先检查列是否存在
|
||||
var columnExists int
|
||||
// 检查并添加/更新 screenshots 列
|
||||
// 检查列是否存在以及数据类型
|
||||
var columnType sql.NullString
|
||||
err := db.QueryRow(`
|
||||
SELECT COUNT(*)
|
||||
SELECT DATA_TYPE
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE()
|
||||
AND TABLE_NAME = 'customers'
|
||||
AND COLUMN_NAME = 'screenshots'
|
||||
`).Scan(&columnExists)
|
||||
`).Scan(&columnType)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check screenshots column: %v", err)
|
||||
}
|
||||
|
||||
if columnExists == 0 {
|
||||
_, err = db.Exec("ALTER TABLE customers ADD COLUMN screenshots TEXT")
|
||||
if err == sql.ErrNoRows {
|
||||
// 列不存在,添加新列
|
||||
_, err = db.Exec("ALTER TABLE customers ADD COLUMN screenshots LONGTEXT")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add screenshots column: %v", err)
|
||||
}
|
||||
log.Println("✅ Added screenshots column to customers table")
|
||||
log.Println("✅ Added screenshots column (LONGTEXT) to customers table")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("failed to check screenshots column: %v", err)
|
||||
} else if columnType.Valid && columnType.String != "longtext" {
|
||||
// 列存在但类型不是 LONGTEXT,修改列类型
|
||||
_, err = db.Exec("ALTER TABLE customers MODIFY COLUMN screenshots LONGTEXT")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to modify screenshots column type: %v", err)
|
||||
}
|
||||
log.Printf("✅ Modified screenshots column type from %s to LONGTEXT\n", columnType.String)
|
||||
}
|
||||
|
||||
log.Println("✅ Database tables migrated successfully")
|
||||
|
||||
@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@ -57,8 +58,16 @@ func (cs *mysqlCustomerStorage) GetAllCustomers() ([]models.Customer, error) {
|
||||
c.Module = module.String
|
||||
c.StatusProgress = statusProgress.String
|
||||
c.Reporter = reporter.String
|
||||
// 解析 screenshots JSON 数组
|
||||
if screenshots.Valid && screenshots.String != "" {
|
||||
// 尝试解析为 JSON 数组
|
||||
var screenshotArray []string
|
||||
if err := json.Unmarshal([]byte(screenshots.String), &screenshotArray); err == nil {
|
||||
c.Screenshots = screenshotArray
|
||||
} else {
|
||||
// 向后兼容:如果不是 JSON,尝试逗号分隔(旧格式)
|
||||
c.Screenshots = strings.Split(screenshots.String, ",")
|
||||
}
|
||||
} else {
|
||||
c.Screenshots = []string{}
|
||||
}
|
||||
@ -101,8 +110,17 @@ func (cs *mysqlCustomerStorage) GetCustomerByID(id string) (*models.Customer, er
|
||||
c.Module = module.String
|
||||
c.StatusProgress = statusProgress.String
|
||||
c.Reporter = reporter.String
|
||||
|
||||
// 解析 screenshots JSON 数组
|
||||
if screenshots.Valid && screenshots.String != "" {
|
||||
// 尝试解析为 JSON 数组
|
||||
var screenshotArray []string
|
||||
if err := json.Unmarshal([]byte(screenshots.String), &screenshotArray); err == nil {
|
||||
c.Screenshots = screenshotArray
|
||||
} else {
|
||||
// 向后兼容:如果不是 JSON,尝试逗号分隔(旧格式)
|
||||
c.Screenshots = strings.Split(screenshots.String, ",")
|
||||
}
|
||||
} else {
|
||||
c.Screenshots = []string{}
|
||||
}
|
||||
@ -124,13 +142,17 @@ func (cs *mysqlCustomerStorage) CreateCustomer(customer models.Customer) error {
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
|
||||
screenshots := strings.Join(customer.Screenshots, ",")
|
||||
// 将 screenshots 数组序列化为 JSON
|
||||
screenshotsJSON, err := json.Marshal(customer.Screenshots)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal screenshots: %v", err)
|
||||
}
|
||||
|
||||
_, err := cs.db.Exec(query,
|
||||
_, err = cs.db.Exec(query,
|
||||
customer.ID, customer.CreatedAt, customer.CustomerName,
|
||||
customer.IntendedProduct, customer.Version, customer.Description,
|
||||
customer.Solution, customer.Type, customer.Module,
|
||||
customer.StatusProgress, customer.Reporter, screenshots,
|
||||
customer.StatusProgress, customer.Reporter, string(screenshotsJSON),
|
||||
)
|
||||
|
||||
return err
|
||||
@ -183,12 +205,16 @@ func (cs *mysqlCustomerStorage) UpdateCustomer(id string, updates models.UpdateC
|
||||
WHERE id = ?
|
||||
`
|
||||
|
||||
screenshots := strings.Join(existing.Screenshots, ",")
|
||||
// 将 screenshots 数组序列化为 JSON
|
||||
screenshotsJSON, err := json.Marshal(existing.Screenshots)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal screenshots: %v", err)
|
||||
}
|
||||
|
||||
_, err = cs.db.Exec(query,
|
||||
existing.CustomerName, existing.IntendedProduct, existing.Version,
|
||||
existing.Description, existing.Solution, existing.Type,
|
||||
existing.Module, existing.StatusProgress, existing.Reporter, screenshots,
|
||||
existing.Module, existing.StatusProgress, existing.Reporter, string(screenshotsJSON),
|
||||
id,
|
||||
)
|
||||
|
||||
|
||||
58
test-screenshot-upload.sh
Executable file
58
test-screenshot-upload.sh
Executable file
@ -0,0 +1,58 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 截图上传功能测试脚本
|
||||
|
||||
set -e
|
||||
|
||||
BASE_URL="http://localhost:8081"
|
||||
TEST_DIR="/tmp/crm-screenshot-test"
|
||||
|
||||
echo "🧪 开始测试截图上传功能..."
|
||||
|
||||
# 创建测试目录
|
||||
mkdir -p "$TEST_DIR"
|
||||
cd "$TEST_DIR"
|
||||
|
||||
# 生成测试图片(使用 ImageMagick 或创建简单的测试文件)
|
||||
echo "📸 生成测试图片..."
|
||||
|
||||
# 创建一个简单的 1x1 像素的 PNG 图片(Base64 编码)
|
||||
echo "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==" | base64 -d > test1.png
|
||||
|
||||
# 创建一个简单的 JPG 图片
|
||||
echo "/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCwAA8A/9k=" | base64 -d > test2.jpg
|
||||
|
||||
echo "✅ 测试图片已生成"
|
||||
|
||||
# 测试上传
|
||||
echo ""
|
||||
echo "📤 测试上传截图..."
|
||||
RESPONSE=$(curl -s -X POST "$BASE_URL/api/upload" \
|
||||
-F "screenshots=@test1.png" \
|
||||
-F "screenshots=@test2.jpg")
|
||||
|
||||
echo "📥 服务器响应:"
|
||||
echo "$RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE"
|
||||
|
||||
# 检查响应是否包含 Base64 数据
|
||||
if echo "$RESPONSE" | grep -q "data:image"; then
|
||||
echo ""
|
||||
echo "✅ 测试通过!服务器返回了 Base64 编码的图片数据"
|
||||
|
||||
# 提取第一个 Base64 字符串的长度
|
||||
BASE64_LENGTH=$(echo "$RESPONSE" | grep -o 'data:image[^"]*' | head -1 | wc -c)
|
||||
echo "📊 Base64 数据长度: $BASE64_LENGTH 字符"
|
||||
else
|
||||
echo ""
|
||||
echo "❌ 测试失败!服务器未返回 Base64 数据"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 清理
|
||||
echo ""
|
||||
echo "🧹 清理测试文件..."
|
||||
cd /tmp
|
||||
rm -rf "$TEST_DIR"
|
||||
|
||||
echo ""
|
||||
echo "🎉 所有测试完成!"
|
||||
Loading…
x
Reference in New Issue
Block a user