# seek - 寻配号路由 # Version: 0.0.4 (2026-04-24) # 更新:移除列表网络匹配实时计算,添加coolbot_matcher导入 # 更新:添加缺失端点,清理废弃依赖 from fastapi import APIRouter, Depends, Query, HTTPException 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 from app.models.models import User, Collection from app.utils.coolbot_matcher import ( match_pattern, match_self_collections_count, match_collections_count_from_coolbot, match_collections_list_from_coolbot ) 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] user_name: Optional[str] = None # 发布者用户名 matched_count: Optional[int] = 0 # 自有匹配数量 network_matched_count: Optional[int] = 0 # 网络匹配数量 class Config: from_attributes = True class MatchConfirmRequest(BaseModel): info_id: str contact: Optional[str] = None collection_id: Optional[str] = None # ============ 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() result = [] for item in items: user = db.query(User).filter(User.f99_90_id == item.user_id).first() user_name = user.f01_01_name if user else '匿名用户' # 构建响应(不实时计算网络匹配,网络匹配在点击时再查) result.append(SeekInfoResponse( id=item.id, user_id=item.user_id, title=item.title, content=item.content, 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, 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, updated_at=item.updated_at, user_name=user_name, matched_count=0, network_matched_count=0 )) return result @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": "删除成功"} # ============ 匹配相关API ============ @router.get("/my-match") def get_seek_match( info_id: str, current_user = Depends(get_current_user), db: Session = Depends(get_db) ): """获取符合条件的我的藏品推荐""" if not current_user: raise HTTPException(status_code=401, detail="请先登录") info = db.query(SeekInfo).filter(SeekInfo.id == info_id).first() if not info: raise HTTPException(status_code=404, detail="寻配号信息不存在") # 获取用户所有藏品 collections = db.query(Collection).filter( Collection.f99_91_user_id == current_user.f99_90_id, Collection.f01_04_status == "in_collection" ).all() # 按号码特征模式匹配 matched = [] if info.expect_number and len(info.expect_number) == 10: pattern = info.expect_number[2:] for c in collections: number = c.f02_10_prefix_serial or '' if len(number) >= 10 and number.startswith('J0'): col_pattern = number[2:10] if match_pattern(col_pattern, pattern): matched.append(c) elif len(number) >= 8: col_pattern = number[:8] if match_pattern(col_pattern, pattern): matched.append(c) else: matched = collections return { "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} for c in matched[:20] ] } @router.get("/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(SeekInfo).filter(SeekInfo.id == info_id).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.post("/match-confirm") def match_seek_confirm( request: MatchConfirmRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """确认匹配寻号 - 用户愿意交换联系方式给发布者""" info = db.query(SeekInfo).filter( SeekInfo.id == request.info_id, SeekInfo.status == "active" ).first() if not info: raise HTTPException(status_code=404, detail="寻配号信息不存在") # 检查是否已被匹配(一个寻号只能被一个用户匹配) if info.is_matched == "matched": raise HTTPException(status_code=400, detail="该寻号已被其他用户匹配") # 检查是否是自己发布的 if info.user_id == current_user.f99_90_id: 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 "" 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"} @router.get("/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(SeekInfo).filter(SeekInfo.id == info_id).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, } @router.get("/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(SeekInfo).filter(SeekInfo.id == info_id).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, }