409 lines
13 KiB
Python
409 lines
13 KiB
Python
# 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,
|
||
}
|