jiachenlong/backend/app/routers/collections.py

1780 lines
47 KiB
Python
Raw Normal View History

# collections - 藏品路由
2026-04-19 22:22:45 +08:00
# Version: 0.0.1
# 更新:
import os
# Version: 1.2.x
# 更新:
2026-03-23 11:08:52 +08:00
import os
# 更新:
2026-03-23 11:08:52 +08:00
import uuid
# 更新:
2026-03-23 11:08:52 +08:00
import re
# 更新:
2026-03-23 11:08:52 +08:00
from typing import Optional, List
# 更新:
2026-03-23 11:08:52 +08:00
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File
# 更新:
2026-03-23 11:08:52 +08:00
from sqlalchemy import func, text
# 更新:
from sqlalchemy.orm import Session, joinedload
# 更新:
2026-03-23 11:08:52 +08:00
from app.core.database import get_db
# 更新:
2026-03-23 11:08:52 +08:00
from app.core.auth import get_current_user
# 更新:
2026-03-23 11:08:52 +08:00
from app.core.logging_config import logger
# 更新:
2026-03-23 11:08:52 +08:00
from app.models.models import User, Collection, CollectionImage, Operation
# 更新:
2026-03-23 11:08:52 +08:00
from app.schemas.schemas import (
# 更新:
2026-03-23 11:08:52 +08:00
CollectionCreate, CollectionUpdate, CollectionResponse,
# 更新:
2026-03-23 11:08:52 +08:00
CollectionListResponse, CollectionImageResponse
# 更新:
2026-03-23 11:08:52 +08:00
)
# 更新:
2026-03-23 11:08:52 +08:00
from app.services.oss import upload_to_oss, get_oss_path, delete_from_oss, get_public_url
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
router = APIRouter(prefix="/api/collections", tags=["藏品"])
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
def to_camel_case(data: dict) -> dict:
# 更新:
2026-03-23 11:08:52 +08:00
"""将字段编码转换为 camelCase 格式"""
# 更新:
2026-03-23 11:08:52 +08:00
if not data:
# 更新:
2026-03-23 11:08:52 +08:00
return data
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
mapping = {
# 更新:
2026-03-23 11:08:52 +08:00
'f99_90_id': 'id',
# 更新:
2026-03-23 11:08:52 +08:00
'f99_91_user_id': 'userId',
# 更新:
2026-03-23 11:08:52 +08:00
'f99_92_created_at': 'createdAt',
# 更新:
2026-03-23 11:08:52 +08:00
'f99_93_updated_at': 'updatedAt',
# 更新:
2026-03-23 11:08:52 +08:00
'f01_01_name': 'name',
# 更新:
2026-03-23 11:08:52 +08:00
'f01_02_code': 'code',
# 更新:
2026-03-23 11:08:52 +08:00
'f01_03_category': 'category',
# 更新:
2026-03-23 11:08:52 +08:00
'f01_04_status': 'status',
# 更新:
2026-03-23 11:08:52 +08:00
'f01_05_remark': 'remark',
# 更新:
2026-03-23 11:08:52 +08:00
'f02_10_prefix_serial': 'prefixSerial',
# 更新:
2026-03-23 11:08:52 +08:00
'f02_11_version': 'version',
# 更新:
2026-03-23 11:08:52 +08:00
'f02_12_packaging': 'packaging',
# 更新:
2026-03-23 11:08:52 +08:00
'f02_13_rarity': 'rarity',
# 更新:
2026-03-23 11:08:52 +08:00
'f02_14_number_category': 'numberCategory',
# 更新:
2026-03-23 11:08:52 +08:00
'f03_20_is_graded': 'isGraded',
# 更新:
2026-03-23 11:08:52 +08:00
'f03_21_grading_company': 'gradingCompany',
# 更新:
2026-03-23 11:08:52 +08:00
'f03_22_grading_score': 'gradingScore',
# 更新:
2026-03-23 11:08:52 +08:00
'f03_23_three_star': 'threeStar',
# 更新:
2026-03-23 11:08:52 +08:00
'f04_30_special_mark': 'specialMark',
# 更新:
2026-03-23 11:08:52 +08:00
'f04_31_serial_feature': 'serialFeature',
# 更新:
2026-03-23 11:08:52 +08:00
'f04_32_issuer': 'issuer',
# 更新:
2026-03-23 11:08:52 +08:00
'f04_33_issue_year': 'issueYear',
# 更新:
2026-03-23 11:08:52 +08:00
'f04_34_material': 'material',
# 更新:
2026-03-23 11:08:52 +08:00
'f04_35_denomination': 'denomination',
# 更新:
2026-03-23 11:08:52 +08:00
'f04_36_issue_quantity': 'issueQuantity',
# 更新:
2026-03-23 11:08:52 +08:00
'f05_40_cost_price': 'costPrice',
# 更新:
2026-03-23 11:08:52 +08:00
'f05_41_target_price': 'targetPrice',
# 更新:
2026-03-23 11:08:52 +08:00
'f05_42_goal_price': 'goalPrice',
# 更新:
2026-03-23 11:08:52 +08:00
'f05_43_repair_fee': 'repairFee',
# 更新:
2026-03-23 11:08:52 +08:00
'f05_44_grading_fee': 'gradingFee',
# 更新:
2026-03-23 11:08:52 +08:00
'f06_50_purpose': 'purpose',
# 更新:
'images': 'images',
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
return {mapping.get(k, k): v for k, v in data.items()}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 编码生成函数
# 更新:
2026-03-23 11:08:52 +08:00
def generate_code(version: str, user_id: str, db: Session) -> str:
# 更新:
"""自动生成藏品编号 - 按用户独立编码"""
# 更新:
2026-03-23 11:08:52 +08:00
import re
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
# 查询当前用户的非空编码(不与其他用户混算)- 使用行锁防止并发
# 更新:
user_codes = db.query(Collection.f01_02_code).filter(
# 更新:
Collection.f01_02_code.isnot(None),
# 更新:
Collection.f99_91_user_id == user_id
# 更新:
).with_for_update().all()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
max_num = 0
# 更新:
for (code,) in user_codes:
# 更新:
# 处理纯数字编码支持4位和5位
# 更新:
if re.match(r'^\d{4,5}$', code):
# 更新:
try:
# 更新:
num = int(code)
# 更新:
if num > max_num:
# 更新:
max_num = num
# 更新:
except (ValueError, TypeError):
# 更新:
pass
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 当前用户最大号 +1
# 更新:
2026-03-23 11:08:52 +08:00
next_num = max_num + 1
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 如果超过9999使用5位否则使用4位
# 更新:
2026-03-23 11:08:52 +08:00
if next_num > 9999:
# 更新:
2026-03-23 11:08:52 +08:00
return str(next_num).zfill(5)
# 更新:
2026-03-23 11:08:52 +08:00
else:
# 更新:
2026-03-23 11:08:52 +08:00
return str(next_num).zfill(4)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
@router.get("/next-code")
# 更新:
2026-03-23 11:08:52 +08:00
def get_next_code(
# 更新:
2026-03-23 11:08:52 +08:00
current_user: User = Depends(get_current_user),
# 更新:
2026-03-23 11:08:52 +08:00
db: Session = Depends(get_db)
# 更新:
2026-03-23 11:08:52 +08:00
):
# 更新:
2026-03-23 11:08:52 +08:00
"""获取下一个藏品编号"""
# 更新:
2026-03-23 11:08:52 +08:00
next_code = generate_code("2024 龙", current_user.f99_90_id, db)
# 更新:
2026-03-23 11:08:52 +08:00
return {"code": 200, "data": {"nextCode": next_code}}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
@router.get("")
# 更新:
2026-03-23 11:08:52 +08:00
def get_collections(
# 更新:
id: str = Query(None, description="filter by collection id"),
# 更新:
2026-03-23 11:08:52 +08:00
category: Optional[str] = None,
# 更新:
2026-03-23 11:08:52 +08:00
status: Optional[str] = None,
# 更新:
2026-03-23 11:08:52 +08:00
search: Optional[str] = None,
# 更新:
2026-03-23 11:08:52 +08:00
specialMark: Optional[str] = Query(None, description="special mark filter"),
# 更新:
2026-03-23 11:08:52 +08:00
numberCategory: Optional[str] = Query(None, description="number category filter"),
# 更新:
2026-03-23 11:08:52 +08:00
gradingCompany: Optional[str] = Query(None, description="grading company filter"),
# 更新:
2026-03-23 11:08:52 +08:00
gradingScore: Optional[str] = Query(None, description="grading score filter"),
# 更新:
2026-03-23 11:08:52 +08:00
packaging: Optional[str] = Query(None, description="packaging filter"),
# 更新:
2026-03-23 11:08:52 +08:00
rarity: Optional[str] = Query(None, description="rarity filter"),
# 更新:
2026-03-23 11:08:52 +08:00
version: Optional[str] = Query(None, description="version filter"),
# 更新:
2026-03-23 11:08:52 +08:00
profitLoss: Optional[str] = Query(None, description="profit loss filter: profit or loss"),
# 更新:
2026-03-23 11:08:52 +08:00
page: int = Query(1, ge=1),
# 更新:
2026-03-23 11:08:52 +08:00
limit: int = Query(20, ge=1, le=500),
# 更新:
2026-03-23 11:08:52 +08:00
sortBy: str = Query('createdAt'),
# 更新:
2026-03-23 11:08:52 +08:00
sortOrder: str = Query('desc'),
# 更新:
2026-03-23 11:08:52 +08:00
all_users: bool = Query(False, description="return all users data for admin"),
# 更新:
user_id: str = Query(None, description="filter by user id"),
# 更新:
2026-03-23 11:08:52 +08:00
current_user: User = Depends(get_current_user),
# 更新:
2026-03-23 11:08:52 +08:00
db: Session = Depends(get_db)
# 更新:
2026-03-23 11:08:52 +08:00
):
# 更新:
2026-03-23 11:08:52 +08:00
"""获取藏品列表"""
# 更新:
2026-03-23 11:08:52 +08:00
# admin 用户可以看到所有藏品,普通用户只能看到自己的
# 更新:
2026-03-23 11:08:52 +08:00
# 如果指定 all_users=true则返回所有用户藏品
# 更新:
2026-03-23 11:08:52 +08:00
from sqlalchemy.orm import joinedload
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
# 管理员默认查看全库,普通用户只看自己,未登录返回空列表
# 更新:
if current_user is None or current_user.role != "admin":
# 更新:
# 非管理员或未登录用户只能查看自己的藏品(如果没有登录则返回空)
# 更新:
if current_user is None:
# 更新:
return {"data": [], "total": 0, "page": 1, "limit": 20}
# 更新:
query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id)
# 更新:
else:
# 更新:
# 管理员查看所有藏品
# 更新:
2026-03-23 11:08:52 +08:00
query = db.query(Collection, User.f01_01_name.label('owner_name')).join(
# 更新:
2026-03-23 11:08:52 +08:00
User, Collection.f99_91_user_id == User.f99_90_id, isouter=True
# 更新:
2026-03-23 11:08:52 +08:00
)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
# 如果指定了user_id参数则只返回该用户的藏品
# 更新:
if user_id:
# 更新:
query = query.filter(Collection.f99_91_user_id == user_id)
# 更新:
# 更新:
# 按ID精确筛选
# 更新:
if id:
# 更新:
query = query.filter(Collection.f99_90_id == id)
# 更新:
# 更新:
2026-03-23 11:08:52 +08:00
if category:
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(Collection.f01_03_category == category)
# 更新:
2026-03-23 11:08:52 +08:00
if status:
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(Collection.f01_04_status == status)
# 更新:
2026-03-23 11:08:52 +08:00
if specialMark:
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(Collection.f04_30_special_mark == specialMark)
# 更新:
2026-03-23 11:08:52 +08:00
if gradingCompany:
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(Collection.f03_21_grading_company.contains(gradingCompany))
# 更新:
2026-03-23 11:08:52 +08:00
if gradingScore:
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(Collection.f03_22_grading_score.contains(gradingScore))
# 更新:
2026-03-23 11:08:52 +08:00
if numberCategory:
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(Collection.f02_14_number_category.contains(numberCategory))
# 更新:
2026-03-23 11:08:52 +08:00
if packaging:
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(Collection.f02_12_packaging== packaging)
# 更新:
2026-03-23 11:08:52 +08:00
if rarity:
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(Collection.f02_13_rarity.contains(rarity))
# 更新:
2026-03-23 11:08:52 +08:00
if version:
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(Collection.f02_11_version.contains(version))
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 盈亏筛选(只对已售藏品有效)
# 更新:
2026-03-23 11:08:52 +08:00
if profitLoss:
# 更新:
2026-03-23 11:08:52 +08:00
if profitLoss == 'profit':
# 更新:
2026-03-23 11:08:52 +08:00
# 盈利:售价 > 成本价
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f01_04_status == 'sold',
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f05_42_goal_price > Collection.f05_40_cost_price
# 更新:
2026-03-23 11:08:52 +08:00
)
# 更新:
2026-03-23 11:08:52 +08:00
elif profitLoss == 'loss':
# 更新:
2026-03-23 11:08:52 +08:00
# 亏损:售价 <= 成本价
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f01_04_status == 'sold',
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f05_42_goal_price <= Collection.f05_40_cost_price
# 更新:
2026-03-23 11:08:52 +08:00
)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
if search:
# 更新:
2026-03-23 11:08:52 +08:00
query = query.filter(
# 更新:
2026-03-23 11:08:52 +08:00
(Collection.f01_01_name.contains(search)) |
# 更新:
2026-03-23 11:08:52 +08:00
(Collection.f01_05_remark.contains(search))
# 更新:
2026-03-23 11:08:52 +08:00
)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 总数(应用筛选条件后的数量)
# 更新:
2026-03-23 11:08:52 +08:00
total = query.count()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
# 使用joinedload预加载图片避免N+1查询问题
# 更新:
query = query.options(joinedload(Collection.images))
# 更新:
# 更新:
2026-03-23 11:08:52 +08:00
# 分页
# 更新:
2026-03-23 11:08:52 +08:00
data = query.order_by(Collection.f99_92_created_at.desc()) \
# 更新:
2026-03-23 11:08:52 +08:00
.offset((page - 1) * limit) \
# 更新:
2026-03-23 11:08:52 +08:00
.limit(limit) \
# 更新:
2026-03-23 11:08:52 +08:00
.all()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 转换为字典列表并转为 camelCase
# 更新:
2026-03-23 11:08:52 +08:00
data_list = []
# 更新:
2026-03-23 11:08:52 +08:00
for item in data:
# 更新:
2026-03-23 11:08:52 +08:00
# 处理联表查询结果
# 更新:
if current_user is not None and current_user.role == "admin":
# 更新:
2026-03-23 11:08:52 +08:00
collection_item, owner_name = item
# 更新:
2026-03-23 11:08:52 +08:00
else:
# 更新:
2026-03-23 11:08:52 +08:00
collection_item = item
# 更新:
2026-03-23 11:08:52 +08:00
owner_name = None
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
item_dict = {
# 更新:
2026-03-23 11:08:52 +08:00
'f99_90_id': collection_item.f99_90_id,
# 更新:
2026-03-23 11:08:52 +08:00
'f99_91_user_id': collection_item.f99_91_user_id,
# 更新:
2026-03-23 11:08:52 +08:00
'owner_name': owner_name, # 所属用户名(仅管理员可见)
# 更新:
2026-03-23 11:08:52 +08:00
'f01_01_name': collection_item.f01_01_name,
# 更新:
2026-03-23 11:08:52 +08:00
'f01_02_code': collection_item.f01_02_code,
# 更新:
2026-03-23 11:08:52 +08:00
'f01_03_category': collection_item.f01_03_category,
# 更新:
2026-03-23 11:08:52 +08:00
'f01_04_status': collection_item.f01_04_status,
# 更新:
2026-03-23 11:08:52 +08:00
'f01_05_remark': collection_item.f01_05_remark,
# 更新:
2026-03-23 11:08:52 +08:00
'f02_10_prefix_serial': collection_item.f02_10_prefix_serial,
# 更新:
2026-03-23 11:08:52 +08:00
'f02_11_version': collection_item.f02_11_version,
# 更新:
2026-03-23 11:08:52 +08:00
'f02_12_packaging': collection_item.f02_12_packaging,
# 更新:
2026-03-23 11:08:52 +08:00
'f02_13_rarity': collection_item.f02_13_rarity,
# 更新:
2026-03-23 11:08:52 +08:00
'f02_14_number_category': collection_item.f02_14_number_category,
# 更新:
2026-03-23 11:08:52 +08:00
'f03_20_is_graded': collection_item.f03_20_is_graded,
# 更新:
2026-03-23 11:08:52 +08:00
'f03_21_grading_company': collection_item.f03_21_grading_company,
# 更新:
2026-03-23 11:08:52 +08:00
'f03_22_grading_score': collection_item.f03_22_grading_score,
# 更新:
2026-03-23 11:08:52 +08:00
'f03_23_three_star': collection_item.f03_23_three_star,
# 更新:
2026-03-23 11:08:52 +08:00
'f04_30_special_mark': collection_item.f04_30_special_mark,
# 更新:
2026-03-23 11:08:52 +08:00
'f04_31_serial_feature': collection_item.f04_31_serial_feature,
# 更新:
2026-03-23 11:08:52 +08:00
'f04_32_issuer': collection_item.f04_32_issuer,
# 更新:
2026-03-23 11:08:52 +08:00
'f04_33_issue_year': collection_item.f04_33_issue_year,
# 更新:
2026-03-23 11:08:52 +08:00
'f04_34_material': collection_item.f04_34_material,
# 更新:
2026-03-23 11:08:52 +08:00
'f04_35_denomination': collection_item.f04_35_denomination,
# 更新:
2026-03-23 11:08:52 +08:00
'f04_36_issue_quantity': collection_item.f04_36_issue_quantity,
# 更新:
2026-03-23 11:08:52 +08:00
'f05_40_cost_price': float(collection_item.f05_40_cost_price) if collection_item.f05_40_cost_price else None,
# 更新:
2026-03-23 11:08:52 +08:00
'f05_41_target_price': float(collection_item.f05_41_target_price) if collection_item.f05_41_target_price else None,
# 更新:
2026-03-23 11:08:52 +08:00
'f05_42_goal_price': float(collection_item.f05_42_goal_price) if collection_item.f05_42_goal_price else None,
# 更新:
2026-03-23 11:08:52 +08:00
'f05_43_repair_fee': float(collection_item.f05_43_repair_fee) if collection_item.f05_43_repair_fee else None,
# 更新:
2026-03-23 11:08:52 +08:00
'f05_44_grading_fee': float(collection_item.f05_44_grading_fee) if collection_item.f05_44_grading_fee else None,
# 更新:
2026-03-23 11:08:52 +08:00
'f06_50_purpose': collection_item.f06_50_purpose,
# 更新:
2026-03-23 11:08:52 +08:00
'f99_92_created_at': collection_item.f99_92_created_at.isoformat() if collection_item.f99_92_created_at else None,
# 更新:
2026-03-23 11:08:52 +08:00
'images': []
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
# 直接使用预加载的图片数据,无需再查询
# 更新:
for img in collection_item.images:
# 更新:
2026-03-23 11:08:52 +08:00
item_dict['images'].append({
# 更新:
2026-03-23 11:08:52 +08:00
'id': img.id,
# 更新:
2026-03-23 11:08:52 +08:00
'filename': img.filename,
# 更新:
2026-03-23 11:08:52 +08:00
'original_name': img.original_name,
# 更新:
2026-03-23 11:08:52 +08:00
'path': img.path,
# 更新:
2026-03-23 11:08:52 +08:00
'created_at': img.created_at.isoformat() if img.created_at else None
# 更新:
2026-03-23 11:08:52 +08:00
})
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
data_list.append(to_camel_case(item_dict))
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
return {
# 更新:
2026-03-23 11:08:52 +08:00
"data": data_list,
# 更新:
2026-03-23 11:08:52 +08:00
"pagination": {
# 更新:
2026-03-23 11:08:52 +08:00
"page": page,
# 更新:
2026-03-23 11:08:52 +08:00
"limit": limit,
# 更新:
2026-03-23 11:08:52 +08:00
"total": total,
# 更新:
2026-03-23 11:08:52 +08:00
"pages": (total + limit - 1) // limit
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
@router.get("/stats")
# 更新:
2026-03-23 11:08:52 +08:00
def get_stats(
# 更新:
2026-03-23 11:08:52 +08:00
current_user: User = Depends(get_current_user),
# 更新:
2026-03-23 11:08:52 +08:00
db: Session = Depends(get_db)
# 更新:
2026-03-23 11:08:52 +08:00
):
# 更新:
"""获取藏品统计 - 使用SQL聚合查询优化性能"""
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
# 非管理员或未登录用户只能查看自己的藏品
# 更新:
if current_user is None:
# 更新:
return {
# 更新:
"totalCount": 0,
# 更新:
"byCategory": [],
# 更新:
"byStatus": [],
# 更新:
"byGrading": [],
# 更新:
"byPackaging": [],
# 更新:
"byRarity": [],
# 更新:
"byVersion": [],
# 更新:
"byGradingCompany": [],
# 更新:
"byGradingScore": [],
# 更新:
"bySpecialMark": [],
# 更新:
"byNumberCategory": [],
# 更新:
"byProfitLoss": [],
# 更新:
"totalCost": 0,
# 更新:
"totalTarget": 0,
# 更新:
"expectedProfit": 0,
# 更新:
"totalRevenue": 0,
# 更新:
"totalProfit": 0
# 更新:
}
# 更新:
# 更新:
# 构建基础查询条件
# 更新:
is_admin = current_user.role == "admin"
# 更新:
# 更新:
if not is_admin:
# 更新:
base_filter = Collection.f99_91_user_id == current_user.f99_90_id
# 更新:
else:
# 更新:
base_filter = None
# 更新:
# 更新:
# 总数 - 使用SQL COUNT
# 更新:
total_count = db.query(func.count(Collection.f99_90_id)).filter(
# 更新:
base_filter if base_filter is not None else True
# 更新:
).scalar()
# 更新:
if base_filter is not None:
# 更新:
total_count = db.query(func.count(Collection.f99_90_id)).filter(base_filter).scalar()
# 更新:
else:
# 更新:
total_count = db.query(func.count(Collection.f99_90_id)).scalar()
# 更新:
# 更新:
# 按分类统计 - 使用SQL GROUP BY
# 更新:
if base_filter is not None:
# 更新:
by_category = db.query(
# 更新:
Collection.f01_03_category,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(base_filter).group_by(Collection.f01_03_category).all()
# 更新:
# 更新:
by_status = db.query(
# 更新:
Collection.f01_04_status,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(base_filter).group_by(Collection.f01_04_status).all()
# 更新:
# 更新:
by_graded = db.query(
# 更新:
Collection.f03_20_is_graded,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(base_filter).group_by(Collection.f03_20_is_graded).all()
# 更新:
# 更新:
by_packaging = db.query(
# 更新:
Collection.f02_12_packaging,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(base_filter, Collection.f02_12_packaging.isnot(None)).group_by(Collection.f02_12_packaging).all()
# 更新:
# 更新:
by_rarity = db.query(
# 更新:
Collection.f02_13_rarity,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(base_filter, Collection.f02_13_rarity.isnot(None)).group_by(Collection.f02_13_rarity).all()
# 更新:
# 更新:
by_version = db.query(
# 更新:
Collection.f02_11_version,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(base_filter, Collection.f02_11_version.isnot(None)).group_by(Collection.f02_11_version).all()
# 更新:
# 更新:
by_grading_company = db.query(
# 更新:
Collection.f03_21_grading_company,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(base_filter, Collection.f03_20_is_graded == True, Collection.f03_21_grading_company.isnot(None)).group_by(Collection.f03_21_grading_company).all()
# 更新:
# 更新:
by_grading_score = db.query(
# 更新:
Collection.f03_22_grading_score,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(base_filter, Collection.f03_20_is_graded == True, Collection.f03_22_grading_score.isnot(None)).group_by(Collection.f03_22_grading_score).all()
# 更新:
# 更新:
by_special_mark = db.query(
# 更新:
Collection.f04_30_special_mark,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(base_filter, Collection.f04_30_special_mark.isnot(None)).group_by(Collection.f04_30_special_mark).all()
# 更新:
# 更新:
by_number_category = db.query(
# 更新:
Collection.f02_14_number_category,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(base_filter, Collection.f02_14_number_category.isnot(None)).group_by(Collection.f02_14_number_category).all()
# 更新:
# 更新:
# 成本相关统计 - 使用SQL SUM
# 更新:
cost_result = db.query(
# 更新:
func.coalesce(func.sum(Collection.f05_40_cost_price), 0) +
# 更新:
func.coalesce(func.sum(Collection.f05_43_repair_fee), 0) +
# 更新:
func.coalesce(func.sum(Collection.f05_44_grading_fee), 0)
# 更新:
).filter(base_filter).first()
# 更新:
total_cost = cost_result[0] if cost_result else 0
# 更新:
# 更新:
# 预期利润
# 更新:
expected_profit_result = db.query(
# 更新:
func.coalesce(func.sum(Collection.f05_41_target_price - Collection.f05_40_cost_price), 0)
# 更新:
).filter(base_filter, Collection.f05_41_target_price.isnot(None), Collection.f05_41_target_price > 0).first()
# 更新:
expected_profit = expected_profit_result[0] if expected_profit_result else 0
# 更新:
# 更新:
# 已售藏品统计
# 更新:
sold_collections = db.query(Collection).filter(
# 更新:
base_filter,
# 更新:
Collection.f01_04_status == 'sold',
# 更新:
Collection.f05_42_goal_price.isnot(None),
# 更新:
Collection.f05_42_goal_price > 0
# 更新:
).all()
# 更新:
# 更新:
else:
# 更新:
# 管理员查看所有数据
# 更新:
by_category = db.query(
# 更新:
Collection.f01_03_category,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).group_by(Collection.f01_03_category).all()
# 更新:
# 更新:
by_status = db.query(
# 更新:
Collection.f01_04_status,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).group_by(Collection.f01_04_status).all()
# 更新:
# 更新:
by_graded = db.query(
# 更新:
Collection.f03_20_is_graded,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).group_by(Collection.f03_20_is_graded).all()
# 更新:
# 更新:
by_packaging = db.query(
# 更新:
Collection.f02_12_packaging,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(Collection.f02_12_packaging.isnot(None)).group_by(Collection.f02_12_packaging).all()
# 更新:
# 更新:
by_rarity = db.query(
# 更新:
Collection.f02_13_rarity,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(Collection.f02_13_rarity.isnot(None)).group_by(Collection.f02_13_rarity).all()
# 更新:
# 更新:
by_version = db.query(
# 更新:
Collection.f02_11_version,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(Collection.f02_11_version.isnot(None)).group_by(Collection.f02_11_version).all()
# 更新:
# 更新:
by_grading_company = db.query(
# 更新:
Collection.f03_21_grading_company,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(Collection.f03_20_is_graded == True, Collection.f03_21_grading_company.isnot(None)).group_by(Collection.f03_21_grading_company).all()
# 更新:
# 更新:
by_grading_score = db.query(
# 更新:
Collection.f03_22_grading_score,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(Collection.f03_20_is_graded == True, Collection.f03_22_grading_score.isnot(None)).group_by(Collection.f03_22_grading_score).all()
# 更新:
# 更新:
by_special_mark = db.query(
# 更新:
Collection.f04_30_special_mark,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(Collection.f04_30_special_mark.isnot(None)).group_by(Collection.f04_30_special_mark).all()
# 更新:
# 更新:
by_number_category = db.query(
# 更新:
Collection.f02_14_number_category,
# 更新:
func.count(Collection.f99_90_id)
# 更新:
).filter(Collection.f02_14_number_category.isnot(None)).group_by(Collection.f02_14_number_category).all()
# 更新:
# 更新:
# 总成本
# 更新:
cost_result = db.query(
# 更新:
func.coalesce(func.sum(Collection.f05_40_cost_price), 0) +
# 更新:
func.coalesce(func.sum(Collection.f05_43_repair_fee), 0) +
# 更新:
func.coalesce(func.sum(Collection.f05_44_grading_fee), 0)
# 更新:
).first()
# 更新:
total_cost = cost_result[0] if cost_result else 0
# 更新:
# 更新:
# 预期利润
# 更新:
expected_profit_result = db.query(
# 更新:
func.coalesce(func.sum(Collection.f05_41_target_price - Collection.f05_40_cost_price), 0)
# 更新:
).filter(Collection.f05_41_target_price.isnot(None), Collection.f05_41_target_price > 0).first()
# 更新:
expected_profit = expected_profit_result[0] if expected_profit_result else 0
# 更新:
# 更新:
# 已售藏品
# 更新:
sold_collections = db.query(Collection).filter(
# 更新:
Collection.f01_04_status == 'sold',
# 更新:
Collection.f05_42_goal_price.isnot(None),
# 更新:
Collection.f05_42_goal_price > 0
# 更新:
).all()
# 更新:
# 更新:
# 总收入和总利润(已售藏品)
# 更新:
total_revenue = sum(c.f05_42_goal_price or 0 for c in sold_collections)
# 更新:
total_profit = sum(
# 更新:
(c.f05_42_goal_price or 0) - (c.f05_40_cost_price or 0) - (c.f05_43_repair_fee or 0) - (c.f05_44_grading_fee or 0)
# 更新:
for c in sold_collections
# 更新:
)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
# 盈亏统计
# 更新:
profit_count = sum(1 for c in sold_collections if c.f05_42_goal_price and c.f05_40_cost_price and c.f05_42_goal_price > c.f05_40_cost_price)
# 更新:
loss_count = sum(1 for c in sold_collections if c.f05_42_goal_price and c.f05_40_cost_price and c.f05_42_goal_price <= c.f05_40_cost_price)
# 更新:
# 更新:
# 目标价格总和
# 更新:
total_target_result = db.query(func.coalesce(func.sum(Collection.f05_41_target_price), 0)).filter(
# 更新:
base_filter if base_filter is not None else True
# 更新:
).first()
# 更新:
if base_filter is not None:
# 更新:
total_target_result = db.query(func.coalesce(func.sum(Collection.f05_41_target_price), 0)).filter(base_filter).first()
# 更新:
else:
# 更新:
total_target_result = db.query(func.coalesce(func.sum(Collection.f05_41_target_price), 0)).first()
# 更新:
total_target = total_target_result[0] if total_target_result else 0
# 更新:
# 更新:
2026-03-23 11:08:52 +08:00
return {
# 更新:
2026-03-23 11:08:52 +08:00
"totalCount": total_count,
# 更新:
2026-03-23 11:08:52 +08:00
"byCategory": [{"category": c, "count": n} for c, n in by_category],
# 更新:
2026-03-23 11:08:52 +08:00
"byStatus": [{"status": s, "count": n} for s, n in by_status],
# 更新:
2026-03-23 11:08:52 +08:00
"byGrading": [{"isGraded": g, "count": n} for g, n in by_graded],
# 更新:
2026-03-23 11:08:52 +08:00
"byPackaging": [{"packaging": p, "count": n} for p, n in by_packaging],
# 更新:
2026-03-23 11:08:52 +08:00
"byRarity": [{"rarity": r, "count": n} for r, n in by_rarity],
# 更新:
2026-03-23 11:08:52 +08:00
"byVersion": [{"version": v, "count": n} for v, n in by_version],
# 更新:
2026-03-23 11:08:52 +08:00
"byGradingCompany": [{"company": c, "count": n} for c, n in by_grading_company],
# 更新:
2026-03-23 11:08:52 +08:00
"byGradingScore": [{"score": s, "count": n} for s, n in by_grading_score],
# 更新:
2026-03-23 11:08:52 +08:00
"bySpecialMark": [{"mark": m, "count": n} for m, n in by_special_mark],
# 更新:
2026-03-23 11:08:52 +08:00
"byNumberCategory": [{"numberCategory": n, "count": c} for n, c in by_number_category],
# 更新:
2026-03-23 11:08:52 +08:00
"byProfitLoss": [
# 更新:
{"type": "profit", "label": "盈利", "count": profit_count},
# 更新:
{"type": "loss", "label": "亏损", "count": loss_count}
# 更新:
2026-03-23 11:08:52 +08:00
],
# 更新:
"totalCost": total_cost,
# 更新:
"totalTarget": total_target,
# 更新:
2026-03-23 11:08:52 +08:00
"expectedProfit": expected_profit,
# 更新:
2026-03-23 11:08:52 +08:00
"totalRevenue": total_revenue,
# 更新:
2026-03-23 11:08:52 +08:00
"totalProfit": total_profit
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
@router.get("/{collection_id}")
# 更新:
2026-03-23 11:08:52 +08:00
def get_collection(
# 更新:
2026-03-23 11:08:52 +08:00
collection_id: str,
# 更新:
2026-03-23 11:08:52 +08:00
current_user: User = Depends(get_current_user),
# 更新:
2026-03-23 11:08:52 +08:00
db: Session = Depends(get_db)
# 更新:
2026-03-23 11:08:52 +08:00
):
# 更新:
2026-03-23 11:08:52 +08:00
"""获取单个藏品详情"""
# 更新:
2026-03-23 11:08:52 +08:00
result = db.execute(
# 更新:
2026-03-23 11:08:52 +08:00
text("SELECT * FROM collections WHERE f99_90_id = :id"),
# 更新:
2026-03-23 11:08:52 +08:00
{"id": collection_id}
# 更新:
2026-03-23 11:08:52 +08:00
).fetchone()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
if not result:
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=404, detail="E00033: 藏品不存在")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
collection = dict(result._mapping)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 非管理员只能查看自己的藏品
# 更新:
if (current_user is None or current_user.role != "admin") and collection.get('f99_91_user_id') != current_user.f99_90_id:
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=403, detail="无权访问")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
result_dict = {
# 更新:
2026-03-23 11:08:52 +08:00
'f99_90_id': collection.get('f99_90_id'),
# 更新:
2026-03-23 11:08:52 +08:00
'f99_91_user_id': collection.get('f99_91_user_id'),
# 更新:
2026-03-23 11:08:52 +08:00
'f01_01_name': collection.get('f01_01_name'),
# 更新:
2026-03-23 11:08:52 +08:00
'f01_02_code': collection.get('f01_02_code'),
# 更新:
2026-03-23 11:08:52 +08:00
'f01_03_category': collection.get('f01_03_category'),
# 更新:
2026-03-23 11:08:52 +08:00
'f01_04_status': collection.get('f01_04_status'),
# 更新:
2026-03-23 11:08:52 +08:00
'f01_05_remark': collection.get('f01_05_remark'),
# 更新:
2026-03-23 11:08:52 +08:00
'f02_10_prefix_serial': collection.get('f02_10_prefix_serial'),
# 更新:
2026-03-23 11:08:52 +08:00
'f02_11_version': collection.get('f02_11_version'),
# 更新:
2026-03-23 11:08:52 +08:00
'f02_12_packaging': collection.get('f02_12_packaging'),
# 更新:
2026-03-23 11:08:52 +08:00
'f02_13_rarity': collection.get('f02_13_rarity'),
# 更新:
2026-03-23 11:08:52 +08:00
'f02_14_number_category': collection.get('f02_14_number_category'),
# 更新:
2026-03-23 11:08:52 +08:00
'f03_20_is_graded': collection.get('f03_20_is_graded'),
# 更新:
2026-03-23 11:08:52 +08:00
'f03_21_grading_company': collection.get('f03_21_grading_company'),
# 更新:
2026-03-23 11:08:52 +08:00
'f03_22_grading_score': collection.get('f03_22_grading_score'),
# 更新:
2026-03-23 11:08:52 +08:00
'f03_23_three_star': collection.get('f03_23_three_star'),
# 更新:
2026-03-23 11:08:52 +08:00
'f04_30_special_mark': collection.get('f04_30_special_mark'),
# 更新:
2026-03-23 11:08:52 +08:00
'f04_31_serial_feature': collection.get('f04_31_serial_feature'),
# 更新:
2026-03-23 11:08:52 +08:00
'f04_32_issuer': collection.get('f04_32_issuer'),
# 更新:
2026-03-23 11:08:52 +08:00
'f04_33_issue_year': collection.get('f04_33_issue_year'),
# 更新:
2026-03-23 11:08:52 +08:00
'f04_34_material': collection.get('f04_34_material'),
# 更新:
2026-03-23 11:08:52 +08:00
'f04_35_denomination': collection.get('f04_35_denomination'),
# 更新:
2026-03-23 11:08:52 +08:00
'f04_36_issue_quantity': collection.get('f04_36_issue_quantity'),
# 更新:
2026-03-23 11:08:52 +08:00
'f05_40_cost_price': float(collection.get('f05_40_cost_price')) if collection.get('f05_40_cost_price') else None,
# 更新:
2026-03-23 11:08:52 +08:00
'f05_41_target_price': float(collection.get('f05_41_target_price')) if collection.get('f05_41_target_price') else None,
# 更新:
2026-03-23 11:08:52 +08:00
'f05_42_goal_price': float(collection.get('f05_42_goal_price')) if collection.get('f05_42_goal_price') else None,
# 更新:
2026-03-23 11:08:52 +08:00
'f05_43_repair_fee': float(collection.get('f05_43_repair_fee')) if collection.get('f05_43_repair_fee') else None,
# 更新:
2026-03-23 11:08:52 +08:00
'f05_44_grading_fee': float(collection.get('f05_44_grading_fee')) if collection.get('f05_44_grading_fee') else None,
# 更新:
2026-03-23 11:08:52 +08:00
'f06_50_purpose': collection.get('f06_50_purpose'),
# 更新:
2026-03-23 11:08:52 +08:00
'f99_92_created_at': collection.get('f99_92_created_at').isoformat() if collection.get('f99_92_created_at') else None,
# 更新:
2026-03-23 11:08:52 +08:00
'images': []
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 加载图片数据
# 更新:
2026-03-23 11:08:52 +08:00
images = db.query(CollectionImage).filter(
# 更新:
2026-03-23 11:08:52 +08:00
CollectionImage.collection_id == collection_id
# 更新:
2026-03-23 11:08:52 +08:00
).all()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
for img in images:
# 更新:
2026-03-23 11:08:52 +08:00
result_dict['images'].append({
# 更新:
2026-03-23 11:08:52 +08:00
'id': img.id,
# 更新:
2026-03-23 11:08:52 +08:00
'filename': img.filename,
# 更新:
2026-03-23 11:08:52 +08:00
'original_name': img.original_name,
# 更新:
2026-03-23 11:08:52 +08:00
'path': img.path,
# 更新:
2026-03-23 11:08:52 +08:00
'created_at': img.created_at.isoformat() if img.created_at else None
# 更新:
2026-03-23 11:08:52 +08:00
})
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
return to_camel_case(result_dict)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
@router.post("")
# 更新:
2026-03-23 11:08:52 +08:00
def create_collection(
# 更新:
2026-03-23 11:08:52 +08:00
collection_data: CollectionCreate,
# 更新:
2026-03-23 11:08:52 +08:00
force: bool = False, # 是否强制保存(忽略重复警告)
# 更新:
2026-03-23 11:08:52 +08:00
current_user: User = Depends(get_current_user),
# 更新:
2026-03-23 11:08:52 +08:00
db: Session = Depends(get_db)
# 更新:
2026-03-23 11:08:52 +08:00
):
# 更新:
2026-03-23 11:08:52 +08:00
"""创建藏品 - 支持冠字号查重"""
# 更新:
2026-03-23 11:08:52 +08:00
from app.core.logging_config import logger
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 自动生成编码
# 更新:
2026-03-23 11:08:52 +08:00
final_code = collection_data.f01_02_code or generate_code(
# 更新:
2026-03-23 11:08:52 +08:00
collection_data.f02_11_version or '2024 龙',
# 更新:
2026-03-23 11:08:52 +08:00
current_user.f99_90_id,
# 更新:
2026-03-23 11:08:52 +08:00
db
# 更新:
2026-03-23 11:08:52 +08:00
)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 编号查重(如果提供了编号且不是强制保存)
# 更新:
2026-03-23 11:08:52 +08:00
if not force and final_code:
# 更新:
2026-03-23 11:08:52 +08:00
existing_code = db.query(Collection).filter(
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f01_02_code == final_code,
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f99_91_user_id == current_user.f99_90_id
# 更新:
2026-03-23 11:08:52 +08:00
).first()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
if existing_code:
# 更新:
2026-03-23 11:08:52 +08:00
logger.warning(f"发现重复编号:{final_code}, 已存在藏品 ID: {existing_code.f99_90_id}")
# 更新:
2026-03-23 11:08:52 +08:00
return {
# 更新:
2026-03-23 11:08:52 +08:00
"error": {
# 更新:
2026-03-23 11:08:52 +08:00
"code": "DUPLICATE_CODE",
# 更新:
2026-03-23 11:08:52 +08:00
"message": f"藏品编号 {final_code} 已存在,请使用其他编号"
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 冠字号查重(如果提供了冠字号且不是强制保存)
# 更新:
2026-03-23 11:08:52 +08:00
if not force and collection_data.f02_10_prefix_serial:
# 更新:
2026-03-23 11:08:52 +08:00
# 查询当前用户是否有相同冠字号的藏品
# 更新:
2026-03-23 11:08:52 +08:00
existing = db.query(Collection).filter(
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f02_10_prefix_serial == collection_data.f02_10_prefix_serial,
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f99_91_user_id == current_user.f99_90_id
# 更新:
2026-03-23 11:08:52 +08:00
).first()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
if existing:
# 更新:
2026-03-23 11:08:52 +08:00
logger.warning(f"发现重复冠字号:{collection_data.f02_10_prefix_serial}, 已存在藏品 ID: {existing.f99_90_id}")
# 更新:
2026-03-23 11:08:52 +08:00
# 返回警告信息,让前端询问用户是否继续
# 更新:
2026-03-23 11:08:52 +08:00
return {
# 更新:
2026-03-23 11:08:52 +08:00
"warning": {
# 更新:
2026-03-23 11:08:52 +08:00
"code": "DUPLICATE_SERIAL",
# 更新:
2026-03-23 11:08:52 +08:00
"message": f"发现重复冠字号:{collection_data.f02_10_prefix_serial}",
# 更新:
2026-03-23 11:08:52 +08:00
"existing_collection": {
# 更新:
2026-03-23 11:08:52 +08:00
"id": existing.f99_90_id,
# 更新:
2026-03-23 11:08:52 +08:00
"name": existing.f01_01_name,
# 更新:
2026-03-23 11:08:52 +08:00
"code": existing.f01_02_code,
# 更新:
2026-03-23 11:08:52 +08:00
"prefix_serial": existing.f02_10_prefix_serial
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
},
# 更新:
2026-03-23 11:08:52 +08:00
"data": {
# 更新:
2026-03-23 11:08:52 +08:00
"ask_continue": True
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
# 自动分类:如果未提供号码分类,则根据冠字号自动分类
# 更新:
if not collection_data.f02_14_number_category and collection_data.f02_10_prefix_serial:
# 更新:
from app.utils.number_category import get_number_category
# 更新:
collection_data.f02_14_number_category = get_number_category(collection_data.f02_10_prefix_serial)
# 更新:
# 更新:
2026-03-23 11:08:52 +08:00
collection = Collection(
# 更新:
2026-03-23 11:08:52 +08:00
f99_91_user_id=current_user.f99_90_id,
# 更新:
2026-03-23 11:08:52 +08:00
f01_01_name=collection_data.f01_01_name,
# 更新:
2026-03-23 11:08:52 +08:00
f01_02_code=final_code,
# 更新:
2026-03-23 11:08:52 +08:00
f01_03_category=collection_data.f01_03_category,
# 更新:
2026-03-23 11:08:52 +08:00
f01_04_status=collection_data.f01_04_status or "in_collection",
# 更新:
2026-03-23 11:08:52 +08:00
f01_05_remark=collection_data.f01_05_remark,
# 更新:
2026-03-23 11:08:52 +08:00
f02_10_prefix_serial=collection_data.f02_10_prefix_serial,
# 更新:
2026-03-23 11:08:52 +08:00
f02_11_version=collection_data.f02_11_version,
# 更新:
2026-03-23 11:08:52 +08:00
f02_12_packaging=collection_data.f02_12_packaging,
# 更新:
2026-03-23 11:08:52 +08:00
f02_13_rarity=collection_data.f02_13_rarity,
# 更新:
2026-03-23 11:08:52 +08:00
f02_14_number_category=collection_data.f02_14_number_category,
# 更新:
2026-03-23 11:08:52 +08:00
f03_20_is_graded=collection_data.f03_20_is_graded or False,
# 更新:
2026-03-23 11:08:52 +08:00
f03_21_grading_company=collection_data.f03_21_grading_company,
# 更新:
2026-03-23 11:08:52 +08:00
f03_22_grading_score=collection_data.f03_22_grading_score,
# 更新:
2026-03-23 11:08:52 +08:00
f03_23_three_star=collection_data.f03_23_three_star or False,
# 更新:
2026-03-23 11:08:52 +08:00
f04_30_special_mark=collection_data.f04_30_special_mark,
# 更新:
2026-03-23 11:08:52 +08:00
f04_31_serial_feature=collection_data.f04_31_serial_feature,
# 更新:
2026-03-23 11:08:52 +08:00
f04_32_issuer=collection_data.f04_32_issuer,
# 更新:
2026-03-23 11:08:52 +08:00
f04_33_issue_year=collection_data.f04_33_issue_year,
# 更新:
2026-03-23 11:08:52 +08:00
f04_34_material=collection_data.f04_34_material,
# 更新:
2026-03-23 11:08:52 +08:00
f04_35_denomination=collection_data.f04_35_denomination,
# 更新:
2026-03-23 11:08:52 +08:00
f04_36_issue_quantity=collection_data.f04_36_issue_quantity,
# 更新:
2026-03-23 11:08:52 +08:00
f05_40_cost_price=collection_data.f05_40_cost_price,
# 更新:
2026-03-23 11:08:52 +08:00
f05_41_target_price=collection_data.f05_41_target_price,
# 更新:
2026-03-23 11:08:52 +08:00
f05_42_goal_price=collection_data.f05_42_goal_price,
# 更新:
2026-03-23 11:08:52 +08:00
f05_43_repair_fee=collection_data.f05_43_repair_fee,
# 更新:
2026-03-23 11:08:52 +08:00
f05_44_grading_fee=collection_data.f05_44_grading_fee,
# 更新:
2026-03-23 11:08:52 +08:00
f06_50_purpose=collection_data.f06_50_purpose
# 更新:
2026-03-23 11:08:52 +08:00
)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
db.add(collection)
# 更新:
2026-03-23 11:08:52 +08:00
db.commit()
# 更新:
2026-03-23 11:08:52 +08:00
db.refresh(collection)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
return {
# 更新:
2026-03-23 11:08:52 +08:00
'f99_90_id': collection.f99_90_id,
# 更新:
2026-03-23 11:08:52 +08:00
'f99_91_user_id': collection.f99_91_user_id,
# 更新:
2026-03-23 11:08:52 +08:00
'f01_01_name': collection.f01_01_name,
# 更新:
2026-03-23 11:08:52 +08:00
'f01_02_code': collection.f01_02_code,
# 更新:
2026-03-23 11:08:52 +08:00
'f01_03_category': collection.f01_03_category,
# 更新:
2026-03-23 11:08:52 +08:00
'f01_04_status': collection.f01_04_status,
# 更新:
2026-03-23 11:08:52 +08:00
'f01_05_remark': collection.f01_05_remark,
# 更新:
2026-03-23 11:08:52 +08:00
'f99_92_created_at': collection.f99_92_created_at.isoformat() if collection.f99_92_created_at else None,
# 更新:
2026-03-23 11:08:52 +08:00
'message': '创建成功'
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
@router.put("/{collection_id}")
# 更新:
2026-03-23 11:08:52 +08:00
def update_collection(
# 更新:
2026-03-23 11:08:52 +08:00
collection_id: str,
# 更新:
2026-03-23 11:08:52 +08:00
collection_data: CollectionUpdate,
# 更新:
2026-03-23 11:08:52 +08:00
current_user: User = Depends(get_current_user),
# 更新:
2026-03-23 11:08:52 +08:00
db: Session = Depends(get_db)
# 更新:
2026-03-23 11:08:52 +08:00
):
# 更新:
2026-03-23 11:08:52 +08:00
"""更新藏品"""
# 更新:
2026-03-23 11:08:52 +08:00
collection = db.query(Collection).filter(
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f99_90_id == collection_id,
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f99_91_user_id == current_user.f99_90_id
# 更新:
2026-03-23 11:08:52 +08:00
).first()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
if not collection:
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=404, detail="E00033: 藏品不存在")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新字段 - 使用 model_fields_set 检查哪些字段被设置
# 更新:
2026-03-23 11:08:52 +08:00
for field_name in collection_data.model_fields_set:
# 更新:
2026-03-23 11:08:52 +08:00
value = getattr(collection_data, field_name)
# 更新:
2026-03-23 11:08:52 +08:00
if value is not None:
# 更新:
2026-03-23 11:08:52 +08:00
setattr(collection, field_name, value)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
db.commit()
# 更新:
2026-03-23 11:08:52 +08:00
db.refresh(collection)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
return {
# 更新:
2026-03-23 11:08:52 +08:00
"f99_90_id": collection.f99_90_id,
# 更新:
2026-03-23 11:08:52 +08:00
"message": "更新成功"
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
@router.delete("/{collection_id}")
# 更新:
2026-03-23 11:08:52 +08:00
def delete_collection(
# 更新:
2026-03-23 11:08:52 +08:00
collection_id: str,
# 更新:
2026-03-23 11:08:52 +08:00
current_user: User = Depends(get_current_user),
# 更新:
2026-03-23 11:08:52 +08:00
db: Session = Depends(get_db)
# 更新:
2026-03-23 11:08:52 +08:00
):
# 更新:
2026-03-23 11:08:52 +08:00
"""删除藏品"""
# 更新:
2026-03-23 11:08:52 +08:00
# 验证权限并检查是否存在
# 更新:
2026-03-23 11:08:52 +08:00
collection = db.query(Collection).filter(
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f99_90_id == collection_id,
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f99_91_user_id == current_user.f99_90_id
# 更新:
2026-03-23 11:08:52 +08:00
).first()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
if not collection:
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=404, detail="E00033: 藏品不存在")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 使用原生 SQL 删除(避免 ORM 级联查询字段不匹配问题)
# 更新:
2026-03-23 11:08:52 +08:00
from sqlalchemy import text
# 更新:
2026-03-23 11:08:52 +08:00
# 1. 删除关联的 operationsf99_91_user_id 关联到 collections.f99_90_id
# 更新:
2026-03-23 11:08:52 +08:00
db.execute(text("DELETE FROM operations WHERE f99_91_user_id = :id"), {"id": collection_id})
# 更新:
2026-03-23 11:08:52 +08:00
# 2. 删除关联的图片
# 更新:
2026-03-23 11:08:52 +08:00
db.execute(text("DELETE FROM collection_images WHERE collection_id = :id"), {"id": collection_id})
# 更新:
2026-03-23 11:08:52 +08:00
# 3. 删除藏品本身
# 更新:
2026-03-23 11:08:52 +08:00
db.execute(text("DELETE FROM collections WHERE f99_90_id = :id"), {"id": collection_id})
# 更新:
2026-03-23 11:08:52 +08:00
db.commit()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
return {"message": "删除成功"}
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
@router.post("/upload-image")
# 更新:
2026-03-23 11:08:52 +08:00
async def upload_image(
# 更新:
2026-03-23 11:08:52 +08:00
collection_id: str = None,
# 更新:
2026-03-23 11:08:52 +08:00
file: UploadFile = File(...),
# 更新:
2026-03-23 11:08:52 +08:00
current_user: User = Depends(get_current_user),
# 更新:
2026-03-23 11:08:52 +08:00
db: Session = Depends(get_db)
# 更新:
2026-03-23 11:08:52 +08:00
):
# 更新:
2026-03-23 11:08:52 +08:00
"""上传藏品图片 - 文件名格式:用户名 - 藏品编号 - 冠字号"""
# 更新:
2026-03-23 11:08:52 +08:00
try:
# 更新:
2026-03-23 11:08:52 +08:00
# 验证藏品是否存在
# 更新:
2026-03-23 11:08:52 +08:00
collection = db.query(Collection).filter(
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f99_90_id == collection_id
# 更新:
2026-03-23 11:08:52 +08:00
).first()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
if not collection:
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=404, detail="E00033: 藏品不存在")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 获取用户信息(用于文件名)
# 更新:
2026-03-23 11:08:52 +08:00
owner = db.query(User).filter(User.f99_90_id == collection.f99_91_user_id).first()
# 更新:
2026-03-23 11:08:52 +08:00
username = owner.f01_01_name if owner else "unknown"
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 获取藏品信息(用于文件名)
# 更新:
2026-03-23 11:08:52 +08:00
code = collection.f01_02_code or "0000"
# 更新:
2026-03-23 11:08:52 +08:00
prefix_serial = collection.f02_10_prefix_serial or ""
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 检查文件类型
# 更新:
2026-03-23 11:08:52 +08:00
if not file.content_type.startswith('image/'):
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=400, detail="E00038: 只能上传图片文件")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 检查文件大小(限制 10MB
# 更新:
2026-03-23 11:08:52 +08:00
file_size = 0
# 更新:
2026-03-23 11:08:52 +08:00
content = await file.read()
# 更新:
2026-03-23 11:08:52 +08:00
file_size = len(content)
# 更新:
2026-03-23 11:08:52 +08:00
if file_size > 10 * 1024 * 1024: # 10MB
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=400, detail=f"E00039: 图片大小不能超过 10MB当前{file_size // 1024 // 1024}MB")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 生成OSS存储路径
# 更新:
2026-03-23 11:08:52 +08:00
user_id = collection.f99_91_user_id
# 更新:
2026-03-23 11:08:52 +08:00
file_extension = file.filename.split('.')[-1] if '.' in file.filename else 'jpg'
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 清理特殊字符
# 更新:
2026-03-23 11:08:52 +08:00
clean_username = re.sub(r'[^\w\u4e00-\u9fff\-]', '', username)
# 更新:
2026-03-23 11:08:52 +08:00
clean_serial = re.sub(r'[^\w\u4e00-\u9fff\-]', '', prefix_serial)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 文件名格式:用户名-藏品编号-冠字号.jpg
# 更新:
2026-03-23 11:08:52 +08:00
if clean_serial:
# 更新:
2026-03-23 11:08:52 +08:00
filename = f"{clean_username}-{code}-{clean_serial}.{file_extension}"
# 更新:
2026-03-23 11:08:52 +08:00
else:
# 更新:
2026-03-23 11:08:52 +08:00
filename = f"{clean_username}-{code}.{file_extension}"
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 生成OSS key
# 更新:
2026-03-23 11:08:52 +08:00
oss_key, unique_name = get_oss_path("collections", user_id=user_id, filename=filename)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 上传到OSS
# 更新:
2026-03-23 11:08:52 +08:00
image_url = upload_to_oss(content, oss_key)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 创建图片记录保存OSS URL
# 更新:
2026-03-23 11:08:52 +08:00
image = CollectionImage(
# 更新:
2026-03-23 11:08:52 +08:00
id=str(uuid.uuid4()),
# 更新:
2026-03-23 11:08:52 +08:00
collection_id=collection_id,
# 更新:
2026-03-23 11:08:52 +08:00
filename=unique_name,
# 更新:
2026-03-23 11:08:52 +08:00
original_name=file.filename,
# 更新:
2026-03-23 11:08:52 +08:00
path=image_url # 保存OSS URL
# 更新:
2026-03-23 11:08:52 +08:00
)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
db.add(image)
# 更新:
2026-03-23 11:08:52 +08:00
db.commit()
# 更新:
2026-03-23 11:08:52 +08:00
db.refresh(image)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
logger.info(f"图片上传成功:{image_url}, collection_id={collection_id}")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
return {
# 更新:
2026-03-23 11:08:52 +08:00
"message": "上传成功",
# 更新:
2026-03-23 11:08:52 +08:00
"image_id": image.id,
# 更新:
2026-03-23 11:08:52 +08:00
"filename": unique_name,
# 更新:
2026-03-23 11:08:52 +08:00
"url": image_url
# 更新:
2026-03-23 11:08:52 +08:00
}
# 更新:
2026-03-23 11:08:52 +08:00
except HTTPException:
# 更新:
2026-03-23 11:08:52 +08:00
raise
# 更新:
2026-03-23 11:08:52 +08:00
except Exception as e:
# 更新:
2026-03-23 11:08:52 +08:00
logger.error(f"图片上传失败:{str(e)}")
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=500, detail="上传失败")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
@router.delete("/images/{image_id}")
# 更新:
2026-03-23 11:08:52 +08:00
async def delete_image(
# 更新:
2026-03-23 11:08:52 +08:00
image_id: str,
# 更新:
2026-03-23 11:08:52 +08:00
current_user: User = Depends(get_current_user),
# 更新:
2026-03-23 11:08:52 +08:00
db: Session = Depends(get_db)
# 更新:
2026-03-23 11:08:52 +08:00
):
# 更新:
2026-03-23 11:08:52 +08:00
"""删除藏品图片"""
# 更新:
2026-03-23 11:08:52 +08:00
try:
# 更新:
2026-03-23 11:08:52 +08:00
# 查找图片记录
# 更新:
2026-03-23 11:08:52 +08:00
image = db.query(CollectionImage).filter(
# 更新:
2026-03-23 11:08:52 +08:00
CollectionImage.id == image_id
# 更新:
2026-03-23 11:08:52 +08:00
).first()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
if not image:
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=404, detail="E00033: 图片不存在")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 检查权限
# 更新:
2026-03-23 11:08:52 +08:00
collection = db.query(Collection).filter(
# 更新:
2026-03-23 11:08:52 +08:00
Collection.f99_90_id == image.collection_id
# 更新:
2026-03-23 11:08:52 +08:00
).first()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
if collection and (current_user is None or current_user.role != "admin") and collection.f99_91_user_id != current_user.f99_90_id:
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=403, detail="E00014: 无权删除此图片")
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 删除OSS文件如果path是OSS URL
# 更新:
2026-03-23 11:08:52 +08:00
if image.path and image.path.startswith("https://"):
# 更新:
2026-03-23 11:08:52 +08:00
# 从OSS URL提取key
# 更新:
2026-03-23 11:08:52 +08:00
try:
# 更新:
2026-03-23 11:08:52 +08:00
oss_key = image.path.replace("https://jiachenlong-oss.oss-cn-hangzhou.aliyuncs.com/", "")
# 更新:
2026-03-23 11:08:52 +08:00
delete_from_oss(oss_key)
# 更新:
2026-03-23 11:08:52 +08:00
except Exception as e:
# 更新:
2026-03-23 11:08:52 +08:00
logger.warning(f"OSS文件删除失败: {e}")
# 更新:
2026-03-23 11:08:52 +08:00
elif image.path and os.path.exists(image.path):
# 更新:
2026-03-23 11:08:52 +08:00
# 兼容旧的本地上传
# 更新:
2026-03-23 11:08:52 +08:00
os.remove(image.path)
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
# 删除数据库记录
# 更新:
2026-03-23 11:08:52 +08:00
db.delete(image)
# 更新:
2026-03-23 11:08:52 +08:00
db.commit()
# 更新:
2026-03-23 11:08:52 +08:00
# 更新:
2026-03-23 11:08:52 +08:00
return {"message": "删除成功"}
# 更新:
2026-03-23 11:08:52 +08:00
except HTTPException:
# 更新:
2026-03-23 11:08:52 +08:00
raise
# 更新:
2026-03-23 11:08:52 +08:00
except Exception as e:
# 更新:
2026-03-23 11:08:52 +08:00
logger.error(f"图片删除失败:{str(e)}")
# 更新:
2026-03-23 11:08:52 +08:00
raise HTTPException(status_code=500, detail="删除失败")
# 更新: