800 lines
32 KiB
Python
800 lines
32 KiB
Python
# 藏品路由 - 使用字段编码
|
||
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
|
||
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.role == "admin":
|
||
# 联表查询获取用户名
|
||
query = db.query(Collection, User.f01_01_name.label('owner_name')).join(
|
||
User, Collection.f99_91_user_id == User.f99_90_id, isouter=True
|
||
)
|
||
else:
|
||
query = db.query(Collection).filter(Collection.f99_91_user_id == current_user.f99_90_id)
|
||
|
||
# 如果指定了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()
|
||
|
||
# 分页
|
||
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.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': []
|
||
}
|
||
|
||
# 加载图片数据
|
||
from app.models.models import CollectionImage
|
||
images = db.query(CollectionImage).filter(
|
||
CollectionImage.collection_id == collection_item.f99_90_id
|
||
).all()
|
||
|
||
for img in 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)
|
||
):
|
||
"""获取藏品统计 - 使用数据库聚合查询优化性能"""
|
||
from sqlalchemy import case, func
|
||
from sqlalchemy.sql import expression
|
||
|
||
# 基础过滤条件
|
||
if current_user.role == "admin":
|
||
base_filter = True
|
||
else:
|
||
base_filter = Collection.f99_91_user_id == current_user.f99_90_id
|
||
|
||
# 总数 - 使用 COUNT
|
||
total_count = db.query(func.count(Collection.f99_90_id)).filter(base_filter).scalar() or 0
|
||
|
||
# 按分类统计
|
||
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()
|
||
|
||
# 财务统计 - 使用数据库 SUM
|
||
# 总成本 = SUM(cost_price + repair_fee + grading_fee)
|
||
cost_result = db.query(
|
||
func.coalesce(func.sum(
|
||
func.coalesce(Collection.f05_40_cost_price, 0) +
|
||
func.coalesce(Collection.f05_43_repair_fee, 0) +
|
||
func.coalesce(Collection.f05_44_grading_fee, 0)
|
||
), 0)
|
||
).filter(base_filter).scalar() or 0
|
||
|
||
# 预期利润 = SUM(target_price - cost_price) where target_price > 0
|
||
target_result = db.query(
|
||
func.coalesce(func.sum(
|
||
func.coalesce(Collection.f05_41_target_price, 0) -
|
||
func.coalesce(Collection.f05_40_cost_price, 0)
|
||
), 0)
|
||
).filter(base_filter, Collection.f05_41_target_price > 0).scalar() or 0
|
||
|
||
expected_profit = target_result
|
||
|
||
# 已售商品过滤条件
|
||
sold_filter = base_filter & (Collection.f01_04_status == 'sold') & (Collection.f05_42_goal_price > 0)
|
||
|
||
# 总收入
|
||
total_revenue = db.query(
|
||
func.coalesce(func.sum(Collection.f05_42_goal_price), 0)
|
||
).filter(sold_filter).scalar() or 0
|
||
|
||
# 总利润 = SUM(goal_price - cost_price - repair_fee - grading_fee)
|
||
total_profit = db.query(
|
||
func.coalesce(func.sum(
|
||
func.coalesce(Collection.f05_42_goal_price, 0) -
|
||
func.coalesce(Collection.f05_40_cost_price, 0) -
|
||
func.coalesce(Collection.f05_43_repair_fee, 0) -
|
||
func.coalesce(Collection.f05_44_grading_fee, 0)
|
||
), 0)
|
||
).filter(sold_filter).scalar() or 0
|
||
|
||
# 盈亏统计
|
||
profit_count = db.query(func.count(Collection.f99_90_id)).filter(
|
||
sold_filter,
|
||
Collection.f05_42_goal_price > Collection.f05_40_cost_price
|
||
).scalar() or 0
|
||
|
||
loss_count = db.query(func.count(Collection.f99_90_id)).filter(
|
||
sold_filter,
|
||
Collection.f05_42_goal_price <= Collection.f05_40_cost_price
|
||
).scalar() or 0
|
||
|
||
# 目标总价
|
||
total_target = db.query(
|
||
func.coalesce(func.sum(func.coalesce(Collection.f05_41_target_price, 0)), 0)
|
||
).filter(base_filter).scalar() or 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": float(cost_result),
|
||
"totalTarget": float(total_target),
|
||
"expectedProfit": float(expected_profit),
|
||
"totalRevenue": float(total_revenue),
|
||
"totalProfit": float(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.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
|
||
}
|
||
}
|
||
|
||
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.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="删除失败")
|