417 lines
8.7 KiB
Markdown
417 lines
8.7 KiB
Markdown
|
|
# 图片处理流程文档
|
|||
|
|
|
|||
|
|
**版本**: v1.0.0
|
|||
|
|
**更新日期**: 2026-03-16
|
|||
|
|
**作者**: 菜鸟小 D 🤖
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📊 完整流程图
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户上传图片
|
|||
|
|
↓
|
|||
|
|
[1] 前端上传组件
|
|||
|
|
↓
|
|||
|
|
[2] 后端接收验证
|
|||
|
|
↓
|
|||
|
|
[3] 文件命名处理
|
|||
|
|
↓
|
|||
|
|
[4] 保存到服务器
|
|||
|
|
↓
|
|||
|
|
[5] 数据库记录
|
|||
|
|
↓
|
|||
|
|
[6] 返回图片 URL
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1️⃣ 前端上传组件
|
|||
|
|
|
|||
|
|
### 上传页面
|
|||
|
|
|
|||
|
|
**文件**: `frontend/src/pages/Add.jsx`
|
|||
|
|
|
|||
|
|
**上传逻辑**:
|
|||
|
|
```jsx
|
|||
|
|
// 选择图片后自动上传
|
|||
|
|
const handleImageSelect = async (e) => {
|
|||
|
|
const file = e.target.files[0]
|
|||
|
|
if (!file) return
|
|||
|
|
|
|||
|
|
const formData = new FormData()
|
|||
|
|
formData.append('file', file)
|
|||
|
|
formData.append('collection_id', collectionId)
|
|||
|
|
|
|||
|
|
const res = await fetch('/api/ocr/recognize', {
|
|||
|
|
method: 'POST',
|
|||
|
|
body: formData
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
const data = await res.json()
|
|||
|
|
// 处理 OCR 识别结果
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 图片显示
|
|||
|
|
|
|||
|
|
**文件**: `frontend/src/pages/Detail.jsx`
|
|||
|
|
|
|||
|
|
**显示逻辑**:
|
|||
|
|
```jsx
|
|||
|
|
<img
|
|||
|
|
src={`/uploads/${img.path}`}
|
|||
|
|
alt={img.originalName}
|
|||
|
|
onError={(e) => {
|
|||
|
|
// 加载失败显示"无图片"占位符
|
|||
|
|
e.target.style.display = 'none';
|
|||
|
|
e.target.parentElement.innerHTML = '<div>无图片</div>';
|
|||
|
|
}}
|
|||
|
|
/>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2️⃣ 后端接收验证
|
|||
|
|
|
|||
|
|
### API 端点
|
|||
|
|
|
|||
|
|
**文件**: `backend/app/routers/collections.py`
|
|||
|
|
|
|||
|
|
**路由**: `POST /api/collections/upload-image`
|
|||
|
|
|
|||
|
|
### 验证流程
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
@router.post("/upload-image")
|
|||
|
|
async def upload_image(
|
|||
|
|
collection_id: str = None,
|
|||
|
|
file: UploadFile = File(...),
|
|||
|
|
current_user: User = Depends(get_current_user),
|
|||
|
|
db: Session = Depends(get_db)
|
|||
|
|
):
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 验证步骤
|
|||
|
|
|
|||
|
|
1. **验证藏品是否存在**
|
|||
|
|
```python
|
|||
|
|
collection = db.query(Collection).filter(
|
|||
|
|
Collection.f99_90_id == collection_id
|
|||
|
|
).first()
|
|||
|
|
|
|||
|
|
if not collection:
|
|||
|
|
raise HTTPException(status_code=404, detail="E00033: 藏品不存在")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **获取用户信息**
|
|||
|
|
```python
|
|||
|
|
owner = db.query(User).filter(
|
|||
|
|
User.f99_90_id == collection.f99_91_user_id
|
|||
|
|
).first()
|
|||
|
|
username = owner.f01_01_name if owner else "unknown"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. **获取藏品信息**
|
|||
|
|
```python
|
|||
|
|
code = collection.f01_02_code or "0000"
|
|||
|
|
prefix_serial = collection.f02_10_prefix_serial or ""
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
4. **验证文件类型**
|
|||
|
|
```python
|
|||
|
|
if not file.content_type.startswith('image/'):
|
|||
|
|
raise HTTPException(status_code=400,
|
|||
|
|
detail="E00038: 只能上传图片文件")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
5. **验证文件大小**
|
|||
|
|
```python
|
|||
|
|
file_size = len(content)
|
|||
|
|
if file_size > 10 * 1024 * 1024: # 10MB
|
|||
|
|
raise HTTPException(status_code=400,
|
|||
|
|
detail=f"图片大小不能超过 10MB")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3️⃣ 文件命名处理
|
|||
|
|
|
|||
|
|
### 命名规则
|
|||
|
|
|
|||
|
|
**格式**: `用户名 - 藏品编号 - 冠字号。扩展名`
|
|||
|
|
|
|||
|
|
**示例**:
|
|||
|
|
- `admin-0001-J051963351.jpeg`
|
|||
|
|
- `admin-0002-J035161361.JPG`
|
|||
|
|
- `testuser-0015.jpeg` (无冠字号)
|
|||
|
|
|
|||
|
|
### 命名代码
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 清理特殊字符,只保留字母、数字、中文、横杠
|
|||
|
|
import re
|
|||
|
|
clean_username = re.sub(r'[^\w\u4e00-\u9fff\-]', '', username)
|
|||
|
|
clean_serial = re.sub(r'[^\w\u4e00-\u9fff\-]', '', prefix_serial)
|
|||
|
|
|
|||
|
|
# 生成文件名
|
|||
|
|
file_extension = file.filename.split('.')[-1] if '.' in file.filename else 'jpg'
|
|||
|
|
|
|||
|
|
if clean_serial:
|
|||
|
|
filename = f"{clean_username}-{code}-{clean_serial}.{file_extension}"
|
|||
|
|
else:
|
|||
|
|
filename = f"{clean_username}-{code}.{file_extension}"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 避免重名
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 如果文件已存在,添加时间戳
|
|||
|
|
file_path = os.path.join(upload_dir, filename)
|
|||
|
|
if os.path.exists(file_path):
|
|||
|
|
import time
|
|||
|
|
timestamp = int(time.time())
|
|||
|
|
base_name = filename.rsplit('.', 1)[0]
|
|||
|
|
filename = f"{base_name}-{timestamp}.{file_extension}"
|
|||
|
|
file_path = os.path.join(upload_dir, filename)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4️⃣ 保存到服务器
|
|||
|
|
|
|||
|
|
### 存储路径
|
|||
|
|
|
|||
|
|
**目录**: `backend/uploads/collections/`
|
|||
|
|
|
|||
|
|
**完整路径**: `/opt/jiachenlong-backend/uploads/collections/`
|
|||
|
|
|
|||
|
|
### 保存代码
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 创建上传目录
|
|||
|
|
upload_dir = "uploads/collections"
|
|||
|
|
os.makedirs(upload_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
# 保存文件
|
|||
|
|
with open(file_path, "wb") as buffer:
|
|||
|
|
buffer.write(content)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 文件权限
|
|||
|
|
|
|||
|
|
- **所有者**: root
|
|||
|
|
- **权限**: 644 (rw-r--r--)
|
|||
|
|
- **组**: root
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5️⃣ 数据库记录
|
|||
|
|
|
|||
|
|
### 数据表
|
|||
|
|
|
|||
|
|
**表名**: `collection_images`
|
|||
|
|
|
|||
|
|
### 表结构
|
|||
|
|
|
|||
|
|
```sql
|
|||
|
|
CREATE TABLE collection_images (
|
|||
|
|
id VARCHAR(36) PRIMARY KEY, -- UUID
|
|||
|
|
collection_id VARCHAR(36), -- 关联藏品 ID
|
|||
|
|
filename VARCHAR(255), -- 文件名
|
|||
|
|
original_name VARCHAR(255), -- 原始文件名
|
|||
|
|
path VARCHAR(500), -- 存储路径
|
|||
|
|
created_at TIMESTAMP DEFAULT NOW() -- 创建时间
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 插入记录
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
from app.models.models import CollectionImage
|
|||
|
|
import uuid
|
|||
|
|
|
|||
|
|
image = CollectionImage(
|
|||
|
|
id=str(uuid.uuid4()),
|
|||
|
|
collection_id=collection_id,
|
|||
|
|
filename=filename,
|
|||
|
|
original_name=file.filename,
|
|||
|
|
path=file_path
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
db.add(image)
|
|||
|
|
db.commit()
|
|||
|
|
db.refresh(image)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 返回数据
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
return {
|
|||
|
|
"message": "上传成功",
|
|||
|
|
"image_id": image.id,
|
|||
|
|
"filename": filename
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6️⃣ 图片访问
|
|||
|
|
|
|||
|
|
### Nginx 代理配置
|
|||
|
|
|
|||
|
|
**文件**: `/etc/nginx/conf.d/jiachenlong.conf`
|
|||
|
|
|
|||
|
|
```nginx
|
|||
|
|
# 图片上传文件代理
|
|||
|
|
location /uploads {
|
|||
|
|
proxy_pass http://47.110.37.129:3000/uploads;
|
|||
|
|
proxy_set_header Host $host;
|
|||
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|||
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|||
|
|
client_max_body_size 20M;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 访问 URL 格式
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
http://8.149.137.26/uploads/collections/admin-0001-J051963351.jpeg
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 后端静态文件服务
|
|||
|
|
|
|||
|
|
**文件**: `backend/app/main.py`
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
# 挂载静态文件目录(图片上传)
|
|||
|
|
uploads_dir = "uploads"
|
|||
|
|
os.makedirs(uploads_dir, exist_ok=True)
|
|||
|
|
app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads")
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔍 OCR 识别流程
|
|||
|
|
|
|||
|
|
### API 端点
|
|||
|
|
|
|||
|
|
**路由**: `POST /api/ocr/recognize`
|
|||
|
|
|
|||
|
|
**文件**: `backend/app/routers/ocr.py`
|
|||
|
|
|
|||
|
|
### 识别步骤
|
|||
|
|
|
|||
|
|
1. **读取图片并转 Base64**
|
|||
|
|
```python
|
|||
|
|
image_data = await image.read()
|
|||
|
|
image_base64 = base64.b64encode(image_data).decode('utf-8')
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. **调用阿里云 DashScope API**
|
|||
|
|
```python
|
|||
|
|
payload = {
|
|||
|
|
"model": "qwen-vl-max",
|
|||
|
|
"messages": [{
|
|||
|
|
"role": "user",
|
|||
|
|
"content": [
|
|||
|
|
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{image_base64}"}},
|
|||
|
|
{"type": "text", "text": PROFESSIONAL_PROMPT}
|
|||
|
|
]
|
|||
|
|
}]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
3. **提取识别结果**
|
|||
|
|
```python
|
|||
|
|
def extract_fields(text: str) -> dict:
|
|||
|
|
patterns = {
|
|||
|
|
'version': r'✅.*?2.*?发行版别.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'prefix_serial': r'✅.*?6.*?冠字序号.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
'grading_score': r'✅.*?8.*?评级分数.*?[::]\s*(.+?)(?:\n|$)',
|
|||
|
|
# ... 更多字段
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
4. **返回结构化数据**
|
|||
|
|
```python
|
|||
|
|
return {
|
|||
|
|
"success": True,
|
|||
|
|
"text": text_content,
|
|||
|
|
"fields": fields
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 📋 完整示例
|
|||
|
|
|
|||
|
|
### 用户上传流程
|
|||
|
|
|
|||
|
|
1. **用户选择图片** → 前端显示预览
|
|||
|
|
2. **点击上传** → 发送到 `/api/ocr/recognize`
|
|||
|
|
3. **OCR 识别** → 提取藏品信息
|
|||
|
|
4. **填写表单** → 用户确认/修改信息
|
|||
|
|
5. **保存藏品** → 创建藏品记录
|
|||
|
|
6. **上传图片** → 发送到 `/api/collections/upload-image`
|
|||
|
|
7. **保存成功** → 返回图片 URL
|
|||
|
|
|
|||
|
|
### 文件命名示例
|
|||
|
|
|
|||
|
|
**输入**:
|
|||
|
|
- 用户名:`admin`
|
|||
|
|
- 藏品编号:`0001`
|
|||
|
|
- 冠字号:`J051963351`
|
|||
|
|
- 原始文件名:`001.JPG`
|
|||
|
|
|
|||
|
|
**输出**:
|
|||
|
|
- 文件名:`admin-0001-J051963351.JPG`
|
|||
|
|
- 路径:`uploads/collections/admin-0001-J051963351.JPG`
|
|||
|
|
- URL:`http://8.149.137.26/uploads/collections/admin-0001-J051963351.JPG`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ⚠️ 注意事项
|
|||
|
|
|
|||
|
|
### 安全限制
|
|||
|
|
|
|||
|
|
1. **文件大小**: 最大 10MB
|
|||
|
|
2. **文件类型**: 仅支持图片(image/*)
|
|||
|
|
3. **认证要求**: 必须登录才能上传
|
|||
|
|
4. **权限控制**: 只能上传到自己的藏品
|
|||
|
|
|
|||
|
|
### 性能优化
|
|||
|
|
|
|||
|
|
1. **图片压缩**: 建议前端先压缩再上传
|
|||
|
|
2. **CDN 加速**: 生产环境建议使用 CDN
|
|||
|
|
3. **缓存策略**: Nginx 配置静态资源缓存
|
|||
|
|
|
|||
|
|
### 备份策略
|
|||
|
|
|
|||
|
|
1. **定期备份**: 备份 `uploads/collections/` 目录
|
|||
|
|
2. **数据库备份**: 定期导出 `collection_images` 表
|
|||
|
|
3. **异地备份**: 重要图片建议异地备份
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 🔧 故障排查
|
|||
|
|
|
|||
|
|
### 图片不显示
|
|||
|
|
|
|||
|
|
1. 检查文件是否存在:`ls -lh /opt/jiachenlong-backend/uploads/collections/`
|
|||
|
|
2. 检查数据库记录:`SELECT * FROM collection_images;`
|
|||
|
|
3. 检查 Nginx 日志:`tail -f /var/log/nginx/error.log`
|
|||
|
|
4. 检查后端日志:`tail -f /tmp/uvicorn.log`
|
|||
|
|
|
|||
|
|
### 上传失败
|
|||
|
|
|
|||
|
|
1. 检查文件大小是否超限
|
|||
|
|
2. 检查文件类型是否正确
|
|||
|
|
3. 检查藏品 ID 是否存在
|
|||
|
|
4. 检查磁盘空间是否充足
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
**最后更新**: 2026-03-16
|
|||
|
|
**维护人员**: 菜鸟小 D 🤖
|