清理废弃代码:(1)创建coolbot_matcher.py工具模块 (2)更新seek.py添加缺失端点并导入新工具 (3)更新News.jsx迁移API到seek.py

Changes:
- backend/app/utils/coolbot_matcher.py: 新建一尘数据库匹配工具模块
- backend/app/routers/seek.py: 更新导入,添加match-confirm/matched-user/publisher端点
- frontend/src/pages/News.jsx: API调用从information迁移到seek

Note: Skill文档已单独更新到47.96.181.36
This commit is contained in:
socool 2026-04-24 10:57:52 +08:00
parent ae87b10fe0
commit e6a0b4fb34
3 changed files with 341 additions and 20 deletions

View File

@ -1,3 +1,7 @@
# seek - 寻配号路由
# Version: 0.0.3 (2026-04-24)
# 更新:添加缺失端点,清理废弃依赖
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from pydantic import BaseModel from pydantic import BaseModel
@ -6,8 +10,13 @@ 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 app.models.models import User, Collection
from sqlalchemy import text 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=["寻配号"]) router = APIRouter(prefix="/api/seek", tags=["寻配号"])
@ -59,6 +68,11 @@ class SeekInfoResponse(BaseModel):
class Config: class Config:
from_attributes = True from_attributes = True
class MatchConfirmRequest(BaseModel):
info_id: str
contact: Optional[str] = None
collection_id: Optional[str] = None
# ============ API ============ # ============ API ============
@router.get("/list", response_model=list[SeekInfoResponse]) @router.get("/list", response_model=list[SeekInfoResponse])
def get_seek_list( def get_seek_list(
@ -83,8 +97,6 @@ def get_seek_list(
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()
# 添加用户名和匹配数量
from app.routers.information import match_collections_count, match_collections_count_from_coolbot
result = [] result = []
for item in items: for item in items:
user = db.query(User).filter(User.f99_90_id == item.user_id).first() user = db.query(User).filter(User.f99_90_id == item.user_id).first()
@ -95,7 +107,7 @@ def get_seek_list(
network_matched_count = 0 network_matched_count = 0
if item.expect_number and len(item.expect_number) == 10: if item.expect_number and len(item.expect_number) == 10:
if current_user and current_user.f99_90_id: if current_user and current_user.f99_90_id:
matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number) matched_count = match_self_collections_count(db, current_user.f99_90_id, item.expect_number)
network_matched_count = match_collections_count_from_coolbot(item.expect_number) network_matched_count = match_collections_count_from_coolbot(item.expect_number)
# 构建响应 # 构建响应
@ -232,7 +244,9 @@ def delete_seek(
db.commit() db.commit()
return {"message": "删除成功"} return {"message": "删除成功"}
# 获取自有藏品匹配列表
# ============ 匹配相关API ============
@router.get("/my-match") @router.get("/my-match")
def get_seek_match( def get_seek_match(
info_id: str, info_id: str,
@ -254,7 +268,6 @@ def get_seek_match(
).all() ).all()
# 按号码特征模式匹配 # 按号码特征模式匹配
from app.routers.information import match_pattern
matched = [] matched = []
if info.expect_number and len(info.expect_number) == 10: if info.expect_number and len(info.expect_number) == 10:
pattern = info.expect_number[2:] pattern = info.expect_number[2:]
@ -280,7 +293,6 @@ def get_seek_match(
] ]
} }
# 获取网络数据匹配列表
@router.get("/network-match/{info_id}") @router.get("/network-match/{info_id}")
def get_network_match( def get_network_match(
info_id: str, info_id: str,
@ -288,8 +300,6 @@ def get_network_match(
db: Session = Depends(get_db) 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() info = db.query(SeekInfo).filter(SeekInfo.id == info_id).first()
if not info: if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在") raise HTTPException(status_code=404, detail="寻配号信息不存在")
@ -299,3 +309,107 @@ def get_network_match(
matched = match_collections_list_from_coolbot(info.expect_number, limit=limit) matched = match_collections_list_from_coolbot(info.expect_number, limit=limit)
return {"matched_count": len(matched), "collections": matched} 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,
}

View File

@ -0,0 +1,208 @@
# coolbot_matcher - 一尘数据库号码匹配工具
# Version: 0.0.1 (2026-04-24)
# 描述:从一尘数据库(coolbot_data)查询匹配的藏品号码
from typing import List
from sqlalchemy import text
from sqlalchemy.orm import Session
from app.core.coolbot_db import coolbot_engine
def match_pattern(col_number: str, pattern: str) -> bool:
"""匹配号码特征模式
通配符规则:
- X = 任意数字
- A = 非4
- B = 非47
- C = 非347
- D = 非247
- E = 非2347
"""
if not col_number or not pattern:
return False
if len(col_number) != len(pattern):
return False
for i, p in enumerate(pattern):
c = col_number[i]
if p == 'X':
continue # 任意数字
elif p == 'A':
if c == '4':
return False
elif p == 'B':
if c == '4':
return False
if i < len(col_number) - 1 and col_number[i+1] == '7' and pattern[i+1] == 'X':
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 == 'L':
# L = 带4
if c != '4':
return False
elif p == 'N':
# N = 无4
if c == '4':
return False
else:
if c != p:
return False
return True
def match_self_collections_count(db: Session, user_id: str, expect_number: str) -> int:
"""根据号码特征计算匹配藏品数量(从用户自有藏品)
Args:
db: 数据库会话
user_id: 用户ID
expect_number: 期望号码 "J012345678" (固定J0前缀+8位特征)
Returns:
匹配的藏品数量
"""
from app.models.models import Collection
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 ''
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数据库
Args:
expect_number: 期望号码 "J012345678" (固定J0前缀+8位特征)
Returns:
匹配的藏品数量
"""
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
query = 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%'
""")
try:
with coolbot_engine.connect() as conn:
result = conn.execute(query)
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数据库
Args:
expect_number: 期望号码 "J012345678"
limit: 返回的最大数量
Returns:
匹配的藏品列表
"""
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 []
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 []

View File

@ -1,7 +1,7 @@
/** /**
* News - 资讯列表页面 * News - 资讯列表页面
* Version: 0.0.1 * Version: 0.0.2 (2026-04-24)
* 更新 * 更新迁移seek相关API到/api/seek/*
*/ */
import React, { useState, useEffect } from 'react' import React, { useState, useEffect } from 'react'
@ -195,7 +195,7 @@ export default function News() {
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
try { try {
const phone = getUserPhone() const phone = getUserPhone()
const res = await fetch(`${API_BASE}/api/information/seek/match-confirm`, { const res = await fetch(`${API_BASE}/api/seek/match-confirm`, {
method: 'POST', method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ info_id: infoId, contact: phone || '用户已同意' }) body: JSON.stringify({ info_id: infoId, contact: phone || '用户已同意' })
@ -232,7 +232,7 @@ export default function News() {
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
} }
try { try {
const res = await fetch(`${API_BASE}/api/information/seek/matched-user/${infoId}`, { headers: { Authorization: `Bearer ${token}` } }) const res = await fetch(`${API_BASE}/api/seek/matched-user/${infoId}`, { headers: { Authorization: `Bearer ${token}` } })
console.log('Matched user response:', res.status) console.log('Matched user response:', res.status)
const data = await res.json() const data = await res.json()
console.log('Matched user data:', data) console.log('Matched user data:', data)
@ -245,7 +245,7 @@ export default function News() {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
try { try {
const res = await fetch(`${API_BASE}/api/information/seek/publisher/${infoId}`, { headers: { Authorization: `Bearer ${token}` } }) const res = await fetch(`${API_BASE}/api/seek/publisher/${infoId}`, { headers: { Authorization: `Bearer ${token}` } })
console.log('Publisher response:', res.status) console.log('Publisher response:', res.status)
const data = await res.json() const data = await res.json()
console.log('Publisher data:', data) console.log('Publisher data:', data)
@ -257,7 +257,7 @@ export default function News() {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return } if (!token) { alert('请先登录'); return }
try { try {
const res = await fetch(`${API_BASE}/api/information/seek/match?info_id=${infoId}`, { const res = await fetch(`${API_BASE}/api/seek/my-match?info_id=${infoId}`, {
headers: { Authorization: `Bearer ${token}` } headers: { Authorization: `Bearer ${token}` }
}) })
const data = await res.json() const data = await res.json()
@ -331,7 +331,7 @@ export default function News() {
try { try {
// //
const content = (editingSeek.content || '') + '\n联系方式: ' + editingSeek.contact const content = (editingSeek.content || '') + '\n联系方式: ' + editingSeek.contact
await fetch(`${API_BASE}/api/information/${editingSeek.id}`, { await fetch(`${API_BASE}/api/seek/${editingSeek.id}`, {
method: 'PUT', method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' }, headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
@ -355,14 +355,13 @@ export default function News() {
const token = localStorage.getItem('token') const token = localStorage.getItem('token')
// editioncategory // editioncategory
const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' } const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' }
const res = await fetch(`${API_BASE}/api/information/`, { const res = await fetch(`${API_BASE}/api/seek/`, {
method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ body: JSON.stringify({
title: seekForm.title, title: seekForm.title,
content, content,
info_type: 'seek',
expect_category: seekForm.edition, expect_category: seekForm.edition,
expect_number: seekForm.features ? 'J0' + seekForm.features : None expect_number: seekForm.features ? 'J0' + seekForm.features : null
}) })
}) })
const data = await res.json() const data = await res.json()