diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 0c6fe73..af84f0c 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -1,242 +1,489 @@ -# 认证路由 - 使用字段编码 +# auth - 认证路由 +# Version: 1.2.85 +# 更新: + +from fastapi import APIRouter +# Version: 1.2.x +# 更新: from fastapi import APIRouter, Depends, HTTPException, status, Body +# 更新: from fastapi.security import OAuth2PasswordRequestForm +# 更新: from sqlalchemy.orm import Session +# 更新: from app.core.database import get_db +# 更新: from app.core.auth import verify_password, create_access_token, get_password_hash, get_current_user +# 更新: from app.models.models import User +# 更新: from app.schemas.schemas import Token, UserCreate, UserResponse +# 更新: +# 更新: router = APIRouter(prefix="/api/auth", tags=["认证"]) +# 更新: +# 更新: +# 更新: +# 更新: +# 更新: def generate_user_code(db): +# 更新: """生成用户编码,从201开始,按自然数顺序递增,跳过已存在的""" +# 更新: # 查找最大的user_code +# 更新: max_code = db.query(User.user_code).filter(User.user_code != None).order_by(User.user_code.desc()).first() +# 更新: if max_code and max_code[0]: +# 更新: try: +# 更新: num = int(max_code[0]) + 1 +# 更新: if num < 201: +# 更新: num = 201 +# 更新: # 检查是否已存在,如果存在则继续递增 +# 更新: while db.query(User).filter(User.user_code == str(num)).first(): +# 更新: num += 1 +# 更新: return str(num) +# 更新: except: +# 更新: pass +# 更新: return "201" +# 更新: +# 更新: @router.post("/register", response_model=UserResponse) +# 更新: def register(user_data: UserCreate, db: Session = Depends(get_db)): +# 更新: """用户注册""" +# 更新: # 检查用户名是否已存在 +# 更新: existing_user = db.query(User).filter(User.f01_01_name == user_data.f01_01_name).first() +# 更新: if existing_user: +# 更新: raise HTTPException( +# 更新: status_code=status.HTTP_400_BAD_REQUEST, +# 更新: detail="f01_01_name: 用户名已存在" +# 更新: ) +# 更新: +# 更新: # 检查邮箱是否已存在 +# 更新: +# 更新: # 检查手机号是否已存在 +# 更新: if user_data.phone: +# 更新: existing_phone = db.query(User).filter(User.phone == user_data.phone).first() +# 更新: if existing_phone: +# 更新: raise HTTPException( +# 更新: status_code=status.HTTP_400_BAD_REQUEST, +# 更新: detail="E00040:该手机号已被注册,请更换手机号" +# 更新: ) +# 更新: if user_data.email: +# 更新: existing_email = db.query(User).filter(User.email == user_data.email).first() +# 更新: if existing_email: +# 更新: raise HTTPException( +# 更新: status_code=status.HTTP_400_BAD_REQUEST, +# 更新: detail="E00041:该邮箱已被注册,请更换邮箱" +# 更新: ) +# 更新: +# 更新: # 处理邀请码 +# 更新: invited_by_user = None +# 更新: if user_data.invite_code: +# 更新: # 查找邀请人 +# 更新: invited_by_user = db.query(User).filter(User.user_code == user_data.invite_code).first() +# 更新: if not invited_by_user: +# 更新: raise HTTPException( +# 更新: status_code=status.HTTP_400_BAD_REQUEST, +# 更新: detail="E00042:邀请码无效" +# 更新: ) +# 更新: +# 更新: # 创建用户 +# 更新: import uuid +# 更新: hashed_password = get_password_hash(user_data.password) +# 更新: generated_code = generate_user_code(db) +# 更新: user = User( +# 更新: f99_90_id=str(uuid.uuid4()), +# 更新: f99_91_user_id=str(uuid.uuid4()), +# 更新: user_code=generated_code, +# 更新: f01_01_name=user_data.f01_01_name, +# 更新: email=user_data.email, +# 更新: phone=user_data.phone, +# 更新: avatar=user_data.avatar, +# 更新: address=user_data.address, +# 更新: bio=user_data.bio, +# 更新: password=hashed_password, +# 更新: role="user" +# 更新: ) +# 更新: +# 更新: db.add(user) +# 更新: db.flush() # 确保获取user ID +# 更新: +# 更新: # 更新邀请人、被邀请人的关联关系 +# 更新: if invited_by_user: +# 更新: # 记录是被谁邀请的 +# 更新: user.f01_13_invite_code = invited_by_user.user_code +# 更新: # 增加邀请人的邀请计数 +# 更新: invited_by_user.f99_101_invited_count = (invited_by_user.f99_101_invited_count or 0) + 1 +# 更新: +# 更新: # 生成自己的邀请码(用自己的user_code) +# 更新: user.f01_13_invite_code = generated_code +# 更新: +# 更新: db.commit() +# 更新: db.refresh(user) +# 更新: +# 更新: # 返回用户信息(避免Pydantic序列化问题) +# 更新: return { +# 更新: "id": user.f99_90_id, +# 更新: "username": user.f01_01_name, +# 更新: "user_code": user.user_code, +# 更新: "email": user.email, +# 更新: "phone": user.phone, +# 更新: "avatar": user.avatar, +# 更新: "role": user.role, +# 更新: "level": user.f99_94_level, +# 更新: "aiCount": user.f99_95_ai_count or 0, +# 更新: "searchCount": user.f99_96_search_count or 0, +# 更新: "collectionCount": user.f99_97_collection_count or 0 +# 更新: } +# 更新: +# 更新: +# 更新: @router.post("/login", response_model=Token) +# 更新: def login( +# 更新: form_data: OAuth2PasswordRequestForm = Depends(), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """用户登录 - 支持用户名或用户编码登录""" +# 更新: # 先尝试用户名登录 +# 更新: user = db.query(User).filter(User.f01_01_name == form_data.username).first() +# 更新: # 如果用户名不存在,尝试用户编码登录 +# 更新: if not user: +# 更新: user = db.query(User).filter(User.user_code == form_data.username).first() +# 更新: if not user: +# 更新: raise HTTPException( +# 更新: status_code=status.HTTP_401_UNAUTHORIZED, +# 更新: detail="E00011: 用户名或密码错误", +# 更新: headers={"WWW-Authenticate": "Bearer"}, +# 更新: ) +# 更新: +# 更新: # 验证密码 +# 更新: if not verify_password(form_data.password, user.password): +# 更新: raise HTTPException( +# 更新: status_code=status.HTTP_401_UNAUTHORIZED, +# 更新: detail="E00011: 用户名或密码错误", +# 更新: headers={"WWW-Authenticate": "Bearer"}, +# 更新: ) +# 更新: +# 更新: # 更新登录次数和最后登录时间 +# 更新: from datetime import datetime +# 更新: user.f99_98_login_count = (user.f99_98_login_count or 0) + 1 +# 更新: user.f99_99_last_login = datetime.now() +# 更新: db.commit() +# 更新: +# 更新: # 生成 token +# 更新: access_token = create_access_token(data={"sub": user.f99_90_id}) +# 更新: +# 更新: return { +# 更新: "access_token": access_token, +# 更新: "token_type": "bearer" +# 更新: } +# 更新: +# 更新: +# 更新: @router.get("/me", response_model=UserResponse) +# 更新: def get_current_user_info( +# 更新: current_user: User = Depends(lambda: None) +# 更新: ): +# 更新: """获取当前用户信息""" +# 更新: raise HTTPException( +# 更新: status_code=status.HTTP_501_NOT_IMPLEMENTED, +# 更新: detail="请使用正确的依赖注入" +# 更新: ) +# 更新: +# 更新: +# 更新: @router.post("/change-password") +# 更新: def change_password( +# 更新: old_password: str = Body(...), +# 更新: new_password: str = Body(...), +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """修改当前用户密码""" +# 更新: from app.core.auth import verify_password, get_password_hash +# 更新: +# 更新: # 在当前session中重新查询用户 +# 更新: user = db.query(User).filter(User.f99_90_id == current_user.f99_90_id).first() +# 更新: if not user: +# 更新: raise HTTPException(status_code=404, detail="用户不存在") +# 更新: +# 更新: # 验证旧密码 +# 更新: if not verify_password(old_password, user.password): +# 更新: raise HTTPException( +# 更新: status_code=status.HTTP_400_BAD_REQUEST, +# 更新: detail="当前密码错误" +# 更新: ) +# 更新: +# 更新: # 更新密码 +# 更新: user.password = get_password_hash(new_password) +# 更新: db.commit() +# 更新: +# 更新: return {"message": "密码修改成功"} +# 更新: +# 更新: +# 更新: # ============ 短信验证码接口 ============ +# 更新: +# 更新: @router.post("/send-verification-code") +# 更新: def send_verification_code( +# 更新: phone: str = Body(..., min_length=11, max_length=11), +# 更新: purpose: str = Body("register") # register | login | reset_password +# 更新: ): +# 更新: """发送短信验证码""" +# 更新: from app.services.sms import send_verification_code as send_sms +# 更新: +# 更新: # 验证手机号格式 +# 更新: if not phone.startswith("1") or len(phone) != 11: +# 更新: return {"success": False, "message": "手机号格式不正确"} +# 更新: +# 更新: result = send_sms(phone) +# 更新: +# 更新: if result["success"]: +# 更新: return { +# 更新: "success": True, +# 更新: "message": f"验证码已发送到 {phone[:3]}****{phone[7:]}", +# 更新: "expire": result.get("expire", 300) +# 更新: } +# 更新: else: +# 更新: return result +# 更新: +# 更新: +# 更新: @router.post("/verify-code") +# 更新: def verify_code( +# 更新: phone: str = Body(...), +# 更新: code: str = Body(..., min_length=6, max_length=6) +# 更新: ): +# 更新: """验证短信验证码(仅验证,不执行后续操作)""" +# 更新: from app.services.sms import verify_code as check_code +# 更新: +# 更新: is_valid = check_code(phone, code) +# 更新: +# 更新: if is_valid: +# 更新: return {"success": True, "message": "验证成功"} +# 更新: else: +# 更新: return {"success": False, "message": "验证码错误或已过期"} +# 更新: diff --git a/backend/app/routers/collections.py b/backend/app/routers/collections.py index b6e2291..74dee59 100644 --- a/backend/app/routers/collections.py +++ b/backend/app/routers/collections.py @@ -1,887 +1,1779 @@ -# 藏品路由 - 使用字段编码 +# collections - 藏品路由 +# Version: 1.2.95 +# 更新: + 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="删除失败") +# 更新: diff --git a/backend/app/routers/deal.py b/backend/app/routers/deal.py index 314f3ca..1bfa328 100644 --- a/backend/app/routers/deal.py +++ b/backend/app/routers/deal.py @@ -1,248 +1,502 @@ +# deal - 成交行情路由 +# Version: 1.2.85 +# 更新: + from fastapi import APIRouter, Depends, Query, HTTPException +# 更新: +# Version: 1.2.x +# 更新: from sqlalchemy.orm import Session +# 更新: from pydantic import BaseModel +# 更新: from typing import Optional +# 更新: from datetime import datetime, date +# 更新: from app.core.database import get_db +# 更新: from app.core.auth import get_current_user +# 更新: from app.models.deal_info import DealInfo +# 更新: +# 更新: router = APIRouter(prefix="/api/deal", tags=["成交行情"]) +# 更新: +# 更新: # ============ Schema ============ +# 更新: class DealInfoCreate(BaseModel): +# 更新: title: str +# 更新: content: Optional[str] = None +# 更新: deal_price: Optional[float] = None +# 更新: deal_date: Optional[str] = None # YYYY-MM-DD +# 更新: packaging: Optional[str] = None +# 更新: category: Optional[str] = None +# 更新: is_graded: Optional[bool] = False +# 更新: grading_company: Optional[str] = None +# 更新: grading_score: Optional[str] = None +# 更新: tail_number: Optional[str] = None +# 更新: size_type: Optional[str] = None +# 更新: version: Optional[str] = None +# 更新: platform: Optional[str] = None +# 更新: seller: Optional[str] = None +# 更新: buyer: Optional[str] = None +# 更新: +# 更新: class DealInfoUpdate(BaseModel): +# 更新: title: Optional[str] = None +# 更新: content: Optional[str] = None +# 更新: deal_price: Optional[float] = None +# 更新: deal_date: Optional[str] = None +# 更新: packaging: Optional[str] = None +# 更新: category: Optional[str] = None +# 更新: is_graded: Optional[bool] = None +# 更新: grading_company: Optional[str] = None +# 更新: grading_score: Optional[str] = None +# 更新: tail_number: Optional[str] = None +# 更新: size_type: Optional[str] = None +# 更新: version: Optional[str] = None +# 更新: platform: Optional[str] = None +# 更新: seller: Optional[str] = None +# 更新: buyer: Optional[str] = None +# 更新: status: Optional[str] = None +# 更新: +# 更新: class DealInfoResponse(BaseModel): +# 更新: id: str +# 更新: user_id: Optional[str] +# 更新: title: str +# 更新: content: Optional[str] +# 更新: deal_price: Optional[float] +# 更新: deal_date: Optional[date] +# 更新: deal_no: Optional[str] +# 更新: packaging: Optional[str] +# 更新: category: Optional[str] +# 更新: is_graded: Optional[bool] +# 更新: grading_company: Optional[str] +# 更新: grading_score: Optional[str] +# 更新: tail_number: Optional[str] +# 更新: size_type: Optional[str] +# 更新: version: Optional[str] +# 更新: platform: Optional[str] +# 更新: seller: Optional[str] +# 更新: buyer: Optional[str] +# 更新: status: str +# 更新: view_count: int +# 更新: contact_count: int +# 更新: created_at: Optional[datetime] +# 更新: updated_at: Optional[datetime] +# 更新: +# 更新: class Config: +# 更新: from_attributes = True +# 更新: +# 更新: # 生成行情编号 +# 更新: def generate_deal_no(db: Session): +# 更新: """生成行情编号,从A000001开始递增""" +# 更新: last = db.query(DealInfo).order_by(DealInfo.deal_no.desc()).first() +# 更新: if last and last.deal_no: +# 更新: # 例如 A000001 -> 2 -> A000002 +# 更新: num = int(last.deal_no[1:]) + 1 +# 更新: return f"A{num:06d}" +# 更新: return "A000001" +# 更新: +# 更新: # ============ API ============ +# 更新: @router.get("/list", response_model=list[DealInfoResponse]) +# 更新: def get_deal_list( +# 更新: status: str = Query("active"), +# 更新: deal_date: Optional[str] = Query(None), +# 更新: page: int = Query(1, ge=1), +# 更新: page_size: int = Query(20, ge=1, le=1000), +# 更新: user_only: bool = Query(False), # 是否只查看自己的 +# 更新: current_user: Optional = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取成交行情列表""" +# 更新: query = db.query(DealInfo).filter(DealInfo.status == status) +# 更新: +# 更新: # 我的行情:只查看自己的(管理员也只看自己的) +# 更新: if user_only and current_user: +# 更新: query = query.filter(DealInfo.user_id == current_user.f99_90_id) +# 更新: +# 更新: # 成交日期过滤 +# 更新: if deal_date: +# 更新: query = query.filter(DealInfo.deal_date == deal_date) +# 更新: +# 更新: # 排序:优先成交日期倒序,同日按编号倒序 +# 更新: query = query.order_by(DealInfo.deal_date.desc().nullslast(), DealInfo.deal_no.desc().nullslast()) +# 更新: +# 更新: # 分页 +# 更新: offset = (page - 1) * page_size +# 更新: items = query.offset(offset).limit(page_size).all() +# 更新: +# 更新: return items +# 更新: +# 更新: @router.get("/stats") +# 更新: def get_deal_stats( +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取成交行情统计""" +# 更新: total = db.query(DealInfo).filter(DealInfo.status == "active").count() +# 更新: +# 更新: # 按日期统计 +# 更新: from sqlalchemy import func +# 更新: date_stats = db.query( +# 更新: DealInfo.deal_date, +# 更新: func.count(DealInfo.id).label('count') +# 更新: ).filter( +# 更新: DealInfo.status == "active", +# 更新: DealInfo.deal_date.isnot(None) +# 更新: ).group_by(DealInfo.deal_date).order_by(DealInfo.deal_date.desc()).limit(10).all() +# 更新: +# 更新: return { +# 更新: "total": total, +# 更新: "by_date": [{"date": str(d.deal_date), "count": d.count} for d in date_stats] +# 更新: } +# 更新: +# 更新: @router.post("", response_model=DealInfoResponse) +# 更新: def create_deal( +# 更新: data: DealInfoCreate, +# 更新: current_user = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """创建成交行情""" +# 更新: if not current_user: +# 更新: raise HTTPException(status_code=401, detail="请先登录") +# 更新: +# 更新: # 生成行情编号 +# 更新: deal_no = generate_deal_no(db) +# 更新: +# 更新: # 解析日期 +# 更新: deal_date = None +# 更新: if data.deal_date: +# 更新: try: +# 更新: deal_date = datetime.strptime(data.deal_date, "%Y-%m-%d").date() +# 更新: except: +# 更新: pass +# 更新: +# 更新: deal = DealInfo( +# 更新: user_id=current_user.f99_90_id if current_user else None, +# 更新: title=data.title, +# 更新: content=data.content, +# 更新: deal_price=data.deal_price, +# 更新: deal_date=deal_date, +# 更新: deal_no=deal_no, +# 更新: packaging=data.packaging, +# 更新: category=data.category, +# 更新: is_graded=data.is_graded or False, +# 更新: grading_company=data.grading_company, +# 更新: grading_score=data.grading_score, +# 更新: tail_number=data.tail_number, +# 更新: size_type=data.size_type, +# 更新: version=data.version, +# 更新: platform=data.platform, +# 更新: seller=data.seller, +# 更新: buyer=data.buyer, +# 更新: status="active" +# 更新: ) +# 更新: db.add(deal) +# 更新: db.commit() +# 更新: db.refresh(deal) +# 更新: return deal +# 更新: +# 更新: @router.get("/{deal_id}", response_model=DealInfoResponse) +# 更新: def get_deal( +# 更新: deal_id: str, +# 更新: current_user = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取成交行情详情""" +# 更新: deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first() +# 更新: if not deal: +# 更新: raise HTTPException(status_code=404, detail="成交行情不存在") +# 更新: +# 更新: # 增加浏览数 +# 更新: deal.view_count += 1 +# 更新: db.commit() +# 更新: +# 更新: return deal +# 更新: +# 更新: @router.put("/{deal_id}", response_model=DealInfoResponse) +# 更新: def update_deal( +# 更新: deal_id: str, +# 更新: data: DealInfoUpdate, +# 更新: current_user = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """更新成交行情""" +# 更新: if not current_user: +# 更新: raise HTTPException(status_code=401, detail="请先登录") +# 更新: +# 更新: deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first() +# 更新: if not deal: +# 更新: raise HTTPException(status_code=404, detail="成交行情不存在") +# 更新: +# 更新: # 处理日期 +# 更新: if data.deal_date: +# 更新: try: +# 更新: data.deal_date = datetime.strptime(data.deal_date, "%Y-%m-%d").date() +# 更新: except: +# 更新: data.deal_date = None +# 更新: +# 更新: for key, value in data.model_dump(exclude_unset=True).items(): +# 更新: setattr(deal, key, value) +# 更新: +# 更新: db.commit() +# 更新: db.refresh(deal) +# 更新: return deal +# 更新: +# 更新: @router.delete("/{deal_id}") +# 更新: def delete_deal( +# 更新: deal_id: str, +# 更新: current_user = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """删除成交行情""" +# 更新: if not current_user: +# 更新: raise HTTPException(status_code=401, detail="请先登录") +# 更新: +# 更新: deal = db.query(DealInfo).filter(DealInfo.id == deal_id).first() +# 更新: if not deal: +# 更新: raise HTTPException(status_code=404, detail="成交行情不存在") +# 更新: +# 更新: deal.status = "deleted" +# 更新: db.commit() +# 更新: - return {"message": "删除成功"} \ No newline at end of file +# 更新: + return {"message": "删除成功"} +# 更新: diff --git a/backend/app/routers/information.py b/backend/app/routers/information.py index e572a95..ca56262 100644 --- a/backend/app/routers/information.py +++ b/backend/app/routers/information.py @@ -1,1473 +1,2948 @@ -# 资讯API路由 +# information - 资讯路由 +# Version: 1.2.92 +# 更新: + from fastapi import APIRouter, Depends, HTTPException, Query, Response, Body +# 更新: from sqlalchemy.orm import Session, joinedload +# 更新: from sqlalchemy import text +# 更新: from typing import List, Optional +# 更新: from pydantic import BaseModel +# 更新: from datetime import datetime, date +# 更新: import os +# 更新: +# 更新: from app.core.database import get_db +# 更新: from app.core.auth import get_current_user +# 更新: from app.core.coolbot_db import coolbot_engine +# 更新: from app.models.models import User, Information, Collection +# 更新: +# 更新: router = APIRouter(prefix="/api/information", tags=["资讯"]) +# 更新: +# 更新: +# 更新: # Schema +# 更新: class InformationCreate(BaseModel): +# 更新: info_type: str # seek-寻配号, deal-成交数据, publish-发布 +# 更新: title: str +# 更新: content: Optional[str] +# 更新: collection_id: Optional[str] = None +# 更新: expect_category: Optional[str] = None +# 更新: expect_version: Optional[str] = None +# 更新: expect_packaging: Optional[str] = None +# 更新: expect_number: Optional[str] = None +# 更新: expect_price_min: Optional[float] = None +# 更新: expect_price_max: Optional[float] = None +# 更新: deal_price: Optional[float] = None +# 更新: deal_date: Optional[date] = None +# 更新: packaging: Optional[str] = None +# 更新: is_graded: Optional[bool] = False +# 更新: grading_company: Optional[str] = None +# 更新: grading_score: Optional[str] = None +# 更新: category: Optional[str] = None +# 更新: deal_no: Optional[str] = None +# 更新: +# 更新: +# 更新: class InformationUpdate(BaseModel): +# 更新: title: Optional[str] = None +# 更新: content: Optional[str] = None +# 更新: status: Optional[str] = None +# 更新: expect_category: Optional[str] = None +# 更新: expect_version: Optional[str] = None +# 更新: expect_packaging: Optional[str] = None +# 更新: expect_number: Optional[str] = None +# 更新: expect_price_min: Optional[float] = None +# 更新: expect_price_max: Optional[float] = None +# 更新: deal_price: Optional[float] = None +# 更新: deal_date: Optional[date] = None +# 更新: packaging: Optional[str] = None +# 更新: is_graded: Optional[bool] = None +# 更新: grading_company: Optional[str] = None +# 更新: grading_score: Optional[str] = None +# 更新: +# 更新: +# 更新: class InformationResponse(BaseModel): +# 更新: id: str +# 更新: user_id: str +# 更新: info_type: str +# 更新: title: str +# 更新: content: Optional[str] +# 更新: collection_id: Optional[str] +# 更新: expect_category: Optional[str] +# 更新: expect_version: Optional[str] +# 更新: expect_packaging: Optional[str] +# 更新: expect_number: Optional[str] +# 更新: expect_price_min: Optional[float] +# 更新: expect_price_max: Optional[float] +# 更新: deal_price: Optional[float] +# 更新: deal_date: Optional[date] +# 更新: status: str +# 更新: is_matched: Optional[str] = "pending" +# 更新: matched_user_id: Optional[str] = None +# 更新: matched_contact: Optional[str] = None +# 更新: view_count: int +# 更新: contact_count: int +# 更新: created_at: datetime +# 更新: # 评级相关字段 +# 更新: packaging: Optional[str] = None +# 更新: is_graded: Optional[bool] = False +# 更新: grading_company: Optional[str] = None +# 更新: grading_score: Optional[str] = None +# 更新: category: Optional[str] = None +# 更新: deal_no: Optional[str] = None +# 更新: # 用户信息 +# 更新: user_name: Optional[str] = None +# 更新: user_avatar: Optional[str] = None +# 更新: # 关联藏品信息 +# 更新: collection_name: Optional[str] = None +# 更新: collection_category: Optional[str] = None +# 更新: collection_version: Optional[str] = None +# 更新: collection_number: Optional[str] = None +# 更新: # 匹配数量(我的藏品中满足条件的数量) +# 更新: matched_count: Optional[int] = 0 +# 更新: # 网络数据匹配数量(coolbot_data数据库中满足条件的数量) +# 更新: network_matched_count: Optional[int] = 0 +# 更新: +# 更新: class Config: +# 更新: from_attributes = True +# 更新: +# 更新: +# 更新: # 资讯列表 +# 更新: @router.get("/list", response_model=List[InformationResponse]) +# 更新: def get_information_list( +# 更新: info_type: Optional[str] = Query(None, description="类型: seek/deal/publish"), +# 更新: status: str = Query("active", description="状态: active/closed/expired"), +# 更新: user_id: Optional[str] = Query(None, description="用户ID,用于获取该用户的行情"), +# 更新: deal_date: Optional[str] = Query(None, description="成交日期过滤,格式YYYY-MM-DD"), +# 更新: page: int = Query(1, ge=1), +# 更新: page_size: int = Query(20, ge=1, le=500), +# 更新: current_user: Optional[User] = Depends(get_current_user), +# 更新: db: Session = Depends(get_db), +# 更新: response: Response = None +# 更新: ): +# 更新: """获取资讯列表(公开,无需登录)""" +# 更新: query = db.query(Information).options( +# 更新: joinedload(Information.user), +# 更新: joinedload(Information.collection) +# 更新: ).filter(Information.status == status) +# 更新: +# 更新: if info_type: +# 更新: query = query.filter(Information.info_type == info_type) +# 更新: +# 更新: # 如果传入了user_id,只返回该用户的行情 +# 更新: if user_id: +# 更新: query = query.filter(Information.user_id == user_id) +# 更新: +# 更新: # 成交日期过滤 +# 更新: if deal_date: +# 更新: from datetime import date +# 更新: deal_date_obj = date.fromisoformat(deal_date) +# 更新: query = query.filter(Information.deal_date == deal_date_obj) +# 更新: +# 更新: # 按创建时间倒序 +# 更新: query = query.order_by(Information.deal_date.desc().nullslast(), Information.deal_no.desc().nullslast()) +# 更新: +# 更新: # 分页 +# 更新: offset = (page - 1) * page_size +# 更新: items = query.offset(offset).limit(page_size).all() +# 更新: +# 更新: # 转换结果 +# 更新: result = [] +# 更新: for item in items: +# 更新: # 计算匹配数量(仅对seek类型,且用户登录时) +# 更新: matched_count = 0 +# 更新: if item.info_type == 'seek' and item.expect_number and current_user: +# 更新: matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number) +# 更新: +# 更新: result.append(InformationResponse( +# 更新: id=item.id, +# 更新: user_id=item.user_id, +# 更新: info_type=item.info_type, +# 更新: title=item.title, +# 更新: content=item.content, +# 更新: collection_id=item.collection_id, +# 更新: expect_category=item.expect_category, +# 更新: expect_version=item.expect_version, +# 更新: expect_packaging=item.expect_packaging, +# 更新: expect_number=item.expect_number, +# 更新: expect_price_min=item.expect_price_min, +# 更新: expect_price_max=item.expect_price_max, +# 更新: deal_price=item.deal_price, +# 更新: deal_date=item.deal_date, +# 更新: status=item.status, +# 更新: is_matched=item.is_matched, +# 更新: matched_user_id=item.matched_user_id, +# 更新: matched_contact=item.matched_contact, +# 更新: view_count=item.view_count, +# 更新: contact_count=item.contact_count, +# 更新: created_at=item.created_at, +# 更新: user_name=item.user.f01_01_name if item.user else None, +# 更新: user_avatar=item.user.avatar if item.user else None, +# 更新: collection_name=item.collection.f01_01_name if item.collection else None, +# 更新: collection_category=item.collection.f01_03_category if item.collection else None, +# 更新: collection_version=item.collection.f02_11_version if item.collection else None, +# 更新: collection_number=item.collection.f02_10_prefix_serial if item.collection else None, +# 更新: packaging=item.packaging, +# 更新: is_graded=item.is_graded or False, +# 更新: grading_company=item.grading_company, +# 更新: grading_score=item.grading_score, +# 更新: category=item.category, +# 更新: deal_no=item.deal_no, +# 更新: matched_count=matched_count, +# 更新: network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0, +# 更新: )) +# 更新: +# 更新: # 获取总数并设置响应头 +# 更新: from fastapi import Response +# 更新: total_query = db.query(Information).filter(Information.status == status) +# 更新: if info_type: +# 更新: total_query = total_query.filter(Information.info_type == info_type) +# 更新: total_count = total_query.count() +# 更新: total_pages = (total_count + page_size - 1) // page_size +# 更新: +# 更新: # 设置响应头 +# 更新: response.headers['X-Total-Pages'] = str(total_pages) +# 更新: response.headers['X-Total-Count'] = str(total_count) +# 更新: +# 更新: return result +# 更新: +# 更新: +# 更新: def match_collections_count(db: Session, user_id: str, expect_number: str) -> int: +# 更新: """根据号码特征计算匹配藏品数量""" +# 更新: if not expect_number or len(expect_number) != 10: +# 更新: return 0 +# 更新: +# 更新: # 固定前缀 +# 更新: if not expect_number.startswith('J0'): +# 更新: return 0 +# 更新: +# 更新: pattern = expect_number[2:] # 后8位 +# 更新: if not pattern: +# 更新: return 0 +# 更新: +# 更新: # 获取用户所有藏品 +# 更新: collections = db.query(Collection).filter( +# 更新: Collection.f99_91_user_id == user_id, +# 更新: Collection.f01_04_status == "in_collection" +# 更新: ).all() +# 更新: +# 更新: count = 0 +# 更新: for c in collections: +# 更新: number = c.f02_10_prefix_serial or '' +# 更新: # 去掉J0前缀后取前8位 +# 更新: if len(number) >= 10 and number.startswith('J0'): +# 更新: col_pattern = number[2:10] +# 更新: if match_pattern(col_pattern, pattern): +# 更新: count += 1 +# 更新: elif len(number) >= 8: +# 更新: col_pattern = number[:8] +# 更新: if match_pattern(col_pattern, pattern): +# 更新: count += 1 +# 更新: +# 更新: return count +# 更新: +# 更新: +# 更新: def match_collections_count_from_coolbot(expect_number: str) -> int: +# 更新: """根据号码特征计算匹配藏品数量(从coolbot_data数据库)""" +# 更新: if not expect_number or len(expect_number) != 10: +# 更新: return 0 +# 更新: +# 更新: # 固定前缀 +# 更新: if not expect_number.startswith('J0'): +# 更新: return 0 +# 更新: +# 更新: pattern = expect_number[2:] # 后8位 +# 更新: if not pattern: +# 更新: return 0 +# 更新: +# 更新: # 直接查询coolbot_data数据库 +# 更新: query = text(""" +# 更新: SELECT COUNT(*) FROM collections +# 更新: WHERE crown_code IS NOT NULL +# 更新: AND crown_code != '' +# 更新: AND LENGTH(crown_code) >= 10 +# 更新: AND crown_code LIKE 'J0%' +# 更新: """) +# 更新: +# 更新: try: +# 更新: with coolbot_engine.connect() as conn: +# 更新: result = conn.execute(query) +# 更新: total_count = result.scalar() or 0 +# 更新: +# 更新: # 遍历匹配 +# 更新: query_all = text(""" +# 更新: SELECT id, crown_code FROM collections +# 更新: WHERE crown_code IS NOT NULL +# 更新: AND crown_code != '' +# 更新: AND LENGTH(crown_code) >= 10 +# 更新: AND crown_code LIKE 'J0%' +# 更新: """) +# 更新: result = conn.execute(query_all) +# 更新: +# 更新: match_count = 0 +# 更新: for row in result: +# 更新: crown_code = row[1] +# 更新: if crown_code and len(crown_code) >= 10: +# 更新: col_pattern = crown_code[2:10] +# 更新: if match_pattern(col_pattern, pattern): +# 更新: match_count += 1 +# 更新: +# 更新: return match_count +# 更新: except Exception as e: +# 更新: print(f"Error querying coolbot_data: {e}") +# 更新: return 0 +# 更新: +# 更新: +# 更新: def match_collections_list_from_coolbot(expect_number: str, limit: int = 20) -> List[dict]: +# 更新: """获取匹配的藏品列表(从coolbot_data数据库)""" +# 更新: if not expect_number or len(expect_number) != 10: +# 更新: return [] +# 更新: +# 更新: # 固定前缀 +# 更新: if not expect_number.startswith('J0'): +# 更新: return [] +# 更新: +# 更新: pattern = expect_number[2:] # 后8位 +# 更新: if not pattern: +# 更新: return [] +# 更新: +# 更新: # 直接查询coolbot_data数据库 +# 更新: query = text(""" +# 更新: SELECT id, name, category, crown_code, price, post_title, post_url, author, post_crawled_at +# 更新: FROM collections +# 更新: WHERE crown_code IS NOT NULL +# 更新: AND crown_code != '' +# 更新: AND LENGTH(crown_code) >= 10 +# 更新: AND crown_code LIKE 'J0%' +# 更新: """) +# 更新: +# 更新: try: +# 更新: with coolbot_engine.connect() as conn: +# 更新: result = conn.execute(query) +# 更新: +# 更新: matched = [] +# 更新: for row in result: +# 更新: crown_code = row[3] +# 更新: if crown_code and len(crown_code) >= 10: +# 更新: col_pattern = crown_code[2:10] +# 更新: if match_pattern(col_pattern, pattern): +# 更新: matched.append({ +# 更新: "id": row[0], +# 更新: "name": row[1], +# 更新: "category": row[2], +# 更新: "crown_code": crown_code, +# 更新: "price": float(row[4]) if row[4] else None, +# 更新: "post_title": row[5], +# 更新: "post_url": row[6], +# 更新: "author": row[7], +# 更新: "post_crawled_at": row[8].isoformat() if row[8] else None +# 更新: }) +# 更新: if len(matched) >= limit: +# 更新: break +# 更新: +# 更新: return matched +# 更新: except Exception as e: +# 更新: print(f"Error querying coolbot_data: {e}") +# 更新: return [] +# 更新: +# 更新: +# 更新: def match_pattern(col_number: str, pattern: str) -> bool: +# 更新: """匹配号码特征模式""" +# 更新: # X = 任意数字 +# 更新: # A = 非4 +# 更新: # B = 非47 +# 更新: # C = 非347 +# 更新: # D = 非247 +# 更新: # E = 非2347 +# 更新: # F = 非23457 +# 更新: # G = 非123457 +# 更新: +# 更新: # 注意:col_number已经是去掉J0前缀后的8位号码,不需要再处理 +# 更新: col_num = col_number +# 更新: +# 更新: for i, p in enumerate(pattern): +# 更新: if i >= len(col_num): +# 更新: return False +# 更新: +# 更新: c = col_num[i] +# 更新: +# 更新: if p == 'X': +# 更新: if not c.isdigit(): +# 更新: return False +# 更新: elif p == 'A': +# 更新: if c == '4': +# 更新: return False +# 更新: elif p == 'B': +# 更新: if c in '47': +# 更新: return False +# 更新: elif p == 'C': +# 更新: if c in '347': +# 更新: return False +# 更新: elif p == 'D': +# 更新: if c in '247': +# 更新: return False +# 更新: elif p == 'E': +# 更新: if c in '2347': +# 更新: return False +# 更新: elif p == 'F': +# 更新: if c in '23457': +# 更新: return False +# 更新: elif p == 'G': +# 更新: if c in '123457': +# 更新: return False +# 更新: else: +# 更新: # 数字或字母必须完全匹配 +# 更新: if p != c: +# 更新: return False +# 更新: +# 更新: return True +# 更新: +# 更新: +# 更新: # 获取单条资讯 +# 更新: @router.get("/{info_id}", response_model=InformationResponse) +# 更新: def get_information( +# 更新: info_id: str, +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取资讯详情""" +# 更新: item = db.query(Information).options( +# 更新: joinedload(Information.user), +# 更新: joinedload(Information.collection) +# 更新: ).filter(Information.id == info_id).first() +# 更新: +# 更新: if not item: +# 更新: raise HTTPException(status_code=404, detail="资讯不存在") +# 更新: +# 更新: # 增加浏览次数 +# 更新: item.view_count += 1 +# 更新: db.commit() +# 更新: +# 更新: return InformationResponse( +# 更新: id=item.id, +# 更新: user_id=item.user_id, +# 更新: info_type=item.info_type, +# 更新: title=item.title, +# 更新: content=item.content, +# 更新: collection_id=item.collection_id, +# 更新: expect_category=item.expect_category, +# 更新: expect_version=item.expect_version, +# 更新: expect_packaging=item.expect_packaging, +# 更新: expect_number=item.expect_number, +# 更新: expect_price_min=item.expect_price_min, +# 更新: expect_price_max=item.expect_price_max, +# 更新: deal_price=item.deal_price, +# 更新: deal_date=item.deal_date, +# 更新: status=item.status, +# 更新: view_count=item.view_count, +# 更新: contact_count=item.contact_count, +# 更新: created_at=item.created_at, +# 更新: packaging=item.packaging, +# 更新: is_graded=item.is_graded or False, +# 更新: grading_company=item.grading_company, +# 更新: grading_score=item.grading_score, +# 更新: category=item.category, +# 更新: user_name=item.user.f01_01_name if item.user else None, +# 更新: user_avatar=item.user.avatar if item.user else None, +# 更新: collection_name=item.collection.f01_01_name if item.collection else None, +# 更新: collection_category=item.collection.f01_03_category if item.collection else None, +# 更新: collection_version=item.collection.f02_11_version if item.collection else None, +# 更新: collection_number=item.collection.f02_10_prefix_serial if item.collection else None, +# 更新: ) +# 更新: +# 更新: +# 更新: # 发布资讯 +# 更新: @router.post("/", response_model=InformationResponse) +# 更新: def create_information( +# 更新: data: InformationCreate, +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """发布资讯""" +# 更新: # 生成行情编号:日期 + 5位自然数(从00001开始) +# 更新: deal_no = None +# 更新: if data.info_type == 'deal': +# 更新: today = datetime.now().strftime('%Y%m%d') +# 更新: # 查询当天已有行情数量 +# 更新: from app.models.models import Information +# 更新: count_today = db.query(Information).filter( +# 更新: Information.info_type == 'deal', +# 更新: Information.deal_no.like(f'DJ{today}%') +# 更新: ).count() +# 更新: # 编号 = 日期 + 5位自然数(如 DJ2026041100001) +# 更新: seq = count_today + 1 +# 更新: deal_no = f"{today[2:]}{seq:04d}" +# 更新: +# 更新: info = Information( +# 更新: user_id=current_user.f99_90_id, +# 更新: info_type=data.info_type, +# 更新: title=data.title, +# 更新: content=data.content, +# 更新: collection_id=data.collection_id, +# 更新: expect_category=data.expect_category, +# 更新: expect_version=data.expect_version, +# 更新: expect_packaging=data.expect_packaging, +# 更新: expect_number=data.expect_number, +# 更新: expect_price_min=data.expect_price_min, +# 更新: expect_price_max=data.expect_price_max, +# 更新: deal_price=data.deal_price, +# 更新: deal_date=data.deal_date, +# 更新: packaging=data.packaging, +# 更新: is_graded=data.is_graded or False, +# 更新: grading_company=data.grading_company, +# 更新: grading_score=data.grading_score, +# 更新: category=data.category, +# 更新: deal_no=deal_no, +# 更新: status="active" +# 更新: ) +# 更新: db.add(info) +# 更新: db.commit() +# 更新: db.refresh(info) +# 更新: +# 更新: return InformationResponse( +# 更新: id=info.id, +# 更新: user_id=info.user_id, +# 更新: info_type=info.info_type, +# 更新: title=info.title, +# 更新: content=info.content, +# 更新: collection_id=info.collection_id, +# 更新: expect_category=info.expect_category, +# 更新: expect_version=info.expect_version, +# 更新: expect_packaging=info.expect_packaging, +# 更新: expect_number=info.expect_number, +# 更新: expect_price_min=info.expect_price_min, +# 更新: expect_price_max=info.expect_price_max, +# 更新: deal_price=info.deal_price, +# 更新: deal_date=info.deal_date, +# 更新: status=info.status, +# 更新: view_count=info.view_count, +# 更新: contact_count=info.contact_count, +# 更新: created_at=info.created_at, +# 更新: user_name=current_user.f01_01_name, +# 更新: user_avatar=current_user.avatar, +# 更新: collection_name=None, +# 更新: collection_category=None, +# 更新: collection_version=None, +# 更新: collection_number=None, +# 更新: ) +# 更新: +# 更新: +# 更新: # 更新资讯 +# 更新: @router.put("/{info_id}", response_model=InformationResponse) +# 更新: def update_information( +# 更新: info_id: str, +# 更新: data: InformationUpdate, +# 更新: current_user: Optional[User] = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """更新资讯""" +# 更新: if not current_user: +# 更新: raise HTTPException(status_code=401, detail="请先登录") +# 更新: # 处理f99_90_id为None的情况 +# 更新: user_filter = current_user.f99_90_id if current_user and current_user.f99_90_id else Information.user_id +# 更新: info = db.query(Information).filter( +# 更新: Information.id == info_id, +# 更新: Information.user_id == user_filter +# 更新: ).first() +# 更新: +# 更新: if not info: +# 更新: raise HTTPException(status_code=404, detail="资讯不存在或无权修改") +# 更新: +# 更新: # 更新字段 +# 更新: if data.title is not None: +# 更新: info.title = data.title +# 更新: if data.content is not None: +# 更新: info.content = data.content +# 更新: if data.status is not None: +# 更新: info.status = data.status +# 更新: if data.expect_category is not None: +# 更新: info.expect_category = data.expect_category +# 更新: if data.expect_version is not None: +# 更新: info.expect_version = data.expect_version +# 更新: if data.expect_packaging is not None: +# 更新: info.expect_packaging = data.expect_packaging +# 更新: if data.expect_number is not None: +# 更新: info.expect_number = data.expect_number +# 更新: if data.expect_price_min is not None: +# 更新: info.expect_price_min = data.expect_price_min +# 更新: if data.expect_price_max is not None: +# 更新: info.expect_price_max = data.expect_price_max +# 更新: if data.deal_price is not None: +# 更新: info.deal_price = data.deal_price +# 更新: if data.deal_date is not None: +# 更新: info.deal_date = data.deal_date +# 更新: if data.packaging is not None: +# 更新: info.packaging = data.packaging +# 更新: if data.is_graded is not None: +# 更新: info.is_graded = data.is_graded +# 更新: if data.grading_company is not None: +# 更新: info.grading_company = data.grading_company +# 更新: if data.grading_score is not None: +# 更新: info.grading_score = data.grading_score +# 更新: +# 更新: db.commit() +# 更新: db.refresh(info) +# 更新: +# 更新: return InformationResponse( +# 更新: id=info.id, +# 更新: user_id=info.user_id, +# 更新: info_type=info.info_type, +# 更新: title=info.title, +# 更新: content=info.content, +# 更新: collection_id=info.collection_id, +# 更新: expect_category=info.expect_category, +# 更新: expect_version=info.expect_version, +# 更新: expect_packaging=info.expect_packaging, +# 更新: expect_number=info.expect_number, +# 更新: expect_price_min=info.expect_price_min, +# 更新: expect_price_max=info.expect_price_max, +# 更新: deal_price=info.deal_price, +# 更新: deal_date=info.deal_date, +# 更新: status=info.status, +# 更新: view_count=info.view_count, +# 更新: contact_count=info.contact_count, +# 更新: created_at=info.created_at, +# 更新: packaging=info.packaging, +# 更新: is_graded=info.is_graded or False, +# 更新: grading_company=info.grading_company, +# 更新: grading_score=info.grading_score, +# 更新: category=info.category, +# 更新: user_name=current_user.f01_01_name, +# 更新: user_avatar=current_user.avatar, +# 更新: collection_name=info.collection.f01_01_name if info.collection else None, +# 更新: collection_category=info.collection.f01_03_category if info.collection else None, +# 更新: collection_version=info.collection.f02_11_version if info.collection else None, +# 更新: collection_number=info.collection.f02_10_prefix_serial if info.collection else None, +# 更新: ) +# 更新: +# 更新: +# 更新: # 删除资讯 +# 更新: @router.delete("/{info_id}") +# 更新: def delete_information( +# 更新: info_id: str, +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """删除资讯""" +# 更新: # 允许admin删除任何人的资讯 +# 更新: if current_user.role == "admin": +# 更新: info = db.query(Information).filter(Information.id == info_id).first() +# 更新: else: +# 更新: info = db.query(Information).filter( +# 更新: Information.id == info_id, +# 更新: Information.user_id == current_user.f99_90_id +# 更新: ).first() +# 更新: +# 更新: if not info: +# 更新: raise HTTPException(status_code=404, detail="资讯不存在或无权删除") +# 更新: +# 更新: db.delete(info) +# 更新: db.commit() +# 更新: +# 更新: return {"message": "删除成功"} +# 更新: +# 更新: +# 更新: # 寻配号 - 自动匹配推荐藏品 +# 更新: @router.get("/seek/match") +# 更新: def get_seek_match( +# 更新: info_id: str, +# 更新: current_user: Optional[User] = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取符合条件的我的藏品推荐""" +# 更新: if not current_user: +# 更新: raise HTTPException(status_code=401, detail="请先登录") +# 更新: +# 更新: info = db.query(Information).filter( +# 更新: Information.id == info_id, +# 更新: Information.info_type == "seek" +# 更新: ).first() +# 更新: +# 更新: if not info: +# 更新: raise HTTPException(status_code=404, detail="寻配号信息不存在") +# 更新: +# 更新: # 更新用户配号(寻号)次数 +# 更新: current_user.f99_96_search_count = (current_user.f99_96_search_count or 0) + 1 +# 更新: db.commit() +# 更新: +# 更新: # 获取用户所有藏品 +# 更新: collections = db.query(Collection).filter( +# 更新: Collection.f99_91_user_id == current_user.f99_90_id, +# 更新: Collection.f01_04_status == "in_collection" +# 更新: ).all() +# 更新: +# 更新: # 去掉版别筛选,因为藏品分类和发布需求的版别不同 +# 更新: # if info.expect_category: +# 更新: # collections = [c for c in collections if c.f01_03_category == info.expect_category] +# 更新: +# 更新: # 按号码特征模式匹配 +# 更新: matched = [] +# 更新: if info.expect_number and len(info.expect_number) == 10: +# 更新: pattern = info.expect_number[2:] # 后8位 +# 更新: for c in collections: +# 更新: number = c.f02_10_prefix_serial or '' +# 更新: # 去掉J0前缀后取前8位 +# 更新: if len(number) >= 10 and number.startswith('J0'): +# 更新: col_pattern = number[2:10] # 取J0后面的8位 +# 更新: if match_pattern(col_pattern, pattern): +# 更新: matched.append(c) +# 更新: elif len(number) >= 8: +# 更新: col_pattern = number[:8] # 取前8位 +# 更新: if match_pattern(col_pattern, pattern): +# 更新: matched.append(c) +# 更新: else: +# 更新: matched = collections +# 更新: +# 更新: return { +# 更新: "info_id": info_id, +# 更新: "matched_count": len(matched), +# 更新: "collections": [ +# 更新: { +# 更新: "id": c.f99_90_id, +# 更新: "code": c.f01_02_code or '', +# 更新: "name": c.f01_01_name, +# 更新: "number": c.f02_10_prefix_serial, +# 更新: "status": c.f01_04_status, +# 更新: "category": c.f01_03_category, +# 更新: "version": c.f02_11_version, +# 更新: "packaging": c.f02_12_packaging, +# 更新: "cost_price": c.f05_40_cost_price, +# 更新: } +# 更新: for c in matched +# 更新: ], +# 更新: "network_matched_count": match_collections_count_from_coolbot(info.expect_number) if info.expect_number else 0, +# 更新: "network_collections": match_collections_list_from_coolbot(info.expect_number, limit=20) if info.expect_number else [] +# 更新: } +# 更新: +# 更新: +# 更新: # 获取网络数据匹配列表 +# 更新: @router.get("/seek/network-match/{info_id}") +# 更新: def get_network_match( +# 更新: info_id: str, +# 更新: limit: int = Query(20, ge=1, le=100), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取一尘数据库中匹配的藏品列表""" +# 更新: info = db.query(Information).filter( +# 更新: Information.id == info_id, +# 更新: Information.info_type == "seek" +# 更新: ).first() +# 更新: +# 更新: if not info: +# 更新: raise HTTPException(status_code=404, detail="寻配号信息不存在") +# 更新: +# 更新: if not info.expect_number: +# 更新: return {"matched_count": 0, "collections": []} +# 更新: +# 更新: matched = match_collections_list_from_coolbot(info.expect_number, limit=limit) +# 更新: +# 更新: return { +# 更新: "matched_count": len(matched), +# 更新: "collections": matched +# 更新: } +# 更新: +# 更新: +# 更新: # 我的寻号列表 +# 更新: @router.get("/my-seeks") +# 更新: def get_my_seeks( +# 更新: current_user: Optional[User] = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取当前用户发布的所有寻号信息""" +# 更新: if not current_user: +# 更新: raise HTTPException(status_code=401, detail="请先登录") +# 更新: +# 更新: items = db.query(Information).filter( +# 更新: Information.user_id == current_user.f99_90_id, +# 更新: Information.info_type == "seek", +# 更新: Information.status == "active" +# 更新: ).order_by(Information.created_at.desc()).all() +# 更新: +# 更新: result = [] +# 更新: for item in items: +# 更新: # 计算匹配数量 +# 更新: matched_count = 0 +# 更新: if item.expect_number: +# 更新: matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number) +# 更新: +# 更新: result.append(InformationResponse( +# 更新: id=item.id, +# 更新: user_id=item.user_id, +# 更新: info_type=item.info_type, +# 更新: title=item.title, +# 更新: content=item.content, +# 更新: collection_id=item.collection_id, +# 更新: expect_category=item.expect_category, +# 更新: expect_version=item.expect_version, +# 更新: expect_packaging=item.expect_packaging, +# 更新: expect_number=item.expect_number, +# 更新: expect_price_min=item.expect_price_min, +# 更新: expect_price_max=item.expect_price_max, +# 更新: deal_price=item.deal_price, +# 更新: deal_date=item.deal_date, +# 更新: status=item.status, +# 更新: is_matched=item.is_matched, +# 更新: matched_user_id=item.matched_user_id, +# 更新: matched_contact=item.matched_contact, +# 更新: view_count=item.view_count, +# 更新: contact_count=item.contact_count, +# 更新: created_at=item.created_at, +# 更新: user_name=item.user.f01_01_name if item.user else None, +# 更新: user_avatar=item.user.avatar if item.user else None, +# 更新: collection_name=item.collection.f01_01_name if item.collection else None, +# 更新: collection_category=item.collection.f01_03_category if item.collection else None, +# 更新: collection_version=item.collection.f02_11_version if item.collection else None, +# 更新: collection_number=item.collection.f02_10_prefix_serial if item.collection else None, +# 更新: matched_count=matched_count, +# 更新: network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0, +# 更新: )) +# 更新: +# 更新: # 获取总数并设置响应头 +# 更新: from fastapi import Response +# 更新: total_query = db.query(Information).filter(Information.status == status) +# 更新: if info_type: +# 更新: total_query = total_query.filter(Information.info_type == info_type) +# 更新: total_count = total_query.count() +# 更新: total_pages = (total_count + page_size - 1) // page_size +# 更新: +# 更新: # 设置响应头 +# 更新: response.headers['X-Total-Pages'] = str(total_pages) +# 更新: response.headers['X-Total-Count'] = str(total_count) +# 更新: +# 更新: return result +# 更新: +# 更新: +# 更新: # 成交数据统计 +# 更新: @router.get("/deal/stats") +# 更新: def get_deal_stats( +# 更新: days: int = Query(7, ge=1, le=90, description="统计天数"), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取成交数据统计""" +# 更新: from sqlalchemy import func +# 更新: from datetime import timedelta +# 更新: +# 更新: start_date = datetime.now() - timedelta(days=days) +# 更新: +# 更新: # 按版别统计 +# 更新: by_version = db.query( +# 更新: Information.expect_version, +# 更新: func.count(Information.id).label("count"), +# 更新: func.avg(Information.deal_price).label("avg_price"), +# 更新: func.max(Information.deal_price).label("max_price"), +# 更新: func.min(Information.deal_price).label("min_price") +# 更新: ).filter( +# 更新: Information.info_type == "deal", +# 更新: Information.status == "active", +# 更新: Information.created_at >= start_date +# 更新: ).group_by(Information.expect_version).all() +# 更新: +# 更新: # 按包装统计 +# 更新: by_packaging = db.query( +# 更新: Information.expect_packaging, +# 更新: func.count(Information.id).label("count"), +# 更新: func.avg(Information.deal_price).label("avg_price") +# 更新: ).filter( +# 更新: Information.info_type == "deal", +# 更新: Information.status == "active", +# 更新: Information.created_at >= start_date +# 更新: ).group_by(Information.expect_packaging).all() +# 更新: +# 更新: # 按号码分类统计 +# 更新: by_number = db.query( +# 更新: Information.expect_number, +# 更新: func.count(Information.id).label("count"), +# 更新: func.avg(Information.deal_price).label("avg_price") +# 更新: ).filter( +# 更新: Information.info_type == "deal", +# 更新: Information.status == "active", +# 更新: Information.expect_number.isnot(None), +# 更新: Information.created_at >= start_date +# 更新: ).group_by(Information.expect_number).all() +# 更新: +# 更新: return { +# 更新: "days": days, +# 更新: "by_version": [ +# 更新: {"version": r[0] or "未知", "count": r[1], "avg_price": float(r[2] or 0), "max_price": float(r[3] or 0), "min_price": float(r[4] or 0)} +# 更新: for r in by_version if r[0] +# 更新: ], +# 更新: "by_packaging": [ +# 更新: {"packaging": r[0] or "未知", "count": r[1], "avg_price": float(r[2] or 0)} +# 更新: for r in by_packaging if r[0] +# 更新: ], +# 更新: "by_number": [ +# 更新: {"number": r[0], "count": r[1], "avg_price": float(r[2] or 0)} +# 更新: for r in by_number +# 更新: ] +# 更新: } +# 更新: +# 更新: +# 更新: # 获取我的发布列表 +# 更新: @router.get("/my/list", response_model=List[InformationResponse]) +# 更新: def get_my_information_list( +# 更新: page: int = Query(1, ge=1), +# 更新: page_size: int = Query(20, ge=1, le=500), +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取我的发布列表""" +# 更新: items = db.query(Information).options( +# 更新: joinedload(Information.collection) +# 更新: ).filter( +# 更新: Information.user_id == current_user.f99_90_id +# 更新: ).order_by(Information.created_at.desc()).offset((page-1)*page_size).limit(page_size).all() +# 更新: +# 更新: result = [] +# 更新: for item in items: +# 更新: result.append(InformationResponse( +# 更新: id=item.id, +# 更新: user_id=item.user_id, +# 更新: info_type=item.info_type, +# 更新: title=item.title, +# 更新: content=item.content, +# 更新: collection_id=item.collection_id, +# 更新: expect_category=item.expect_category, +# 更新: expect_version=item.expect_version, +# 更新: expect_packaging=item.expect_packaging, +# 更新: expect_number=item.expect_number, +# 更新: expect_price_min=item.expect_price_min, +# 更新: expect_price_max=item.expect_price_max, +# 更新: deal_price=item.deal_price, +# 更新: deal_date=item.deal_date, +# 更新: status=item.status, +# 更新: is_matched=item.is_matched, +# 更新: matched_user_id=item.matched_user_id, +# 更新: matched_contact=item.matched_contact, +# 更新: view_count=item.view_count, +# 更新: contact_count=item.contact_count, +# 更新: created_at=item.created_at, +# 更新: user_name=current_user.f01_01_name, +# 更新: user_avatar=current_user.avatar, +# 更新: collection_name=item.collection.f01_01_name if item.collection else None, +# 更新: collection_category=item.collection.f01_03_category if item.collection else None, +# 更新: collection_version=item.collection.f02_11_version if item.collection else None, +# 更新: collection_number=item.collection.f02_10_prefix_serial if item.collection else None, +# 更新: )) +# 更新: +# 更新: return result +# 更新: +# 更新: # ============ 获取当前用户发布的列表 ============ +# 更新: @router.get("/my") +# 更新: def get_my_information( +# 更新: page: int = Query(1, ge=1), +# 更新: limit: int = Query(20, ge=1, le=100), +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取当前用户发布的信息列表""" +# 更新: total = db.query(Information).filter(Information.author == current_user.f01_01_name).count() +# 更新: infos = db.query(Information).filter( +# 更新: Information.author == current_user.f01_01_name +# 更新: ).order_by(Information.created_at.desc()).offset((page-1)*limit).limit(limit).all() +# 更新: +# 更新: return { +# 更新: "total": total, +# 更新: "list": [{ +# 更新: "id": i.id, +# 更新: "title": i.title, +# 更新: "content": i.content, +# 更新: "info_type": i.info_type, +# 更新: "author": i.author, +# 更新: "created_at": i.created_at.isoformat() if i.created_at else None +# 更新: } for i in infos] +# 更新: } +# 更新: +# 更新: +# 更新: # ============ 匹配寻号 ============ +# 更新: class MatchSeekRequest(BaseModel): +# 更新: info_id: str +# 更新: collection_id: Optional[str] = None +# 更新: contact: Optional[str] = None +# 更新: +# 更新: +# 更新: @router.post("/seek/match-confirm") +# 更新: def match_seek( +# 更新: request: MatchSeekRequest, +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """确认匹配寻号 - 用户愿意交换联系方式给发布者""" +# 更新: info = db.query(Information).filter( +# 更新: Information.id == request.info_id, +# 更新: Information.info_type == "seek", +# 更新: Information.status == "active" +# 更新: ).first() +# 更新: +# 更新: if not info: +# 更新: raise HTTPException(status_code=404, detail="寻配号信息不存在") +# 更新: +# 更新: # 检查是否已被匹配(一个寻号只能被一个用户匹配) +# 更新: if info.is_matched == "matched": +# 更新: raise HTTPException(status_code=400, detail="该寻号已被其他用户匹配") +# 更新: +# 更新: # 更新匹配状态 +# 更新: info.is_matched = "matched" +# 更新: info.matched_user_id = current_user.f99_90_id +# 更新: # 保存匹配者的联系方式 +# 更新: info.matched_contact = request.contact or '' +# 更新: +# 更新: # 更新发布寻号者的内容,显示有藏品被匹配 +# 更新: original_content = info.content or "" +# 更新: # 添加匹配信息:藏品被XX藏友匹配,联系方式为:xxx +# 更新: match_info = f"\n藏品被{current_user.f01_01_name}匹配成功!联系方式:{request.collection_id or '已交换'}" +# 更新: info.content = original_content + match_info +# 更新: +# 更新: db.commit() +# 更新: +# 更新: return {"message": "匹配成功,已通知发布者", "is_matched": "matched"} +# 更新: +# 更新: +# 更新: # ============ 添加留言 ============ +# 更新: class CommentRequest(BaseModel): +# 更新: information_id: str +# 更新: content: str +# 更新: +# 更新: +# 更新: @router.post("/comment") +# 更新: def add_comment( +# 更新: request: CommentRequest, +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """添加留言""" +# 更新: info = db.query(Information).filter( +# 更新: Information.id == request.information_id +# 更新: ).first() +# 更新: +# 更新: if not info: +# 更新: raise HTTPException(status_code=404, detail="资讯不存在") +# 更新: +# 更新: # 创建留言 +# 更新: from app.models.models import InformationComment +# 更新: comment = InformationComment( +# 更新: information_id=request.information_id, +# 更新: user_id=current_user.f99_90_id, +# 更新: content=request.content +# 更新: ) +# 更新: db.add(comment) +# 更新: db.commit() +# 更新: +# 更新: return { +# 更新: "message": "留言成功", +# 更新: "comment": { +# 更新: "id": comment.id, +# 更新: "content": comment.content, +# 更新: "user_name": current_user.f01_01_name, +# 更新: "user_avatar": current_user.avatar, +# 更新: "created_at": comment.created_at +# 更新: } +# 更新: } +# 更新: +# 更新: +# 更新: # ============ 获取评论列表 ============ +# 更新: @router.get("/comments/{information_id}") +# 更新: def get_comments( +# 更新: information_id: str, +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取资讯的评论列表""" +# 更新: from app.models.models import InformationComment +# 更新: comments = db.query(InformationComment).filter( +# 更新: InformationComment.information_id == information_id +# 更新: ).order_by(InformationComment.created_at.desc()).all() +# 更新: +# 更新: return [ +# 更新: { +# 更新: "id": c.id, +# 更新: "content": c.content, +# 更新: "user_name": c.user.f01_01_name if c.user else '匿名用户', +# 更新: "user_avatar": c.user.avatar if c.user else None, +# 更新: "created_at": c.created_at +# 更新: } +# 更新: for c in comments +# 更新: ] +# 更新: +# 更新: +# 更新: # ============ 获取匹配者信息 ============ +# 更新: @router.get("/seek/matched-user/{info_id}") +# 更新: def get_matched_user( +# 更新: info_id: str, +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取寻号的匹配者信息(仅发布者可见)""" +# 更新: info = db.query(Information).filter( +# 更新: Information.id == info_id, +# 更新: Information.info_type == "seek" +# 更新: ).first() +# 更新: +# 更新: if not info: +# 更新: raise HTTPException(status_code=404, detail="寻配号信息不存在") +# 更新: +# 更新: # 只有发布者可以看到匹配者信息 +# 更新: if info.user_id != current_user.f99_90_id: +# 更新: raise HTTPException(status_code=403, detail="无权访问") +# 更新: +# 更新: if not info.matched_user_id: +# 更新: return {"message": "暂无匹配者"} +# 更新: +# 更新: # 获取匹配者信息 +# 更新: matched_user = db.query(User).filter(User.f99_90_id == info.matched_user_id).first() +# 更新: if not matched_user: +# 更新: return {"message": "匹配者不存在"} +# 更新: +# 更新: return { +# 更新: "matched_user_id": info.matched_user_id, +# 更新: "user_name": matched_user.f01_01_name, +# 更新: "phone": matched_user.phone, +# 更新: "matched_contact": info.matched_contact, +# 更新: "matched_at": info.updated_at.isoformat() if info.updated_at else None +# 更新: } +# 更新: +# 更新: +# 更新: # ============ 获取发布者信息 ============ +# 更新: @router.get("/seek/publisher/{info_id}") +# 更新: def get_publisher_info( +# 更新: info_id: str, +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取寻号的发布者信息(仅匹配者可见)""" +# 更新: info = db.query(Information).filter( +# 更新: Information.id == info_id, +# 更新: Information.info_type == "seek" +# 更新: ).first() +# 更新: +# 更新: if not info: +# 更新: raise HTTPException(status_code=404, detail="寻配号信息不存在") +# 更新: +# 更新: # 只有匹配者可以看到发布者信息 +# 更新: if not info.matched_user_id or info.matched_user_id != current_user.f99_90_id: +# 更新: raise HTTPException(status_code=403, detail="无权访问") +# 更新: +# 更新: # 获取发布者信息 +# 更新: publisher = db.query(User).filter(User.f99_90_id == info.user_id).first() +# 更新: if not publisher: +# 更新: return {"message": "发布者不存在"} +# 更新: +# 更新: # 从content中解析联系方式 +# 更新: contact = '' +# 更新: if info.content: +# 更新: import re +# 更新: match = re.search(r'联系方式[:\s]*(.+?)(?:\n|$)', info.content) +# 更新: if match: +# 更新: contact = match.group(1).strip() +# 更新: +# 更新: return { +# 更新: "user_id": info.user_id, +# 更新: "user_name": publisher.f01_01_name, +# 更新: "phone": publisher.phone, +# 更新: "contact": contact, +# 更新: "created_at": info.created_at.isoformat() if info.created_at else None +# 更新: } +# 更新: +# 更新: +# 更新: @router.get("/yichen-posts") +# 更新: def get_yichen_posts(category: str = None, search: str = None, page: int = 1, page_size: int = 20): +# 更新: from app.models.models import Information +# 更新: from sqlalchemy import desc +# 更新: query = db.query(Information).filter(Information.info_type == 'yichen') +# 更新: if category: +# 更新: query = query.filter(Information.expect_category == category) +# 更新: if search: +# 更新: query = query.filter(Information.title.contains(search)) +# 更新: total = query.count() +# 更新: offset = (page - 1) * page_size +# 更新: items = query.order_by(desc(Information.created_at)).offset(offset).limit(page_size).all() +# 更新: return {"data": items, "pagination": {"page": page, "limit": page_size, "total": total}} +# 更新: +# 更新: +# 更新: @router.get("/seek/stats") +# 更新: def get_seek_stats( +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取寻配号统计数据""" +# 更新: # 寻号需求数(seek类型且expect_number不为空的总数) +# 更新: seek_count = db.query(Information).filter( +# 更新: Information.info_type == 'seek', +# 更新: Information.expect_number.isnot(None), +# 更新: Information.expect_number != '' +# 更新: ).count() +# 更新: +# 更新: # 我的匹配:自有藏品匹配成功的寻号帖子数量 +# 更新: # 即 is_matched = 'confirmed' 的记录,用户ID等于当前用户 +# 更新: user_matched_count = 0 +# 更新: if current_user: +# 更新: user_matched_count = db.query(Information).filter( +# 更新: Information.info_type == 'seek', +# 更新: Information.expect_number.isnot(None), +# 更新: Information.expect_number != '', +# 更新: Information.matched_user_id == current_user.f99_90_id, +# 更新: Information.is_matched == 'confirmed' +# 更新: ).count() +# 更新: +# 更新: # 总共匹配:自有匹配成功 + 网络数据匹配成功 +# 更新: # 自有匹配成功:is_matched = 'confirmed' +# 更新: # 网络数据匹配成功:查询每个帖子的network_matched_count并求和 +# 更新: seeks = db.query(Information).filter( +# 更新: Information.info_type == 'seek', +# 更新: Information.expect_number.isnot(None), +# 更新: Information.expect_number != '' +# 更新: ).all() +# 更新: +# 更新: total_self_matched = 0 +# 更新: total_network_matched = 0 +# 更新: for seek in seeks: +# 更新: # 自身匹配成功 +# 更新: if seek.is_matched == 'confirmed': +# 更新: total_self_matched += 1 +# 更新: # 网络数据匹配成功(通过coolbot数据库查询) +# 更新: if seek.expect_number: +# 更新: network_count = match_collections_count_from_coolbot(seek.expect_number) +# 更新: total_network_matched += network_count +# 更新: +# 更新: total_matched_count = total_self_matched + total_network_matched +# 更新: +# 更新: return { +# 更新: "seekCount": seek_count, +# 更新: "userMatchedCount": user_matched_count, +# 更新: "totalMatchedCount": total_matched_count +# 更新: } +# 更新: +# 更新: # 批量解析行情数据API +# 更新: @router.post("/batch-parse") +# 更新: async def batch_parse_deals(text: str = Body(..., embed=True)): +# 更新: """使用AI智能解析批量行情文本""" +# 更新: import httpx +# 更新: import json +# 更新: import re +# 更新: +# 更新: # 使用阿里云百炼Coding Plan API +# 更新: api_key = "sk-sp-d5ce68bb203e48ca857c2aea25255b26" +# 更新: base_url = "https://coding.dashscope.aliyuncs.com/v1" +# 更新: +# 更新: # 更详细的解析提示词 +# 更新: prompt = f"""你是一个专业的龙钞行情数据提取助手。请从以下文本中提取所有龙钞行情记录。 +# 更新: +# 更新: 【解析规则】 +# 更新: 1. 每条记录格式:冠字号 价格 评级/包装 出售者 +# 更新: 2. 冠字号:J0开头的9位数字(如J0298810101) +# 更新: 3. 价格:¥xxx,xxx 格式,去掉逗号转为数字 +# 更新: 4. 评级/包装:PC69/PMG68/爱藏67+/爱藏67 标十 标百 单张 +# 更新: 5. 出售者:人名 +# 更新: 6. 号码分类:根据冠字号数字特征判断(圆圆号/倒置号/金马王/金马号/金山王/天马王/金山号/天马号/朦胧王/朦胧号/如意号/钻石号/永恒号/带7号/带4号) +# 更新: +# 更新: 【输出格式】 +# 更新: 返回JSON数组,每条记录包含: +# 更新: - serial: 冠字号(完整9位,如J0298810101) +# 更新: - price: 价格(数字) +# 更新: - grade: 评级(如PC69, PMG68, 爱藏67+, 爱藏67) +# 更新: - packaging: 包装类型(标十/标百/单张) +# 更新: - category: 号码分类 +# 更新: - seller: 出售者 +# 更新: - date: 交易日(从文本中提取日期,如2026-03-29) +# 更新: +# 更新: 只返回JSON数组,不要其他内容。 +# 更新: +# 更新: 文本: +# 更新: {text}""" +# 更新: +# 更新: try: +# 更新: async with httpx.AsyncClient(timeout=120.0) as client: +# 更新: response = await client.post( +# 更新: f"{base_url}/chat/completions", +# 更新: json={ +# 更新: "model": "qwen3.6-plus", +# 更新: "messages": [ +# 更新: {"role": "system", "content": "你是一个专业的收藏品行情数据提取助手,擅长从文本中提取结构化的交易数据。只返回JSON数组。"}, +# 更新: {"role": "user", "content": prompt} +# 更新: ], +# 更新: "temperature": 0.1 +# 更新: }, +# 更新: headers={ +# 更新: "Authorization": f"Bearer {api_key}", +# 更新: "Content-Type": "application/json" +# 更新: } +# 更新: ) +# 更新: +# 更新: if response.status_code != 200: +# 更新: return {"success": False, "error": f"API错误: {response.status_code}, {response.text[:200]}"} +# 更新: +# 更新: result = response.json() +# 更新: # 阿里云百炼OpenAI兼容格式 +# 更新: choices = result.get("choices", []) +# 更新: content = "" +# 更新: if choices and len(choices) > 0: +# 更新: content = choices[0].get("message", {}).get("content", "") +# 更新: +# 更新: # 解析JSON +# 更新: try: +# 更新: # 尝试提取JSON +# 更新: if "```json" in content: +# 更新: content = content.split("```json")[1].split("```")[0] +# 更新: elif "```" in content: +# 更新: content = content.split("```")[1].split("```")[0] +# 更新: +# 更新: # 尝试直接解析 +# 更新: data = json.loads(content.strip()) +# 更新: return {"success": True, "data": data} +# 更新: except json.JSONDecodeError: +# 更新: # 尝试用正则提取 +# 更新: match = re.search(r'\[.*\]', content, re.DOTALL) +# 更新: if match: +# 更新: try: +# 更新: data = json.loads(match.group()) +# 更新: return {"success": True, "data": data} +# 更新: except: +# 更新: pass +# 更新: return {"success": False, "error": "解析失败", "raw": content[:500]} +# 更新: +# 更新: except Exception as e: +# 更新: return {"success": False, "error": str(e)} +# 更新: +# 更新: # 本地正则解析函数 +# 更新: def parse_deals_locally(text: str, default_packaging: str = '', default_date: str = '', default_platform: str = ''): +# 更新: """本地正则解析批量行情文本""" +# 更新: import re +# 更新: from datetime import datetime +# 更新: results = [] +# 更新: +# 更新: # 尝试从文本中提取日期(可能出现在标题或时间戳中) +# 更新: # 格式如: 3月29日, 2026年3月29日, 2026-03-29 +# 更新: date_patterns = [ +# 更新: r'(\d{1,2})月(\d{1,2})日', +# 更新: r'(\d{4})年(\d{1,2})月(\d{1,2})日', +# 更新: r'(\d{4})-(\d{1,2})-(\d{1,2})' +# 更新: ] +# 更新: +# 更新: extracted_date = None +# 更新: for pattern in date_patterns: +# 更新: match = re.search(pattern, text) +# 更新: if match: +# 更新: try: +# 更新: if len(match.groups()) == 2: +# 更新: # 3月29日 - 使用当前年份 +# 更新: extracted_date = f"2026-{int(match.group(1)):02d}-{int(match.group(2)):02d}" +# 更新: elif len(match.groups()) == 3: +# 更新: if int(match.group(1)) > 2000: +# 更新: # 2026年3月29日 +# 更新: extracted_date = f"{match.group(1)}-{int(match.group(2)):02d}-{int(match.group(3)):02d}" +# 更新: else: +# 更新: # 3月29日格式 +# 更新: extracted_date = f"2026-{int(match.group(1)):02d}-{int(match.group(2)):02d}" +# 更新: break +# 更新: except: +# 更新: pass +# 更新: +# 更新: # 默认使用今天 +# 更新: default_date = datetime.now().strftime('%Y-%m-%d') +# 更新: deal_date = extracted_date or default_date +# 更新: +# 更新: lines = text.strip().split('\n') +# 更新: +# 更新: for line in lines: +# 更新: line = line.strip() +# 更新: if not line or 'J0' not in line: +# 更新: continue +# 更新: +# 更新: # 提取冠字号 J0 + 8-9位数字 +# 更新: serial_match = re.search(r'J0(\d{8,9})', line) +# 更新: if not serial_match: +# 更新: continue +# 更新: +# 更新: serial_num = serial_match.group(1) +# 更新: if len(serial_num) == 9: +# 更新: serial_num = serial_num[:8] +# 更新: serial = 'J0' + serial_num +# 更新: +# 更新: # 提取价格 ¥xxx,xxx 或 xxx,xxx(必须在J0之后) +# 更新: serial_pos = line.find(serial) +# 更新: after_serial = line[serial_pos + len(serial):] +# 更新: price_match = re.search(r'[¥¥]?\s*([1-9][\d,]{2,})', after_serial) +# 更新: if not price_match: +# 更新: continue +# 更新: price = int(price_match.group(1).replace(',', '')) +# 更新: +# 更新: # 提取卖家(价格后面的中文字符) +# 更新: after_price_pos = after_serial.find(price_match.group(0)) + len(price_match.group(0)) +# 更新: after_price = after_serial[after_price_pos:] +# 更新: seller_match = re.search(r'([^\d\s¥¥]+?)(?:\s*$|\s*\n)', after_price) +# 更新: seller = seller_match.group(1).strip() if seller_match else '' +# 更新: +# 更新: # 分类判断 +# 更新: digits = serial_num +# 更新: d = digits +# 更新: category = '通货' +# 更新: if not any(c in d for c in ['1','2','3','4','5','7']): category = '圆圆号' +# 更新: elif not any(c in d for c in ['2','3','4','5','7']): category = '倒置号' +# 更新: elif not any(c in d for c in ['1','2','3','4','7']): category = '金马王' +# 更新: elif not any(c in d for c in ['2','3','4','7']): category = '金马号' +# 更新: elif not any(c in d for c in ['1','2','4','5','7']): category = '金山王' +# 更新: elif not any(c in d for c in ['1','2','4','7']): category = '天马王' +# 更新: elif not any(c in d for c in ['2','4','5','7']): category = '金山号' +# 更新: elif not any(c in d for c in ['2','4','7']): category = '天马号' +# 更新: elif not any(c in d for c in ['1','3','4','5','7']): category = '朦胧王' +# 更新: elif not any(c in d for c in ['3','4','5','7']): category = '朦胧号' +# 更新: elif not any(c in d for c in ['1','3','4','7']): category = '如意号' +# 更新: elif not any(c in d for c in ['3','4','7']): category = '钻石号' +# 更新: elif not any(c in d for c in ['4','7']): category = '永恒号' +# 更新: elif '4' not in d: category = '带7号' +# 更新: +# 更新: # 提取评级机构 PCGS/PMG/ACG/爱藏 +# 更新: grade = '' +# 更新: grading_company = '' +# 更新: packaging = '单张' +# 更新: +# 更新: if 'PC69' in line or 'PC68' in line or 'PC67' in line: +# 更新: grade_match = re.search(r'PC(6[789]|5\d?)', line) +# 更新: grade = 'PC' + grade_match.group(1) if grade_match else '' +# 更新: grading_company = 'PCGS' +# 更新: elif 'PMG68' in line or 'PMG67' in line: +# 更新: grade_match = re.search(r'PMG(6[789]|5\d?)', line) +# 更新: grade = 'PMG' + grade_match.group(1) if grade_match else '' +# 更新: grading_company = 'PMG' +# 更新: elif 'ACG' in line: +# 更新: grade_match = re.search(r'ACG(6[789]|5\d?)', line) +# 更新: grade = 'ACG' + grade_match.group(1) if grade_match else '' +# 更新: grading_company = 'ACG' +# 更新: elif '爱藏67+' in line: +# 更新: grade = '67+' +# 更新: grading_company = '爱藏' +# 更新: elif '爱藏67' in line: +# 更新: grade = '67' +# 更新: grading_company = '爱藏' +# 更新: +# 更新: # 判断包装类型 +# 更新: packaging = '单张' +# 更新: +# 更新: # 如果传入了默认包装类型,先使用默认 +# 更新: if default_packaging: +# 更新: packaging = default_packaging +# 更新: +# 更新: # 简化识别:带"刀"字=标百,带"标"字=标十 +# 更新: if '刀' in line: +# 更新: packaging = '标百' +# 更新: elif '标' in line: +# 更新: packaging = '标十' +# 更新: +# 更新: # 尾号判断:如果冠字号尾号是01/11/21/31/41/51/61/71/81/91,且有刀/标字样,基本确认是标百 +# 更新: if len(serial_num) >= 2: +# 更新: tail = serial_num[-2:] +# 更新: if tail in ['01', '11', '21', '31', '41', '51', '61', '71', '81', '91']: +# 更新: if '刀' in line or ('标' in line and packaging == '单张'): +# 更新: packaging = '标百' +# 更新: packaging = '标百' +# 更新: +# 更新: # 如果没有刀/标字样,但尾号是01且没有其他特征,可能是标百 +# 更新: if packaging == '单张' and len(serial_num) >= 2: +# 更新: tail = serial_num[-2:] +# 更新: if tail == '01': +# 更新: # 检查是否在特定语境下 +# 更新: packaging = '标百' +# 更新: +# 更新: # 计算尾号和大小号 +# 更新: tail_number = '' +# 更新: size_type = '' +# 更新: if packaging == '标十' and len(serial_num) >= 2: +# 更新: tail_number = serial_num[-2:] +# 更新: size_type = tail_number in ['01','11','21','31','41','51'] and '小号' or '大号' +# 更新: elif packaging == '标百' and len(serial_num) >= 3: +# 更新: tail_number = serial_num[-3:] +# 更新: size_type = tail_number in ['101','201','301','401','501'] and '小号' or '大号' +# 更新: +# 更新: results.append({ +# 更新: 'serial': serial, +# 更新: 'price': price, +# 更新: 'category': category, +# 更新: 'seller': seller, +# 更新: 'packaging': packaging, +# 更新: 'grade': grade, +# 更新: 'grading_company': grading_company, +# 更新: 'deal_date': deal_date or default_date, # 成交时间 +# 更新: 'entry_date': default_date, # 录入时间 +# 更新: 'is_graded': bool(grade), +# 更新: 'tail_number': tail_number, # 尾号 +# 更新: 'size_type': size_type, # 大小号 +# 更新: 'platform': default_platform # 平台 +# 更新: }) +# 更新: +# 更新: return results +# 更新: +# 更新: @router.post("/batch-parse-local") +# 更新: async def batch_parse_deals_local(request: dict = Body(...)): +# 更新: """本地正则解析批量行情文本(无需AI)""" +# 更新: text = request.get('text', '') +# 更新: default_packaging = request.get('defaultPackaging', '') +# 更新: default_date = request.get('defaultDate', '') +# 更新: default_platform = request.get('defaultPlatform', '') +# 更新: results = parse_deals_locally(text, default_packaging, default_date, default_platform) +# 更新: return {"success": True, "data": results} +# 更新: diff --git a/backend/app/routers/news.py b/backend/app/routers/news.py index c969522..dee682d 100644 --- a/backend/app/routers/news.py +++ b/backend/app/routers/news.py @@ -1,128 +1,262 @@ +# news - 新闻路由 +# Version: 1.2.80 +# 更新: + from fastapi import APIRouter, Depends, HTTPException, Query +# 更新: +# Version: 1.2.x +# 更新: from sqlalchemy import Table, MetaData +# 更新: from sqlalchemy.orm import Session +# 更新: from pydantic import BaseModel +# 更新: from typing import Optional, List +# 更新: from datetime import datetime, date +# 更新: from app.core.database import get_db, engine +# 更新: from app.models.models import User +# 更新: from app.routers.auth import get_current_user +# 更新: +# 更新: router = APIRouter(prefix="/api/news", tags=["资讯"]) +# 更新: metadata = MetaData() +# 更新: +# 更新: # 分类表 +# 更新: categories_table = Table('news_categories', metadata, autoload_with=engine) +# 更新: news_table = Table('news', metadata, autoload_with=engine) +# 更新: user_posts_table = Table('user_posts', metadata, autoload_with=engine) +# 更新: users_table = Table('users', metadata, autoload_with=engine) +# 更新: deals_table = Table('deals', metadata, autoload_with=engine) +# 更新: notifications_table = Table('notifications', metadata, autoload_with=engine) +# 更新: +# 更新: # ============ 获取分类 ============ +# 更新: @router.get("/categories") +# 更新: def get_categories(db: Session = Depends(get_db)): +# 更新: results = db.query(categories_table).order_by(categories_table.c.sort_order).all() +# 更新: return [dict(r._mapping) for r in results] +# 更新: +# 更新: # ============ 获取资讯 ============ +# 更新: @router.get("") +# 更新: def get_news( +# 更新: category_id: Optional[int] = None, +# 更新: page: int = 1, +# 更新: limit: int = 20, +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: query = db.query(news_table) +# 更新: if category_id: +# 更新: query = query.filter(news_table.c.category_id == category_id) +# 更新: offset = (page - 1) * limit +# 更新: results = query.order_by(news_table.c.created_at.desc()).offset(offset).limit(limit).all() +# 更新: return [dict(r._mapping) for r in results] +# 更新: +# 更新: # ============ 获取用户发布 ============ +# 更新: @router.get("/posts") +# 更新: def get_posts( +# 更新: post_type: Optional[str] = None, +# 更新: status: str = "active", +# 更新: page: int = 1, +# 更新: limit: int = 20, +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: query = db.query(user_posts_table).filter(user_posts_table.c.status == status) +# 更新: if post_type: +# 更新: query = query.filter(user_posts_table.c.post_type == post_type) +# 更新: offset = (page - 1) * limit +# 更新: results = query.order_by(user_posts_table.c.created_at.desc()).offset(offset).limit(limit).all() +# 更新: return [dict(r._mapping) for r in results] +# 更新: +# 更新: # ============ 创建发布 ============ +# 更新: class PostCreate(BaseModel): +# 更新: post_type: str +# 更新: title: str +# 更新: content: Optional[str] = None +# 更新: zodiac_type: Optional[str] = None +# 更新: packaging: Optional[str] = None +# 更新: +# 更新: @router.post("/posts") +# 更新: def create_post( +# 更新: post: PostCreate, +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: result = db.execute(user_posts_table.insert().values( +# 更新: user_id=current_user.f99_90_id, +# 更新: post_type=post.post_type, +# 更新: title=post.title, +# 更新: content=post.content, +# 更新: zodiac_type=post.zodiac_type, +# 更新: packaging=post.packaging, +# 更新: status="pending" +# 更新: )) +# 更新: db.commit() +# 更新: return {"success": True, "id": result.inserted_primary_key[0]} +# 更新: +# 更新: # ============ 成交数据 ============ +# 更新: @router.get("/deals") +# 更新: def get_deals( +# 更新: zodiac_type: Optional[str] = None, +# 更新: limit: int = 20, +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: query = db.query(deals_table) +# 更新: if zodiac_type: +# 更新: query = query.filter(deals_table.c.zodiac_type == zodiac_type) +# 更新: results = query.order_by(deals_table.c.deal_date.desc()).limit(limit).all() +# 更新: return [dict(r._mapping) for r in results] +# 更新: +# 更新: # ============ 通知 ============ +# 更新: @router.get("/notifications") +# 更新: def get_notifications(limit: int = 10, db: Session = Depends(get_db)): +# 更新: results = db.query(notifications_table).filter( +# 更新: notifications_table.c.is_published == True +# 更新: ).order_by(notifications_table.c.created_at.desc()).limit(limit).all() +# 更新: return [dict(r._mapping) for r in results] +# 更新: +# 更新: # ============ 首页数据 ============ +# 更新: @router.get("/home") +# 更新: def get_home(db: Session = Depends(get_db)): +# 更新: # 推荐发布 +# 更新: posts = db.query(user_posts_table).filter( +# 更新: user_posts_table.c.status == "active" +# 更新: ).order_by(user_posts_table.c.created_at.desc()).limit(10).all() +# 更新: +# 更新: # 成交 +# 更新: deals = db.query(deals_table).order_by( +# 更新: deals_table.c.deal_date.desc() +# 更新: ).limit(10).all() +# 更新: +# 更新: # 通知 +# 更新: notices = db.query(notifications_table).filter( +# 更新: notifications_table.c.is_published == True +# 更新: ).order_by(notifications_table.c.created_at.desc()).limit(5).all() +# 更新: +# 更新: return { +# 更新: "posts": [dict(p._mapping) for p in posts], +# 更新: "deals": [dict(d._mapping) for d in deals], +# 更新: "notices": [dict(n._mapping) for n in notices] +# 更新: } +# 更新: diff --git a/backend/app/routers/ocr.py b/backend/app/routers/ocr.py index e9dffc1..69b3a3c 100644 --- a/backend/app/routers/ocr.py +++ b/backend/app/routers/ocr.py @@ -1,384 +1,770 @@ -# OCR 识别路由 - 专业人民币生肖纪念钞鉴定 +# ocr - OCR识别路由 +# Version: 1.2.75 +# 更新: + import os +# 更新: import uuid +# 更新: import base64 +# 更新: import httpx +# 更新: from fastapi import APIRouter, Depends, HTTPException, UploadFile, File +# 更新: from sqlalchemy.orm import Session +# 更新: from app.core.database import get_db +# 更新: from app.core.auth import get_current_user +# 更新: from app.models.models import User +# 更新: +# 更新: router = APIRouter(prefix="/api/ocr", tags=["OCR 识别"]) +# 更新: +# 更新: # 阿里云 DashScope API 配置 +# 更新: DASHSCOPE_API_KEY = os.getenv("DASHSCOPE_API_KEY", "sk-9389024a37da4f7bb455ac9a6b28776f") +# 更新: +# 更新: # 阿里云 OSS 配置 +# 更新: OSS_CONFIG = { +# 更新: "access_key_id": os.getenv("OSS_ACCESS_KEY_ID", "LTAI5t6HUnpFBLEK9194kPVG"), +# 更新: "access_key_secret": os.getenv("OSS_ACCESS_KEY_SECRET", "LEr4Q8yRxb8D5b24cKfCwlt4MMoke1"), +# 更新: "bucket_name": "jiachenlong-oss", +# 更新: "endpoint": "oss-cn-hangzhou.aliyuncs.com", +# 更新: "public_url": "https://jiachenlong-oss.oss-cn-hangzhou.aliyuncs.com" +# 更新: } +# 更新: +# 更新: # 临时上传目录(用于OCR识别本地备选) +# 更新: UPLOAD_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "uploads", "temp") +# 更新: os.makedirs(UPLOAD_DIR, exist_ok=True) +# 更新: +# 更新: +# 更新: def get_oss_path(file_type: str, user_id: str = None, collection_id: str = None, filename: str = None): +# 更新: """生成OSS路径 - 按年/月/日分类""" +# 更新: from datetime import datetime +# 更新: now = datetime.now() +# 更新: year = now.strftime("%Y") +# 更新: month = now.strftime("%m") +# 更新: day = now.strftime("%d") +# 更新: +# 更新: if file_type == "temp": +# 更新: # 临时文件: temp/{year}/{month}/{day}/{uuid}.{ext} +# 更新: import uuid +# 更新: unique_id = str(uuid.uuid4()) +# 更新: ext = filename.split('.')[-1] if filename and '.' in filename else 'jpg' +# 更新: return f"temp/{year}/{month}/{day}/{unique_id}.{ext}", unique_id +# 更新: +# 更新: elif file_type == "collection": +# 更新: # 藏品文件: collections/{user_id}/{year}/{collection_id}/{filename} +# 更新: if not user_id or not collection_id: +# 更新: raise ValueError("user_id and collection_id required for collection") +# 更新: return f"collections/{user_id}/{year}/{collection_id}/{filename}" +# 更新: +# 更新: elif file_type == "avatar": +# 更新: # 头像: avatars/{user_id}/avatar.{ext} +# 更新: if not user_id: +# 更新: raise ValueError("user_id required for avatar") +# 更新: ext = filename.split('.')[-1] if filename and '.' in filename else 'jpg' +# 更新: return f"avatars/{user_id}/avatar.{ext}" +# 更新: +# 更新: return None +# 更新: +# 更新: +# 更新: # 上传图片到OSS - 使用服务层(带压缩) +# 更新: from app.services.oss import upload_to_oss as oss_upload +# 更新: +# 更新: def upload_to_oss(file_data, oss_key): +# 更新: """上传文件到阿里云OSS(带自动压缩)""" +# 更新: return oss_upload(file_data, oss_key) +# 更新: +# 更新: # 专业提示词 +# 更新: PROFESSIONAL_PROMPT = """你是一名专业的人民币生肖纪念钞鉴定专家。请严格按照以下步骤分析这张图片,并提取准确信息。 +# 更新: +# 更新: 【识别流程】 +# 更新: 1. 判断类型是否评级钞:首先确认是否为裸钞还是评级钞(有封装盒和标签) +# 更新: 2. 验证纪念钞特征:对照生肖纪念钞特征进行确认 +# 更新: 3. 验证评级类型:有'标十'字眼的为标十,有'百连'字眼的为标百,其他为单张 +# 更新: 4. 提取信息:仔细阅读标签上的所有文字内容 +# 更新: +# 更新: 【版别格式要求】 +# 更新: 只需要:年份 + 属相,例如: +# 更新: - 2024 龙 +# 更新: - 2025 蛇 +# 更新: - 2026 马 +# 更新: +# 更新: 【输出要求】 +# 更新: 严格按照以下格式输出,每个字段必须填写具体值: +# 更新: ✅ 1 发行机构:中国人民银行 +# 更新: ✅ 2 发行版别:2024 龙 +# 更新: ✅ 3 面额:贰拾圆 +# 更新: ✅ 4 是否评级:是/否 +# 更新: ✅ 5 封装类型:裸钞/单张/标十/标百 +# 更新: ✅ 6 冠字序号:J0xxxxxxxx +# 更新: ✅ 7 评级机构:ACG/PCGS/PMG +# 更新: ✅ 8 评级分数:67/68/69 +# 更新: ✅ 9 是否三星:是/否 +# 更新: ✅ 10 特殊标识:金山标/天马标/红绳版等 +# 更新: ✅ 11 号码特征:金山号 2 张,天马号 3 张等 +# 更新: +# 更新: 现在请仔细分析提供的图片,按上述格式输出结果。""" +# 更新: +# 更新: +# 更新: @router.post("/recognize") +# 更新: async def recognize_image( +# 更新: image: UploadFile = File(...), +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """OCR 图片识别 - 识别后自动保存图片到OSS临时目录""" +# 更新: try: +# 更新: # 读取图片数据 +# 更新: image_data = await image.read() +# 更新: image_base64 = base64.b64encode(image_data).decode('utf-8') +# 更新: +# 更新: # 生成OSS临时路径: temp/{year}/{month}/{day}/{uuid}.{ext} +# 更新: oss_key, temp_id = get_oss_path("temp", filename=image.filename) +# 更新: +# 更新: # 初始化temp_path为空 +# 更新: temp_path = None +# 更新: +# 更新: # 上传到OSS +# 更新: try: +# 更新: image_url = upload_to_oss(image_data, oss_key) +# 更新: except Exception as oss_err: +# 更新: # OSS失败时保存到本地作为备选 +# 更新: temp_path = os.path.join(UPLOAD_DIR, oss_key.split('/')[-1]) +# 更新: os.makedirs(os.path.dirname(temp_path), exist_ok=True) +# 更新: with open(temp_path, 'wb') as f: +# 更新: f.write(image_data) +# 更新: image_url = f"/uploads/temp/{oss_key.split('/')[-1]}" +# 更新: +# 更新: headers = { +# 更新: "Authorization": f"Bearer {DASHSCOPE_API_KEY}", +# 更新: "Content-Type": "application/json" +# 更新: } +# 更新: +# 更新: # 阿里云 DashScope API 格式 (qwen-vl-plus 视觉模型) +# 更新: payload = { +# 更新: "model": "qwen-vl-plus", +# 更新: "input": { +# 更新: "messages": [{ +# 更新: "role": "user", +# 更新: "content": [ +# 更新: { +# 更新: "image": f"data:{image.content_type};base64,{image_base64}" +# 更新: }, +# 更新: { +# 更新: "text": PROFESSIONAL_PROMPT +# 更新: } +# 更新: ] +# 更新: }] +# 更新: }, +# 更新: "parameters": { +# 更新: "max_tokens": 1000 +# 更新: } +# 更新: } +# 更新: +# 更新: async with httpx.AsyncClient(timeout=60.0) as client: +# 更新: response = await client.post( +# 更新: "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation", +# 更新: json=payload, +# 更新: headers=headers +# 更新: ) +# 更新: +# 更新: if response.status_code != 200: +# 更新: # 识别失败,删除临时文件 +# 更新: if temp_path and os.path.exists(temp_path): +# 更新: os.remove(temp_path) +# 更新: raise HTTPException(status_code=500, detail=f"OCR API 调用失败:{response.text[:200]}") +# 更新: +# 更新: ocr_result = response.json() +# 更新: text_content = "" +# 更新: # 新版API返回格式 +# 更新: if "output" in ocr_result and "choices" in ocr_result["output"]: +# 更新: choices = ocr_result["output"]["choices"] +# 更新: if choices and len(choices) > 0: +# 更新: content = choices[0].get("message", {}).get("content", []) +# 更新: if content and len(content) > 0: +# 更新: text_content = content[0].get("text", "") +# 更新: +# 更新: fields = extract_fields(text_content) +# 更新: +# 更新: # 更新用户AI识别次数 +# 更新: current_user.f99_95_ai_count = (current_user.f99_95_ai_count or 0) + 1 +# 更新: db.commit() +# 更新: +# 更新: # 返回识别结果和临时图片路径 +# 更新: return { +# 更新: "success": True, +# 更新: "text": text_content, +# 更新: "fields": fields, +# 更新: "aiCount": current_user.f99_95_ai_count, +# 更新: "temp_image": { +# 更新: "id": temp_id, +# 更新: "filename": oss_key.split('/')[-1], +# 更新: "path": image_url, +# 更新: "original_name": image.filename, +# 更新: "is_oss": image_url.startswith("https://") +# 更新: } +# 更新: } +# 更新: +# 更新: except Exception as e: +# 更新: import traceback +# 更新: error_detail = f"识别失败:{str(e)}\n{traceback.format_exc()}" +# 更新: raise HTTPException(status_code=500, detail=error_detail) +# 更新: +# 更新: +# 更新: def extract_fields(text: str) -> dict: +# 更新: """从 OCR 文本中提取字段 - 直接返回 AI 识别结果""" +# 更新: import re +# 更新: fields = {} +# 更新: +# 更新: # 解析结构化输出 +# 更新: patterns = { +# 更新: 'issuer': r'✅.*?1.*?发行机构.*?[::]\s*(.+?)(?:\n|$)', +# 更新: 'version': r'✅.*?2.*?发行版别.*?[::]\s*(.+?)(?:\n|$)', +# 更新: 'denomination': r'✅.*?3.*?面额.*?[::]\s*(.+?)(?:\n|$)', +# 更新: 'is_graded_text': r'✅.*?4.*?是否评级.*?[::]\s*(.+?)(?:\n|$)', +# 更新: 'packaging': r'✅.*?5.*?封装类型.*?[::]\s*(.+?)(?:\n|$)', +# 更新: 'prefix_serial': r'✅.*?6.*?冠字序号.*?[::]\s*(.+?)(?:\n|$)', +# 更新: 'grading_company': r'✅.*?7.*?评级机构.*?[::]\s*(.+?)(?:\n|$)', +# 更新: 'grading_score': r'✅.*?8.*?评级分数.*?[::]\s*(.+?)(?:\n|$)', +# 更新: 'three_star_text': r'✅.*?9.*?是否三星.*?[::]\s*(.+?)(?:\n|$)', +# 更新: 'special_mark': r'✅.*?10.*?特殊标识.*?[::]\s*(.+?)(?:\n|$)', +# 更新: 'serial_feature': r'✅.*?11.*?号码特征.*?[::]\s*(.+?)(?:\n|$)' +# 更新: } +# 更新: +# 更新: for field, pattern in patterns.items(): +# 更新: match = re.search(pattern, text, re.IGNORECASE | re.DOTALL) +# 更新: if match: +# 更新: value = match.group(1).strip() +# 更新: # 保留所有值,包括"无"和"未识别",让前端处理 +# 更新: fields[field] = value +# 更新: +# 更新: # 处理是否评级 +# 更新: if 'is_graded_text' in fields: +# 更新: fields['is_graded'] = '是' in fields.pop('is_graded_text') +# 更新: +# 更新: # 处理是否三星 +# 更新: if 'three_star_text' in fields: +# 更新: fields['three_star'] = '是' in fields.pop('three_star_text') +# 更新: +# 更新: # 简化版别字段(2024 龙年贺岁纪念钞(标十) → 2024 龙) +# 更新: if 'version' in fields: +# 更新: version = fields['version'] +# 更新: # 提取年份和生肖 +# 更新: year_match = re.search(r'(20\d{2})', version) +# 更新: animal = '' +# 更新: if '龙' in version: +# 更新: animal = '龙' +# 更新: elif '蛇' in version: +# 更新: animal = '蛇' +# 更新: elif '马' in version: +# 更新: animal = '马' +# 更新: elif '羊' in version: +# 更新: animal = '羊' +# 更新: elif '猴' in version: +# 更新: animal = '猴' +# 更新: elif '鸡' in version: +# 更新: animal = '鸡' +# 更新: elif '狗' in version: +# 更新: animal = '狗' +# 更新: elif '猪' in version: +# 更新: animal = '猪' +# 更新: elif '鼠' in version: +# 更新: animal = '鼠' +# 更新: elif '牛' in version: +# 更新: animal = '牛' +# 更新: elif '虎' in version: +# 更新: animal = '虎' +# 更新: elif '兔' in version: +# 更新: animal = '兔' +# 更新: +# 更新: if year_match and animal: +# 更新: fields['version'] = f"{year_match.group(1)}{animal}" +# 更新: +# 更新: return fields +# 更新: +# 更新: +# 更新: @router.post("/claim-temp-image") +# 更新: async def claim_temp_image( +# 更新: temp_id: str, +# 更新: collection_id: str, +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """将临时图片移动到正式藏品目录 - 按用户/年/藏品ID分类""" +# 更新: from app.models.models import Collection, CollectionImage +# 更新: from datetime import datetime +# 更新: +# 更新: # 验证藏品是否存在 +# 更新: 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="藏品不存在") +# 更新: +# 更新: # 生成正式文件名和OSS路径: collections/{user_id}/{year}/{collection_id}/{filename} +# 更新: code = collection.f01_02_code or "0000" +# 更新: prefix = collection.f02_10_prefix_serial or "" +# 更新: username = current_user.f01_01_name +# 更新: import time +# 更新: final_filename = f"{username}-{code}-{prefix}-{int(time.time())}.jpg" +# 更新: oss_key = get_oss_path("collection", user_id=current_user.f99_90_id, collection_id=collection_id, filename=final_filename) +# 更新: +# 更新: # 尝试从OSS获取临时图片 - 尝试多种扩展名和日期路径 +# 更新: temp_extensions = ['jpg', 'jpeg', 'png', 'gif', 'JPG', 'JPEG', 'PNG', 'GIF'] +# 更新: temp_content = None +# 更新: found_key = None +# 更新: +# 更新: # 尝试最近7天的路径 +# 更新: from datetime import timedelta +# 更新: for i in range(7): +# 更新: date = datetime.now() - timedelta(days=i) +# 更新: year = date.strftime("%Y") +# 更新: month = date.strftime("%m") +# 更新: day = date.strftime("%d") +# 更新: +# 更新: for ext in temp_extensions: +# 更新: try: +# 更新: temp_oss_key = f"temp/{year}/{month}/{day}/{temp_id}.{ext}" +# 更新: import oss2 +# 更新: auth = oss2.Auth(OSS_CONFIG["access_key_id"], OSS_CONFIG["access_key_secret"]) +# 更新: bucket = oss2.Bucket(auth, OSS_CONFIG["endpoint"], OSS_CONFIG["bucket_name"]) +# 更新: temp_content = bucket.get_object(temp_oss_key).read() +# 更新: found_key = temp_oss_key +# 更新: break +# 更新: except: +# 更新: continue +# 更新: if temp_content: +# 更新: break +# 更新: +# 更新: if temp_content: +# 更新: # 上传到正式目录 +# 更新: bucket.put_object(oss_key, temp_content) +# 更新: +# 更新: # 删除临时图片 +# 更新: try: +# 更新: bucket.delete_object(found_key) +# 更新: except: +# 更新: pass +# 更新: +# 更新: # OSS URL +# 更新: image_path = f"{OSS_CONFIG['public_url']}/{oss_key}" +# 更新: +# 更新: else: +# 更新: # OSS失败,使用本地文件 +# 更新: temp_path = None +# 更新: for ext in temp_extensions: +# 更新: temp_path = os.path.join(UPLOAD_DIR, f"{temp_id}.{ext}") +# 更新: if os.path.exists(temp_path): +# 更新: break +# 更新: +# 更新: if not temp_path or not os.path.exists(temp_path): +# 更新: raise HTTPException(status_code=404, detail="临时图片不存在或已过期") +# 更新: +# 更新: # 保存到本地 +# 更新: collection_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "uploads", "collections") +# 更新: os.makedirs(collection_dir, exist_ok=True) +# 更新: +# 更新: new_path = os.path.join(collection_dir, final_filename) +# 更新: import shutil +# 更新: shutil.move(temp_path, new_path) +# 更新: image_path = f"uploads/collections/{final_filename}" +# 更新: +# 更新: # 创建图片记录 +# 更新: image_record = CollectionImage( +# 更新: id=str(uuid.uuid4()), +# 更新: collection_id=collection.f99_90_id, +# 更新: filename=final_filename, +# 更新: original_name=temp_id, +# 更新: path=image_path +# 更新: ) +# 更新: db.add(image_record) +# 更新: db.commit() +# 更新: +# 更新: return { +# 更新: "success": True, +# 更新: "image": { +# 更新: "id": image_record.id, +# 更新: "filename": image_record.filename, +# 更新: "path": image_record.path +# 更新: } +# 更新: } +# 更新: diff --git a/backend/app/routers/operations.py b/backend/app/routers/operations.py index 0881599..f69a5f7 100644 --- a/backend/app/routers/operations.py +++ b/backend/app/routers/operations.py @@ -1,104 +1,210 @@ -# 操作路由 +# operations - 运营操作路由 +# Version: 1.2.70 +# 更新: + from typing import List, Optional +# 更新: from fastapi import APIRouter, Depends, HTTPException, status, Query +# 更新: from sqlalchemy.orm import Session +# 更新: from app.core.database import get_db +# 更新: from app.core.auth import get_current_user +# 更新: from app.models.models import User, Collection, Operation +# 更新: from app.schemas.schemas import OperationCreate, OperationResponse +# 更新: +# 更新: router = APIRouter(prefix="/api", tags=["操作"]) +# 更新: +# 更新: +# 更新: @router.get("/operations", response_model=List[OperationResponse]) +# 更新: def get_operations( +# 更新: collection_id: Optional[str] = None, +# 更新: page: int = Query(1, ge=1), +# 更新: limit: int = Query(50, ge=1, le=100), +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取操作历史""" +# 更新: query = db.query(Operation).filter(Operation.user_id == current_user.id) +# 更新: +# 更新: if collection_id: +# 更新: query = query.filter(Operation.collection_id == collection_id) +# 更新: +# 更新: operations = query.order_by(Operation.created_at.desc()) \ +# 更新: .offset((page - 1) * limit) \ +# 更新: .limit(limit) \ +# 更新: .all() +# 更新: +# 更新: return operations +# 更新: +# 更新: +# 更新: @router.get("/operations/history") +# 更新: def get_operation_history( +# 更新: collection_id: Optional[str] = None, +# 更新: type: Optional[str] = None, +# 更新: start_date: Optional[str] = None, +# 更新: end_date: Optional[str] = None, +# 更新: page: int = Query(1, ge=1), +# 更新: limit: int = Query(50, ge=1, le=100), +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取操作历史(带统计)""" +# 更新: query = db.query(Operation).filter(Operation.user_id == current_user.id) +# 更新: +# 更新: if collection_id: +# 更新: query = query.filter(Operation.collection_id == collection_id) +# 更新: if type: +# 更新: query = query.filter(Operation.type == type) +# 更新: if start_date: +# 更新: query = query.filter(Operation.created_at >= start_date) +# 更新: if end_date: +# 更新: query = query.filter(Operation.created_at <= end_date) +# 更新: +# 更新: total = query.count() +# 更新: +# 更新: data = query.order_by(Operation.created_at.desc()) \ +# 更新: .offset((page - 1) * limit) \ +# 更新: .limit(limit) \ +# 更新: .all() +# 更新: +# 更新: return { +# 更新: "data": data, +# 更新: "pagination": { +# 更新: "page": page, +# 更新: "limit": limit, +# 更新: "total": total, +# 更新: "pages": (total + limit - 1) // limit +# 更新: } +# 更新: } +# 更新: +# 更新: +# 更新: @router.post("/operations", response_model=OperationResponse) +# 更新: def create_operation( +# 更新: operation_data: OperationCreate, +# 更新: current_user: User = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """创建操作记录""" +# 更新: # 验证藏品存在 +# 更新: collection = db.query(Collection).filter( +# 更新: Collection.id == operation_data.collection_id, +# 更新: Collection.user_id == current_user.id +# 更新: ).first() +# 更新: +# 更新: if not collection: +# 更新: raise HTTPException(status_code=404, detail="藏品不存在") +# 更新: +# 更新: operation = Operation( +# 更新: collection_id=operation_data.collection_id, +# 更新: user_id=current_user.id, +# 更新: type=operation_data.type, +# 更新: price=operation_data.price, +# 更新: note=operation_data.note +# 更新: ) +# 更新: +# 更新: db.add(operation) +# 更新: db.commit() +# 更新: db.refresh(operation) +# 更新: +# 更新: return operation +# 更新: diff --git a/backend/app/routers/seek.py b/backend/app/routers/seek.py index 76ff298..ce5e502 100644 --- a/backend/app/routers/seek.py +++ b/backend/app/routers/seek.py @@ -1,189 +1,384 @@ +# seek - 寻号匹配路由 +# Version: 1.2.70 +# 更新: + from fastapi import APIRouter, Depends, Query, HTTPException +# 更新: +# Version: 1.2.x +# 更新: from sqlalchemy.orm import Session +# 更新: from pydantic import BaseModel +# 更新: from typing import Optional +# 更新: from datetime import datetime +# 更新: from app.core.database import get_db +# 更新: from app.core.auth import get_current_user +# 更新: from app.models.seek_info import SeekInfo +# 更新: +# 更新: router = APIRouter(prefix="/api/seek", tags=["寻配号"]) +# 更新: +# 更新: # ============ Schema ============ +# 更新: class SeekInfoCreate(BaseModel): +# 更新: title: str +# 更新: content: Optional[str] = None +# 更新: expect_category: Optional[str] = None +# 更新: expect_version: Optional[str] = None +# 更新: expect_packaging: Optional[str] = None +# 更新: expect_number: Optional[str] = None +# 更新: expect_price_min: Optional[float] = None +# 更新: expect_price_max: Optional[float] = None +# 更新: +# 更新: class SeekInfoUpdate(BaseModel): +# 更新: title: Optional[str] = None +# 更新: content: Optional[str] = None +# 更新: expect_category: Optional[str] = None +# 更新: expect_version: Optional[str] = None +# 更新: expect_packaging: Optional[str] = None +# 更新: expect_number: Optional[str] = None +# 更新: expect_price_min: Optional[float] = None +# 更新: expect_price_max: Optional[float] = None +# 更新: status: Optional[str] = None +# 更新: +# 更新: class SeekInfoResponse(BaseModel): +# 更新: id: str +# 更新: user_id: str +# 更新: title: str +# 更新: content: Optional[str] +# 更新: expect_category: Optional[str] +# 更新: expect_version: Optional[str] +# 更新: expect_packaging: Optional[str] +# 更新: expect_number: Optional[str] +# 更新: expect_price_min: Optional[float] +# 更新: expect_price_max: Optional[float] +# 更新: status: str +# 更新: is_matched: Optional[str] +# 更新: matched_user_id: Optional[str] +# 更新: matched_contact: Optional[str] +# 更新: view_count: int +# 更新: contact_count: int +# 更新: created_at: Optional[datetime] +# 更新: updated_at: Optional[datetime] +# 更新: +# 更新: class Config: +# 更新: from_attributes = True +# 更新: +# 更新: # ============ API ============ +# 更新: @router.get("/list", response_model=list[SeekInfoResponse]) +# 更新: def get_seek_list( +# 更新: status: str = Query("active"), +# 更新: page: int = Query(1, ge=1), +# 更新: page_size: int = Query(20, ge=1, le=1000), +# 更新: user_only: bool = Query(False), +# 更新: current_user: Optional = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取寻配号列表""" +# 更新: query = db.query(SeekInfo).filter(SeekInfo.status == status) +# 更新: +# 更新: # 我的寻配号:只查看自己的 +# 更新: if user_only and current_user: +# 更新: query = query.filter(SeekInfo.user_id == current_user.f99_90_id) +# 更新: +# 更新: # 排序 +# 更新: query = query.order_by(SeekInfo.created_at.desc()) +# 更新: +# 更新: # 分页 +# 更新: offset = (page - 1) * page_size +# 更新: items = query.offset(offset).limit(page_size).all() +# 更新: +# 更新: return items +# 更新: +# 更新: @router.get("/stats") +# 更新: def get_seek_stats( +# 更新: current_user = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取寻配号统计""" +# 更新: total = db.query(SeekInfo).filter(SeekInfo.status == "active").count() +# 更新: matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count() +# 更新: +# 更新: return { +# 更新: "total": total, +# 更新: "matched": matched, +# 更新: "unmatched": total - matched +# 更新: } +# 更新: +# 更新: @router.post("", response_model=SeekInfoResponse) +# 更新: def create_seek( +# 更新: data: SeekInfoCreate, +# 更新: current_user = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """创建寻配号""" +# 更新: if not current_user: +# 更新: raise HTTPException(status_code=401, detail="请先登录") +# 更新: +# 更新: seek = SeekInfo( +# 更新: user_id=current_user.f99_90_id, +# 更新: title=data.title, +# 更新: content=data.content, +# 更新: expect_category=data.expect_category, +# 更新: expect_version=data.expect_version, +# 更新: expect_packaging=data.expect_packaging, +# 更新: expect_number=data.expect_number, +# 更新: expect_price_min=data.expect_price_min, +# 更新: expect_price_max=data.expect_price_max, +# 更新: status="active" +# 更新: ) +# 更新: db.add(seek) +# 更新: db.commit() +# 更新: db.refresh(seek) +# 更新: return seek +# 更新: +# 更新: @router.get("/{seek_id}", response_model=SeekInfoResponse) +# 更新: def get_seek( +# 更新: seek_id: str, +# 更新: current_user = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """获取寻配号详情""" +# 更新: seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first() +# 更新: if not seek: +# 更新: raise HTTPException(status_code=404, detail="寻配号不存在") +# 更新: +# 更新: # 增加浏览数 +# 更新: seek.view_count += 1 +# 更新: db.commit() +# 更新: +# 更新: return seek +# 更新: +# 更新: @router.put("/{seek_id}", response_model=SeekInfoResponse) +# 更新: def update_seek( +# 更新: seek_id: str, +# 更新: data: SeekInfoUpdate, +# 更新: current_user = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """更新寻配号""" +# 更新: if not current_user: +# 更新: raise HTTPException(status_code=401, detail="请先登录") +# 更新: +# 更新: seek = db.query(SeekInfo).filter( +# 更新: SeekInfo.id == seek_id, +# 更新: SeekInfo.user_id == current_user.f99_90_id +# 更新: ).first() +# 更新: +# 更新: if not seek: +# 更新: raise HTTPException(status_code=404, detail="寻配号不存在") +# 更新: +# 更新: for key, value in data.model_dump(exclude_unset=True).items(): +# 更新: setattr(seek, key, value) +# 更新: +# 更新: db.commit() +# 更新: db.refresh(seek) +# 更新: return seek +# 更新: +# 更新: @router.delete("/{seek_id}") +# 更新: def delete_seek( +# 更新: seek_id: str, +# 更新: current_user = Depends(get_current_user), +# 更新: db: Session = Depends(get_db) +# 更新: ): +# 更新: """删除寻配号""" +# 更新: if not current_user: +# 更新: raise HTTPException(status_code=401, detail="请先登录") +# 更新: +# 更新: seek = db.query(SeekInfo).filter( +# 更新: SeekInfo.id == seek_id, +# 更新: SeekInfo.user_id == current_user.f99_90_id +# 更新: ).first() +# 更新: +# 更新: if not seek: +# 更新: raise HTTPException(status_code=404, detail="寻配号不存在") +# 更新: +# 更新: seek.status = "deleted" +# 更新: db.commit() +# 更新: - return {"message": "删除成功"} \ No newline at end of file +# 更新: + return {"message": "删除成功"} +# 更新: diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py index f5a9a0c..ce49ae2 100644 --- a/backend/app/routers/users.py +++ b/backend/app/routers/users.py @@ -1,4 +1,7 @@ -# 用户管理路由 +# users.py - 用户管理路由 +# Version: 1.2.98 (2026-04-19) +# 更新:新增 dealCount 字段,从 Information 表统计用户发布的行情数量 + from typing import Optional from fastapi import APIRouter, Depends, HTTPException, status, Query, Body from sqlalchemy.orm import Session diff --git a/backend/app/routers/yichens.py b/backend/app/routers/yichens.py index a4b5ef4..d8335ed 100644 --- a/backend/app/routers/yichens.py +++ b/backend/app/routers/yichens.py @@ -1,377 +1,760 @@ +# yichens - 一尘数据路由 +# Version: 1.2.70 +# 更新: + from fastapi import APIRouter, Depends, Query +# 更新: +# Version: 1.2.x +# 更新: from sqlalchemy import func, text +# 更新: from sqlalchemy.orm import Session +# 更新: from pydantic import BaseModel +# 更新: from typing import Optional, List +# 更新: from datetime import datetime, date +# 更新: from app.core.coolbot_db import get_coolbot_db +# 更新: +# 更新: router = APIRouter(prefix="/api/yichens", tags=["一尘看板"]) +# 更新: +# 更新: # ============ 数据模型 ============ +# 更新: class YichensPostStats(BaseModel): +# 更新: total_posts: int +# 更新: total_deals: int # 出售 +# 更新: total_wants: int # 求购 +# 更新: total_replies: int +# 更新: total_views: int +# 更新: avg_price: Optional[float] +# 更新: +# 更新: class CategoryStat(BaseModel): +# 更新: category: str +# 更新: count: int +# 更新: +# 更新: class PostItem(BaseModel): +# 更新: post_id: str +# 更新: title: str +# 更新: category: Optional[str] +# 更新: post_type: str +# 更新: price: Optional[float] +# 更新: author_username: str +# 更新: post_time: str +# 更新: reply_count: int +# 更新: view_count: int +# 更新: url: Optional[str] +# 更新: content: Optional[str] +# 更新: +# 更新: class UserStat(BaseModel): +# 更新: total_users: int +# 更新: new_users_today: int +# 更新: sellers: int +# 更新: +# 更新: class UserItem(BaseModel): +# 更新: user_id: str +# 更新: username: str +# 更新: avatar_url: Optional[str] +# 更新: content: Optional[str] +# 更新: credit_level: Optional[str] +# 更新: credit_score: Optional[int] +# 更新: post_count: int +# 更新: is_seller: bool +# 更新: registration_date: Optional[str] +# 更新: +# 更新: # ============ 统计接口 ============ +# 更新: @router.get("/stats/posts", response_model=YichensPostStats) +# 更新: def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)): +# 更新: """获取帖子统计""" +# 更新: result = db.execute(text(""" +# 更新: SELECT +# 更新: COUNT(*) as total_posts, +# 更新: COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals, +# 更新: COUNT(*) FILTER (WHERE post_type = 'want') as total_wants, +# 更新: COALESCE(SUM(reply_count), 0) as total_replies, +# 更新: COALESCE(SUM(view_count), 0) as total_views, +# 更新: AVG(price) as avg_price +# 更新: FROM yichens_posts +# 更新: WHERE post_time >= NOW() - INTERVAL '1 day' * :days +# 更新: """), {"days": days}).fetchone() +# 更新: +# 更新: return YichensPostStats( +# 更新: total_posts=result[0] or 0, +# 更新: total_deals=result[1] or 0, +# 更新: total_wants=result[2] or 0, +# 更新: total_replies=result[3] or 0, +# 更新: total_views=result[4] or 0, +# 更新: avg_price=float(result[5]) if result[5] else None +# 更新: ) +# 更新: +# 更新: @router.get("/stats/categories", response_model=List[CategoryStat]) +# 更新: def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)): +# 更新: """按分类统计帖子数量""" +# 更新: results = db.execute(text(""" +# 更新: SELECT category, COUNT(*) as count +# 更新: FROM yichens_posts +# 更新: WHERE post_time >= NOW() - INTERVAL '1 day' * :days +# 更新: GROUP BY category +# 更新: ORDER BY count DESC +# 更新: """), {"days": days}).fetchall() +# 更新: +# 更新: return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results] +# 更新: +# 更新: @router.get("/stats/users", response_model=UserStat) +# 更新: def get_user_stats(db: Session = Depends(get_coolbot_db)): +# 更新: """获取用户统计""" +# 更新: result = db.execute(text(""" +# 更新: SELECT +# 更新: COUNT(*) as total_users, +# 更新: COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today, +# 更新: COUNT(*) FILTER (WHERE is_seller = true) as sellers +# 更新: FROM yichens_users +# 更新: """)).fetchone() +# 更新: +# 更新: return UserStat( +# 更新: total_users=result[0] or 0, +# 更新: new_users_today=result[1] or 0, +# 更新: sellers=result[2] or 0 +# 更新: ) +# 更新: +# 更新: @router.get("/posts") +# 更新: def get_posts( +# 更新: limit: int = Query(20, ge=1, le=500), +# 更新: offset: int = Query(0, ge=0), +# 更新: category: Optional[str] = None, +# 更新: post_type: Optional[str] = None, +# 更新: keyword: Optional[str] = None, +# 更新: db: Session = Depends(get_coolbot_db) +# 更新: ): +# 更新: """获取帖子列表 - 支持全局搜索,返回总数和分页信息""" +# 更新: # 构建WHERE条件 +# 更新: where_clauses = ["1=1"] +# 更新: params = {"limit": limit, "offset": offset} +# 更新: +# 更新: if category: +# 更新: where_clauses.append("category = :category") +# 更新: params["category"] = category +# 更新: +# 更新: if post_type: +# 更新: where_clauses.append("post_type = :post_type") +# 更新: params["post_type"] = post_type +# 更新: +# 更新: # 全局搜索 +# 更新: if keyword: +# 更新: where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)") +# 更新: params["keyword"] = f"%{keyword}%" +# 更新: +# 更新: where_sql = " AND ".join(where_clauses) +# 更新: +# 更新: # 查询总数 +# 更新: count_query = f"SELECT COUNT(*) FROM yichens_posts WHERE {where_sql}" +# 更新: total_count = db.execute(text(count_query), {k: v for k, v in params.items() if k not in ['limit', 'offset']}).scalar() or 0 +# 更新: +# 更新: # 查询数据 - 有post_time时按post_time排序,没有时按crawled_at排序 +# 更新: data_query = f""" +# 更新: SELECT post_id, title, content, category, post_type, price, +# 更新: author_username, post_time, reply_count, view_count, url +# 更新: FROM yichens_posts +# 更新: WHERE {where_sql} +# 更新: ORDER BY COALESCE(post_time, crawled_at) DESC LIMIT :limit OFFSET :offset +# 更新: """ +# 更新: results = db.execute(text(data_query), params).fetchall() +# 更新: +# 更新: posts = [PostItem( +# 更新: post_id=r[0], +# 更新: title=r[1] or "", +# 更新: content=r[2] or "", +# 更新: category=r[3], +# 更新: post_type=r[4] or "", +# 更新: price=float(r[5]) if r[5] else None, +# 更新: author_username=r[6] or "", +# 更新: post_time=str(r[7]) if r[7] else "", +# 更新: reply_count=r[8] or 0, +# 更新: view_count=r[9] or 0, +# 更新: url=r[10] +# 更新: ) for r in results] +# 更新: +# 更新: return { +# 更新: "posts": posts, +# 更新: "total": total_count, +# 更新: "page": offset // limit + 1, +# 更新: "page_size": limit +# 更新: } +# 更新: +# 更新: @router.get("/users", response_model=List[UserItem]) +# 更新: def get_users( +# 更新: limit: int = Query(20, ge=1, le=500), +# 更新: offset: int = Query(0, ge=0), +# 更新: is_seller: Optional[bool] = None, +# 更新: db: Session = Depends(get_coolbot_db) +# 更新: ): +# 更新: """获取用户列表""" +# 更新: query = """ +# 更新: SELECT user_id, username, avatar_url, credit_level, credit_score, +# 更新: post_count, is_seller, registration_date +# 更新: FROM yichens_users +# 更新: WHERE 1=1 +# 更新: """ +# 更新: params = {"limit": limit, "offset": offset} +# 更新: +# 更新: if is_seller is not None: +# 更新: query += " AND is_seller = :is_seller" +# 更新: params["is_seller"] = is_seller +# 更新: +# 更新: query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset" +# 更新: +# 更新: results = db.execute(text(query), params).fetchall() +# 更新: +# 更新: return [UserItem( +# 更新: user_id=r[0], +# 更新: username=r[1] or "", +# 更新: avatar_url=r[2], +# 更新: credit_level=r[3], +# 更新: credit_score=r[4], +# 更新: post_count=r[5] or 0, +# 更新: is_seller=r[6] or False, +# 更新: registration_date=str(r[7]) if r[7] else None +# 更新: ) for r in results] +# 更新: +# 更新: +# 更新: @router.get("/stats/today") +# 更新: async def get_today_stats(db: Session = Depends(get_coolbot_db)): +# 更新: """获取今日新增帖子统计""" +# 更新: query = """ +# 更新: SELECT +# 更新: COUNT(*) as total, +# 更新: SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, +# 更新: SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants, +# 更新: SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others, +# 更新: SUM(CASE WHEN category LIKE '%龙%' THEN 1 ELSE 0 END) as dragons, +# 更新: SUM(CASE WHEN category LIKE '%马%' THEN 1 ELSE 0 END) as horses, +# 更新: SUM(CASE WHEN category LIKE '%蛇%' THEN 1 ELSE 0 END) as snakes, +# 更新: SUM(CASE WHEN title LIKE '%天马%' OR content LIKE '%天马%' THEN 1 ELSE 0 END) as tianma +# 更新: FROM yichens_posts +# 更新: WHERE post_time >= CURRENT_DATE +# 更新: """ +# 更新: result = db.execute(text(query)).fetchone() +# 更新: return { +# 更新: "total": result[0] or 0, +# 更新: "deals": result[1] or 0, +# 更新: "wants": result[2] or 0, +# 更新: "others": result[3] or 0, +# 更新: "dragons": result[4] or 0, +# 更新: "horses": result[5] or 0, +# 更新: "snakes": result[6] or 0, +# 更新: "tianma": result[7] or 0 +# 更新: } +# 更新: +# 更新: @router.get("/stats/hour") +# 更新: async def get_hour_stats(db: Session = Depends(get_coolbot_db)): +# 更新: """获取近一个小时新增帖子统计""" +# 更新: query = """ +# 更新: SELECT +# 更新: COUNT(*) as total, +# 更新: SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, +# 更新: SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants, +# 更新: SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others, +# 更新: SUM(CASE WHEN category LIKE '%龙%' THEN 1 ELSE 0 END) as dragons, +# 更新: SUM(CASE WHEN category LIKE '%马%' THEN 1 ELSE 0 END) as horses, +# 更新: SUM(CASE WHEN category LIKE '%蛇%' THEN 1 ELSE 0 END) as snakes +# 更新: FROM yichens_posts +# 更新: WHERE post_time >= NOW() - INTERVAL '1 hour' +# 更新: """ +# 更新: result = db.execute(text(query)).fetchone() +# 更新: return { +# 更新: "total": result[0] or 0, +# 更新: "deals": result[1] or 0, +# 更新: "wants": result[2] or 0, +# 更新: "others": result[3] or 0, +# 更新: "dragons": result[3] or 0, +# 更新: "horses": result[4] or 0, +# 更新: "snakes": result[5] or 0 +# 更新: } +# 更新: +# 更新: @router.get("/stats/today-category") +# 更新: async def get_today_category_stats(db: Session = Depends(get_coolbot_db)): +# 更新: """获取今日帖子分类统计""" +# 更新: query = """ +# 更新: SELECT category, COUNT(*) as count +# 更新: FROM yichens_posts +# 更新: WHERE post_time >= CURRENT_DATE +# 更新: GROUP BY category +# 更新: ORDER BY count DESC +# 更新: """ +# 更新: results = db.execute(text(query)).fetchall() +# 更新: return [{"category": r[0] or "未分类", "count": r[1]} for r in results] +# 更新: +# 更新: +# 更新: @router.get("/stats/dragons-today") +# 更新: def get_dragons_stats_today( +# 更新: db: Session = Depends(get_coolbot_db) +# 更新: ): +# 更新: """获取今日龙钞详细统计数据(按号码分类)- 就高不就低""" +# 更新: from sqlalchemy import text +# 更新: +# 更新: # 1. 带4:包含"带4"、"带四"、"通货" +# 更新: dai4 = db.execute(text(""" +# 更新: SELECT +# 更新: COUNT(*) as total, +# 更新: SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, +# 更新: SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants +# 更新: FROM yichens_posts +# 更新: WHERE post_time >= CURRENT_DATE +# 更新: AND category LIKE '%龙%' +# 更新: AND ( +# 更新: content LIKE '%带4%' OR title LIKE '%带4%' +# 更新: OR content LIKE '%带四%' OR title LIKE '%带四%' +# 更新: OR content LIKE '%通货%' OR title LIKE '%通货%' +# 更新: ) +# 更新: """)).fetchone() +# 更新: +# 更新: # 2. 无4:包含"无4"、"无四",排除"无47"、"无247"、"无四七"、"永恒" +# 更新: wu4 = db.execute(text(""" +# 更新: SELECT +# 更新: COUNT(*) as total, +# 更新: SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, +# 更新: SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants +# 更新: FROM yichens_posts +# 更新: WHERE post_time >= CURRENT_DATE +# 更新: AND category LIKE '%龙%' +# 更新: AND ( +# 更新: content LIKE '%无4%' OR title LIKE '%无4%' +# 更新: OR content LIKE '%无四%' OR title LIKE '%无四%' +# 更新: ) +# 更新: AND content NOT LIKE '%无47%' AND title NOT LIKE '%无47%' +# 更新: AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%' +# 更新: AND content NOT LIKE '%永恒%' AND title NOT LIKE '%永恒%' +# 更新: AND content NOT LIKE '%无四七%' AND title NOT LIKE '%无四七%' +# 更新: """)).fetchone() +# 更新: +# 更新: # 3. 无47:包含"无47"、"永恒"、"无四七",排除"无247" +# 更新: wu47 = db.execute(text(""" +# 更新: SELECT +# 更新: COUNT(*) as total, +# 更新: SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, +# 更新: SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants +# 更新: FROM yichens_posts +# 更新: WHERE post_time >= CURRENT_DATE +# 更新: AND category LIKE '%龙%' +# 更新: AND ( +# 更新: content LIKE '%无47%' OR title LIKE '%无47%' +# 更新: OR content LIKE '%永恒%' OR title LIKE '%永恒%' +# 更新: OR content LIKE '%无四七%' OR title LIKE '%无四七%' +# 更新: ) +# 更新: AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%' +# 更新: """)).fetchone() +# 更新: +# 更新: # 4. 无247:包含"无247"、"天马"、"金山",排除"无347" +# 更新: wu247 = db.execute(text(""" +# 更新: SELECT +# 更新: COUNT(*) as total, +# 更新: SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, +# 更新: SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants +# 更新: FROM yichens_posts +# 更新: WHERE post_time >= CURRENT_DATE +# 更新: AND category LIKE '%龙%' +# 更新: AND ( +# 更新: content LIKE '%无247%' OR title LIKE '%无247%' +# 更新: OR content LIKE '%天马%' OR title LIKE '%天马%' +# 更新: OR content LIKE '%金山%' OR title LIKE '%金山%' +# 更新: ) +# 更新: AND content NOT LIKE '%无347%' AND title NOT LIKE '%无347%' +# 更新: """)).fetchone() +# 更新: +# 更新: # 5. 无347:包含"无347"、"钻石"、"金马"、"魅力"、"朦胧" +# 更新: wu347 = db.execute(text(""" +# 更新: SELECT +# 更新: COUNT(*) as total, +# 更新: SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, +# 更新: SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants +# 更新: FROM yichens_posts +# 更新: WHERE post_time >= CURRENT_DATE +# 更新: AND category LIKE '%龙%' +# 更新: AND ( +# 更新: content LIKE '%无347%' OR title LIKE '%无347%' +# 更新: OR content LIKE '%钻石%' OR title LIKE '%钻石%' +# 更新: OR content LIKE '%金马%' OR title LIKE '%金马%' +# 更新: OR content LIKE '%魅力%' OR title LIKE '%魅力%' +# 更新: OR content LIKE '%朦胧%' OR title LIKE '%朦胧%' +# 更新: ) +# 更新: """)).fetchone() +# 更新: +# 更新: return { +# 更新: "dai4": {"total": dai4[0] or 0, "deals": dai4[1] or 0, "wants": dai4[2] or 0}, +# 更新: "wu4": {"total": wu4[0] or 0, "deals": wu4[1] or 0, "wants": wu4[2] or 0}, +# 更新: "wu47": {"total": wu47[0] or 0, "deals": wu47[1] or 0, "wants": wu47[2] or 0}, +# 更新: "wu247": {"total": wu247[0] or 0, "deals": wu247[1] or 0, "wants": wu247[2] or 0}, +# 更新: "wu347": {"total": wu347[0] or 0, "deals": wu347[1] or 0, "wants": wu347[2] or 0} +# 更新: } +# 更新: +# 更新: diff --git a/config/CHANGELOG.md b/config/CHANGELOG.md new file mode 100644 index 0000000..df80eb9 --- /dev/null +++ b/config/CHANGELOG.md @@ -0,0 +1,35 @@ +# 版本更新记录 + +## v1.2.98 (2026-04-19) + +### 前端更新 +- **Admin.jsx** + - 功能:修复管理员用户列表行情数显示问题 + - 改进:新增 dealCount 字段显示用户发布的成交行情数量 + - 代码:将"配号"改为"📈 行情: X条" + +### 后端更新 +- **users.py** + - 功能:修复管理员用户列表行情数API + - 改进:新增 dealCount 字段,从 Information 表统计用户发布的 deal 数量 + - 代码:新增 Information 模型导入,统计查询 + +### Vite配置更新 +- **vite.config.js** + - 功能:修复前端构建版本号不更新问题 + - 改进:将版本更新改为 closeBundle hook 执行 + - 代码:重构 updateHtmlTitle 函数 + +--- + +## v1.2.97 (2026-04-XX) + +### 更新内容 +- (待记录) + +--- + +## v1.2.96 (2026-04-XX) + +### 更新内容 +- (待记录) \ No newline at end of file diff --git a/config/VERSION.json b/config/VERSION.json new file mode 100644 index 0000000..d374dc3 --- /dev/null +++ b/config/VERSION.json @@ -0,0 +1,48 @@ +{ + "version": "1.2.98", + "updated": "2026-04-19", + "modules": { + "frontend": { + "version": "1.2.98", + "pages": { + "Add": "1.2.90", + "Admin": "1.2.98", + "Detail": "1.2.90", + "Edit": "1.2.90", + "Home": "1.2.95", + "List": "1.2.93", + "Login": "1.2.85", + "News": "1.2.80", + "News_YichensBoard": "1.2.75", + "Settings": "1.2.75", + "Stats": "1.2.70", + "YichensBoard": "1.2.70" + }, + "config": { + "version": "1.2.98" + } + }, + "backend": { + "version": "1.2.98", + "routers": { + "auth": "1.2.90", + "collections": "1.2.95", + "deal": "1.2.85", + "information": "1.2.92", + "news": "1.2.80", + "ocr": "1.2.75", + "operations": "1.2.70", + "seek": "1.2.70", + "users": "1.2.98", + "yichens": "1.2.70" + }, + "app": { + "core": "1.2.0", + "models": "1.2.0", + "schemas": "1.2.0", + "services": "1.2.0", + "utils": "1.2.0" + } + } + } +} \ No newline at end of file diff --git a/frontend/src/pages/Add.jsx b/frontend/src/pages/Add.jsx index 09a9208..e8803d2 100644 --- a/frontend/src/pages/Add.jsx +++ b/frontend/src/pages/Add.jsx @@ -1,4 +1,9 @@ -// 添加藏品页面 - 支持 AI 识别/手工录入 +/** + * Add - 添加藏品页面 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useRef } from 'react' import { APP_VERSION } from '../config/version' const API_BASE = localStorage.getItem('API_BASE') || '' diff --git a/frontend/src/pages/Admin.jsx b/frontend/src/pages/Admin.jsx index 92683d9..7735695 100644 --- a/frontend/src/pages/Admin.jsx +++ b/frontend/src/pages/Admin.jsx @@ -1,3 +1,9 @@ +/** + * Admin - 管理员用户管理页面 + * Version: 1.2.98 (2026-04-19) + * 更新:新增 dealCount 字段显示用户发布的行情数量 + */ + import React, { useState, useEffect } from 'react' import { APP_VERSION } from '../config/version' diff --git a/frontend/src/pages/Detail.jsx b/frontend/src/pages/Detail.jsx index 7df0297..8b206bf 100644 --- a/frontend/src/pages/Detail.jsx +++ b/frontend/src/pages/Detail.jsx @@ -1,3 +1,9 @@ +/** + * Detail - 藏品详情页面 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useEffect } from 'react' export default function Detail() { diff --git a/frontend/src/pages/Edit.jsx b/frontend/src/pages/Edit.jsx index 118fc6e..0f10252 100644 --- a/frontend/src/pages/Edit.jsx +++ b/frontend/src/pages/Edit.jsx @@ -1,3 +1,9 @@ +/** + * Edit - 编辑藏品页面 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useRef, useEffect } from 'react' // Input 组件(复用 Add.jsx 的定义) diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index f6912a9..1d66733 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -1,3 +1,9 @@ +/** + * Home - 首页 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useEffect } from 'react' import { APP_VERSION } from '../config/version' diff --git a/frontend/src/pages/List.jsx b/frontend/src/pages/List.jsx index b8b5ff8..2743c48 100644 --- a/frontend/src/pages/List.jsx +++ b/frontend/src/pages/List.jsx @@ -1,3 +1,9 @@ +/** + * List - 藏品列表页面 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useEffect } from 'react' import { APP_VERSION } from '../config/version' const API_BASE = localStorage.getItem('API_BASE') || '' diff --git a/frontend/src/pages/Login.jsx b/frontend/src/pages/Login.jsx index 8c8dfc1..f4fde17 100644 --- a/frontend/src/pages/Login.jsx +++ b/frontend/src/pages/Login.jsx @@ -1,3 +1,9 @@ +/** + * Login - 登录页面 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useEffect } from 'react' import { APP_VERSION } from '../config/version' import { api } from '../utils/api' diff --git a/frontend/src/pages/News.jsx b/frontend/src/pages/News.jsx index c77c20f..f5a2c40 100644 --- a/frontend/src/pages/News.jsx +++ b/frontend/src/pages/News.jsx @@ -1,3 +1,9 @@ +/** + * News - 资讯列表页面 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useEffect } from 'react' import YichensBoard from './YichensBoard' diff --git a/frontend/src/pages/News_YichensBoard.jsx b/frontend/src/pages/News_YichensBoard.jsx index 7af8ba1..0320933 100644 --- a/frontend/src/pages/News_YichensBoard.jsx +++ b/frontend/src/pages/News_YichensBoard.jsx @@ -1,3 +1,9 @@ +/** + * News_YichensBoard - 一尘帖子页面 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useEffect } from 'react' export default function YichensBoard() { diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index 7857338..554390c 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -1,3 +1,9 @@ +/** + * Settings - 设置页面 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useEffect } from 'react' import { useNavigate } from 'react-router-dom' diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx index 666341d..f100a8a 100644 --- a/frontend/src/pages/Stats.jsx +++ b/frontend/src/pages/Stats.jsx @@ -1,4 +1,9 @@ -// 统计分析页面 - 支持点击跳转 +/** + * Stats - 统计页面 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useEffect } from 'react' import { APP_VERSION } from '../config/version' diff --git a/frontend/src/pages/YichensBoard.jsx b/frontend/src/pages/YichensBoard.jsx index 85d80e0..d4a7d42 100644 --- a/frontend/src/pages/YichensBoard.jsx +++ b/frontend/src/pages/YichensBoard.jsx @@ -1,3 +1,9 @@ +/** + * YichensBoard - 一尘看板页面 + * Version: 1.2.x + * 更新: + */ + import React, { useState, useEffect } from 'react' export default function YichensBoard() { diff --git a/scripts/check_version.sh b/scripts/check_version.sh new file mode 100755 index 0000000..ef0794f --- /dev/null +++ b/scripts/check_version.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# 版本一致性检查脚本 +# 用法: ./scripts/check_version.sh + +echo "=========================================" +echo " 版本一致性检查" +echo "=========================================" + +CONFIG_FILE="/root/.openclaw/workspace/jiachenlong/config/VERSION.json" +VERSION=$(grep -o '"version": "[^"]*"' $CONFIG_FILE | cut -d'"' -f4) +UPDATED=$(grep -o '"updated": "[^"]*"' $CONFIG_FILE | cut -d'"' -f4) + +echo "" +echo "当前版本: $VERSION" +echo "更新时间: $UPDATED" +echo "" + +# 检查前端页面版本 +echo "--- 前端页面 (最后修改时间) ---" +for file in /root/.openclaw/workspace/jiachenlong/frontend/src/pages/*.jsx; do + name=$(basename $file .jsx) + mtime=$(date -r "$file" "+%Y-%m-%d %H:%M") + echo "$name: $mtime" +done + +echo "" +echo "--- 后端路由 (最后修改时间) ---" +for file in /root/.openclaw/workspace/jiachenlong/backend/app/routers/*.py; do + name=$(basename $file .py) + if [ "$name" != "__init__" ]; then + mtime=$(date -r "$file" "+%Y-%m-%d %H:%M") + echo "$name: $mtime" + fi +done + +echo "" +echo "=========================================" +echo " 检查完成" +echo "=========================================" \ No newline at end of file