修复寻配号三个问题:1.限制冠字号最多8位 2.显示真实用户名 3.添加匹配数量计算

This commit is contained in:
甲辰生产 2026-04-24 10:20:42 +08:00
parent d47a002495
commit 7d0f92b1eb
5 changed files with 135 additions and 1682 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,384 +1,301 @@
# seek - 寻号匹配路由
# Version: 0.0.1
# 更新:
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
# 更新:
# Version: 1.2.x
# 更新:
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
# 更新:
from pydantic import BaseModel from pydantic import BaseModel
# 更新:
from typing import Optional from typing import Optional
# 更新:
from datetime import datetime from datetime import datetime
# 更新:
from app.core.database import get_db from app.core.database import get_db
# 更新:
from app.core.auth import get_current_user from app.core.auth import get_current_user
# 更新:
from app.models.seek_info import SeekInfo from app.models.seek_info import SeekInfo
# 更新: from app.models.models import User, Collection, Information
from sqlalchemy import text
# 更新:
router = APIRouter(prefix="/api/seek", tags=["寻配号"]) router = APIRouter(prefix="/api/seek", tags=["寻配号"])
# 更新:
# 更新:
# ============ Schema ============ # ============ Schema ============
# 更新:
class SeekInfoCreate(BaseModel): class SeekInfoCreate(BaseModel):
# 更新:
title: str title: str
# 更新:
content: Optional[str] = None content: Optional[str] = None
# 更新:
expect_category: Optional[str] = None expect_category: Optional[str] = None
# 更新:
expect_version: Optional[str] = None expect_version: Optional[str] = None
# 更新:
expect_packaging: Optional[str] = None expect_packaging: Optional[str] = None
# 更新:
expect_number: Optional[str] = None expect_number: Optional[str] = None
# 更新:
expect_price_min: Optional[float] = None expect_price_min: Optional[float] = None
# 更新:
expect_price_max: Optional[float] = None expect_price_max: Optional[float] = None
# 更新:
# 更新:
class SeekInfoUpdate(BaseModel): class SeekInfoUpdate(BaseModel):
# 更新:
title: Optional[str] = None title: Optional[str] = None
# 更新:
content: Optional[str] = None content: Optional[str] = None
# 更新:
expect_category: Optional[str] = None expect_category: Optional[str] = None
# 更新:
expect_version: Optional[str] = None expect_version: Optional[str] = None
# 更新:
expect_packaging: Optional[str] = None expect_packaging: Optional[str] = None
# 更新:
expect_number: Optional[str] = None expect_number: Optional[str] = None
# 更新:
expect_price_min: Optional[float] = None expect_price_min: Optional[float] = None
# 更新:
expect_price_max: Optional[float] = None expect_price_max: Optional[float] = None
# 更新:
status: Optional[str] = None status: Optional[str] = None
# 更新:
# 更新:
class SeekInfoResponse(BaseModel): class SeekInfoResponse(BaseModel):
# 更新:
id: str id: str
# 更新:
user_id: str user_id: str
# 更新:
title: str title: str
# 更新:
content: Optional[str] content: Optional[str]
# 更新:
expect_category: Optional[str] expect_category: Optional[str]
# 更新:
expect_version: Optional[str] expect_version: Optional[str]
# 更新:
expect_packaging: Optional[str] expect_packaging: Optional[str]
# 更新:
expect_number: Optional[str] expect_number: Optional[str]
# 更新:
expect_price_min: Optional[float] expect_price_min: Optional[float]
# 更新:
expect_price_max: Optional[float] expect_price_max: Optional[float]
# 更新:
status: str status: str
# 更新:
is_matched: Optional[str] is_matched: Optional[str]
# 更新:
matched_user_id: Optional[str] matched_user_id: Optional[str]
# 更新:
matched_contact: Optional[str] matched_contact: Optional[str]
# 更新:
view_count: int view_count: int
# 更新:
contact_count: int contact_count: int
# 更新:
created_at: Optional[datetime] created_at: Optional[datetime]
# 更新:
updated_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: class Config:
# 更新:
from_attributes = True from_attributes = True
# 更新:
# 更新:
# ============ API ============ # ============ API ============
# 更新:
@router.get("/list", response_model=list[SeekInfoResponse]) @router.get("/list", response_model=list[SeekInfoResponse])
# 更新:
def get_seek_list( def get_seek_list(
# 更新:
status: str = Query("active"), status: str = Query("active"),
# 更新:
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
# 更新:
page_size: int = Query(20, ge=1, le=1000), page_size: int = Query(20, ge=1, le=1000),
# 更新:
user_only: bool = Query(False), user_only: bool = Query(False),
# 更新:
current_user: Optional = Depends(get_current_user), current_user: Optional = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号列表""" """获取寻配号列表"""
# 更新:
query = db.query(SeekInfo).filter(SeekInfo.status == status) query = db.query(SeekInfo).filter(SeekInfo.status == status)
# 更新:
# 更新:
# 我的寻配号:只查看自己的 # 我的寻配号:只查看自己的
# 更新:
if user_only and current_user: if user_only and current_user:
# 更新:
query = query.filter(SeekInfo.user_id == current_user.f99_90_id) query = query.filter(SeekInfo.user_id == current_user.f99_90_id)
# 更新:
# 更新:
# 排序 # 排序
# 更新:
query = query.order_by(SeekInfo.created_at.desc()) query = query.order_by(SeekInfo.created_at.desc())
# 更新:
# 更新:
# 分页 # 分页
# 更新:
offset = (page - 1) * page_size offset = (page - 1) * page_size
# 更新:
items = query.offset(offset).limit(page_size).all() items = query.offset(offset).limit(page_size).all()
# 更新:
# 更新: # 添加用户名和匹配数量
return items from app.routers.information import match_collections_count, match_collections_count_from_coolbot
# 更新: 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 '匿名用户'
# 计算匹配数量
matched_count = 0
network_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)
network_matched_count = match_collections_count_from_coolbot(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") @router.get("/stats")
# 更新:
def get_seek_stats( def get_seek_stats(
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号统计""" """获取寻配号统计"""
# 更新:
total = db.query(SeekInfo).filter(SeekInfo.status == "active").count() total = db.query(SeekInfo).filter(SeekInfo.status == "active").count()
# 更新:
matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count() matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count()
# 更新:
# 更新:
return { return {
# 更新:
"total": total, "total": total,
# 更新:
"matched": matched, "matched": matched,
# 更新:
"unmatched": total - matched "unmatched": total - matched
# 更新:
} }
# 更新:
# 更新:
@router.post("", response_model=SeekInfoResponse) @router.post("", response_model=SeekInfoResponse)
# 更新:
def create_seek( def create_seek(
# 更新:
data: SeekInfoCreate, data: SeekInfoCreate,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""创建寻配号""" """创建寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
seek = SeekInfo( seek = SeekInfo(
# 更新:
user_id=current_user.f99_90_id, user_id=current_user.f99_90_id,
# 更新:
title=data.title, title=data.title,
# 更新:
content=data.content, content=data.content,
# 更新:
expect_category=data.expect_category, expect_category=data.expect_category,
# 更新:
expect_version=data.expect_version, expect_version=data.expect_version,
# 更新:
expect_packaging=data.expect_packaging, expect_packaging=data.expect_packaging,
# 更新:
expect_number=data.expect_number, expect_number=data.expect_number,
# 更新:
expect_price_min=data.expect_price_min, expect_price_min=data.expect_price_min,
# 更新:
expect_price_max=data.expect_price_max, expect_price_max=data.expect_price_max,
# 更新:
status="active" status="active"
# 更新:
) )
# 更新:
db.add(seek) db.add(seek)
# 更新:
db.commit() db.commit()
# 更新:
db.refresh(seek) db.refresh(seek)
# 更新:
return seek return seek
# 更新:
# 更新:
@router.get("/{seek_id}", response_model=SeekInfoResponse) @router.get("/{seek_id}", response_model=SeekInfoResponse)
# 更新:
def get_seek( def get_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号详情""" """获取寻配号详情"""
# 更新:
seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first() seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first()
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
# 更新:
# 更新:
# 增加浏览数 # 增加浏览数
# 更新:
seek.view_count += 1 seek.view_count += 1
# 更新:
db.commit() db.commit()
# 更新:
# 更新:
return seek return seek
# 更新:
# 更新:
@router.put("/{seek_id}", response_model=SeekInfoResponse) @router.put("/{seek_id}", response_model=SeekInfoResponse)
# 更新:
def update_seek( def update_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
data: SeekInfoUpdate, data: SeekInfoUpdate,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""更新寻配号""" """更新寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
seek = db.query(SeekInfo).filter( seek = db.query(SeekInfo).filter(
# 更新:
SeekInfo.id == seek_id, SeekInfo.id == seek_id,
# 更新:
SeekInfo.user_id == current_user.f99_90_id SeekInfo.user_id == current_user.f99_90_id
# 更新:
).first() ).first()
# 更新:
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
# 更新:
# 更新:
for key, value in data.model_dump(exclude_unset=True).items(): for key, value in data.model_dump(exclude_unset=True).items():
# 更新:
setattr(seek, key, value) setattr(seek, key, value)
# 更新:
# 更新:
db.commit() db.commit()
# 更新:
db.refresh(seek) db.refresh(seek)
# 更新:
return seek return seek
# 更新:
# 更新:
@router.delete("/{seek_id}") @router.delete("/{seek_id}")
# 更新:
def delete_seek( def delete_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""删除寻配号""" """删除寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
seek = db.query(SeekInfo).filter( seek = db.query(SeekInfo).filter(
# 更新:
SeekInfo.id == seek_id, SeekInfo.id == seek_id,
# 更新:
SeekInfo.user_id == current_user.f99_90_id SeekInfo.user_id == current_user.f99_90_id
# 更新:
).first() ).first()
# 更新:
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
# 更新:
# 更新:
seek.status = "deleted" seek.status = "deleted"
# 更新:
db.commit() db.commit()
# 更新:
# 更新:
return {"message": "删除成功"} return {"message": "删除成功"}
# 更新: # 获取自有藏品匹配列表
@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()
# 按号码特征模式匹配
from app.routers.information import match_pattern
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)
):
"""获取一尘数据库中匹配的藏品列表"""
from app.routers.information import match_collections_list_from_coolbot
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}

View File

@ -1 +1 @@
VERSION=v0.0.3 VERSION=v0.0.5

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<title>甲辰收藏 v=0.0.3</title> <title>甲辰收藏 v=0.0.5</title>
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="/static/icons/favicon.png" /> <link rel="icon" type="image/png" href="/static/icons/favicon.png" />

View File

@ -71,9 +71,10 @@ export default function News() {
fetchInfoList() fetchInfoList()
}, [activeTab, dealDate]) }, [activeTab, dealDate])
// // 8
useEffect(() => { useEffect(() => {
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${seekForm.features || 'XXXXXXXX'}` const features = (seekForm.features || '').slice(0, 8).padEnd(8, 'X')
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${features}`
setSeekForm(prev => ({...prev, title})) setSeekForm(prev => ({...prev, title}))
}, [seekForm.edition, seekForm.features]) }, [seekForm.edition, seekForm.features])
@ -274,7 +275,7 @@ export default function News() {
// //
const fetchNetworkMatchCollections = async (infoId) => { const fetchNetworkMatchCollections = async (infoId) => {
try { try {
const res = await fetch(`${API_BASE}/api/information/seek/network-match/${infoId}`) const res = await fetch(`${API_BASE}/api/seek/network-match/${infoId}`)
const data = await res.json() const data = await res.json()
console.log('网络数据匹配结果:', data) console.log('网络数据匹配结果:', data)
setNetworkMatchCollections(data.collections || []) setNetworkMatchCollections(data.collections || [])
@ -451,12 +452,22 @@ export default function News() {
onChange={(e) => { onChange={(e) => {
if (i < 2) return if (i < 2) return
const val = e.target.value.toUpperCase().replace(/[^0-9XABCDEFG]/g, '') const val = e.target.value.toUpperCase().replace(/[^0-9XABCDEFG]/g, '')
const newFeatures = (seekForm.features || '').split('') if (!val) {
//
const newFeatures = (seekForm.features || '').split('')
newFeatures[i - 2] = ''
setSeekForm({...seekForm, features: newFeatures.join('').slice(0, 8)})
return
}
// 8
let currentLen = (seekForm.features || '').length
if (currentLen >= 8 && i - 2 >= currentLen) return // 8
const newFeatures = (seekForm.features || '').slice(0, 8).split('')
while (newFeatures.length < 8) newFeatures.push('') while (newFeatures.length < 8) newFeatures.push('')
newFeatures[i - 2] = val newFeatures[i - 2] = val
setSeekForm({...seekForm, features: newFeatures.join('')}) setSeekForm({...seekForm, features: newFeatures.join('').slice(0, 8)})
// //
if (val && i < 9) { if (i < 9) {
setTimeout(() => { setTimeout(() => {
const nextInput = document.querySelector(`input[data-index="${i+1}"]`) const nextInput = document.querySelector(`input[data-index="${i+1}"]`)
if (nextInput) nextInput.focus() if (nextInput) nextInput.focus()
@ -752,7 +763,7 @@ export default function News() {
</div> </div>
{/* 创建日期 + 用户名 */} {/* 创建日期 + 用户名 */}
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}> <div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}>
📅 {formatDate(item.created_at)} &nbsp;|&nbsp; 👤 {(item.content && item.content.match(/发布者: (.+)/)?.[1]) || item.user_name || '匿名用户'} 📅 {formatDate(item.created_at)} &nbsp;|&nbsp; 👤 {item.user_name || '匿名用户'}
</div> </div>
{/* 号码特征 */} {/* 号码特征 */}
{features && ( {features && (