jiachenlong/backend/app/routers/seek.py

497 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# seek - 寻配号路由
# Version: 0.0.9 (2026-04-24)
# 稳定版:自有匹配+网络匹配缓存
from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional, List
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.routers.information import match_collections_count, match_collections_count_from_coolbot, match_pattern
from app.utils.coolbot_matcher import (
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 '匿名用户'
# 构建响应(不实时计算网络匹配,网络匹配在点击时再查)
# 网络匹配数量从数据库读取不再实时计算初始为0
network_matched_count = 0
if hasattr(item, 'network_matched_count') and item.network_matched_count:
network_matched_count = item.network_matched_count
# 自有匹配数量使用缓存方式如为0则计算一次
matched_count = 0
if item.expect_number and len(item.expect_number) == 10:
if current_user and current_user.f99_90_id:
matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number)
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=matched_count,
network_matched_count=network_matched_count
))
return result
@router.get("/stats")
def get_seek_stats(
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="请先登录")
# 计算网络匹配数量(创建时初始化)
network_matched_count = 0
if data.expect_number and len(data.expect_number) == 10:
network_matched_count = match_collections_count_from_coolbot(data.expect_number)
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",
view_count=0,
contact_count=0,
network_matched_count=network_matched_count # 保存初始化值
)
db.add(seek)
db.commit()
db.refresh(seek)
return seek
# ============ 匹配相关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(100, ge=1, le=500),
db: Session = Depends(get_db)
):
"""获取一尘数据库中匹配的藏品列表点击时更新network_matched_count"""
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)
actual_count = len(matched)
# 点击查看时更新network_matched_count
info.network_matched_count = actual_count
db.commit()
return {"matched_count": actual_count, "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,
}
# ============ 通配路由(必须放最后,避免匹配冲突)============
@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).first()
if not seek:
raise HTTPException(status_code=404, detail="寻配号不存在")
if seek.user_id != current_user.f99_90_id:
raise HTTPException(status_code=403, detail="无权限")
if data.title is not None:
seek.title = data.title
if data.content is not None:
seek.content = data.content
if data.expect_category is not None:
seek.expect_category = data.expect_category
if data.expect_version is not None:
seek.expect_version = data.expect_version
if data.expect_packaging is not None:
seek.expect_packaging = data.expect_packaging
if data.status is not None:
seek.status = data.status
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).first()
if not seek:
raise HTTPException(status_code=404, detail="寻配号不存在")
if seek.user_id != current_user.f99_90_id:
raise HTTPException(status_code=403, detail="无权限")
seek.status = "deleted"
db.commit()
return {"message": "删除成功"}
# ============ 留言功能从information.py迁移============
from pydantic import BaseModel
class CommentRequest(BaseModel):
information_id: str
content: str
@router.post("/comment")
def add_comment(
request: CommentRequest,
current_user = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""添加留言 - 支持seek_info表"""
if not current_user:
raise HTTPException(status_code=401, detail="请先登录")
# 查询seek_info表
info = db.query(SeekInfo).filter(SeekInfo.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
]