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