# 图片处理流程文档

**版本**: 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 🤖
