diff --git a/backend/app/routers/information.py b/backend/app/routers/information.py
index 2987785..a249fbb 100644
--- a/backend/app/routers/information.py
+++ b/backend/app/routers/information.py
@@ -1,12 +1,14 @@
# 资讯API路由
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session, joinedload
+from sqlalchemy import text
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime, date
from app.core.database import get_db
from app.core.auth import get_current_user
+from app.core.coolbot_db import coolbot_engine
from app.models.models import User, Information, Collection
router = APIRouter(prefix="/api/information", tags=["资讯"])
@@ -74,6 +76,8 @@ class InformationResponse(BaseModel):
collection_number: Optional[str] = None
# 匹配数量(我的藏品中满足条件的数量)
matched_count: Optional[int] = 0
+ # 网络数据匹配数量(coolbot_data数据库中满足条件的数量)
+ network_matched_count: Optional[int] = 0
class Config:
from_attributes = True
@@ -87,7 +91,8 @@ def get_information_list(
page: int = Query(1, ge=1),
page_size: int = Query(20, ge=1, le=100),
current_user: Optional[User] = Depends(get_current_user),
- db: Session = Depends(get_db)
+ db: Session = Depends(get_db),
+ response: Response = None
):
"""获取资讯列表(公开,无需登录)"""
query = db.query(Information).options(
@@ -142,8 +147,21 @@ def get_information_list(
collection_version=item.collection.f02_11_version if item.collection else None,
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
matched_count=matched_count,
+ network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0,
))
+ # 获取总数并设置响应头
+ from fastapi import Response
+ total_query = db.query(Information).filter(Information.status == status)
+ if info_type:
+ total_query = total_query.filter(Information.info_type == info_type)
+ total_count = total_query.count()
+ total_pages = (total_count + page_size - 1) // page_size
+
+ # 设置响应头
+ response.headers['X-Total-Pages'] = str(total_pages)
+ response.headers['X-Total-Count'] = str(total_count)
+
return result
@@ -182,6 +200,110 @@ def match_collections_count(db: Session, user_id: str, expect_number: str) -> in
return count
+def match_collections_count_from_coolbot(expect_number: str) -> int:
+ """根据号码特征计算匹配藏品数量(从coolbot_data数据库)"""
+ 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
+
+ # 直接查询coolbot_data数据库
+ query = text("""
+ SELECT COUNT(*) 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)
+ total_count = result.scalar() or 0
+
+ # 遍历匹配
+ query_all = 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%'
+ """)
+ result = conn.execute(query_all)
+
+ 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数据库)"""
+ 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 []
+
+ # 直接查询coolbot_data数据库
+ 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 []
+
+
def match_pattern(col_number: str, pattern: str) -> bool:
"""匹配号码特征模式"""
# X = 任意数字
@@ -502,7 +624,36 @@ def get_seek_match(
"cost_price": c.f05_40_cost_price,
}
for c in matched
- ]
+ ],
+ "network_matched_count": match_collections_count_from_coolbot(info.expect_number) if info.expect_number else 0,
+ "network_collections": match_collections_list_from_coolbot(info.expect_number, limit=20) if info.expect_number else []
+ }
+
+
+# 获取网络数据匹配列表
+@router.get("/seek/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(Information).filter(
+ Information.id == info_id,
+ Information.info_type == "seek"
+ ).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
}
@@ -558,8 +709,21 @@ def get_my_seeks(
collection_version=item.collection.f02_11_version if item.collection else None,
collection_number=item.collection.f02_10_prefix_serial if item.collection else None,
matched_count=matched_count,
+ network_matched_count=match_collections_count_from_coolbot(item.expect_number) if item.info_type == 'seek' and item.expect_number else 0,
))
+ # 获取总数并设置响应头
+ from fastapi import Response
+ total_query = db.query(Information).filter(Information.status == status)
+ if info_type:
+ total_query = total_query.filter(Information.info_type == info_type)
+ total_count = total_query.count()
+ total_pages = (total_count + page_size - 1) // page_size
+
+ # 设置响应头
+ response.headers['X-Total-Pages'] = str(total_pages)
+ response.headers['X-Total-Count'] = str(total_count)
+
return result
diff --git a/config/VERSION b/config/VERSION
index 9d33ae0..532db09 100644
--- a/config/VERSION
+++ b/config/VERSION
@@ -1 +1 @@
-VERSION=1.2.49
+VERSION=1.2.50
diff --git a/frontend/index.html b/frontend/index.html
index b11aa93..6525875 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
-
甲辰收藏 v1.2.49
+ 甲辰收藏 v1.2.50
diff --git a/frontend/src/pages/News.jsx b/frontend/src/pages/News.jsx
index adad95b..b316cc6 100644
--- a/frontend/src/pages/News.jsx
+++ b/frontend/src/pages/News.jsx
@@ -33,6 +33,8 @@ export default function News() {
const [matchedStatus, setMatchedStatus] = useState({}) // 存储各寻号的匹配状态
const [showMatchList, setShowMatchList] = useState(false)
const [matchCollections, setMatchCollections] = useState([])
+ const [networkMatchCollections, setNetworkMatchCollections] = useState([])
+ const [showNetworkMatchList, setShowNetworkMatchList] = useState(false)
const [customModal, setCustomModal] = useState({show: false, title: '', content: ''})
const [seekForm, setSeekForm] = useState({
edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || ''
@@ -41,6 +43,8 @@ export default function News() {
const [viewMode, setViewMode] = useState('all')
const [expandedItems, setExpandedItems] = useState({}) // 展开状态
const [loading, setLoading] = useState(false)
+ const [currentPage, setCurrentPage] = useState(1)
+ const [totalPages, setTotalPages] = useState(1)
const API_BASE = localStorage.getItem('API_BASE') || ''
const currentUser = localStorage.getItem('token') ? { id: 'logged_in' } : null
@@ -73,7 +77,10 @@ export default function News() {
const type = activeTab === 'seek' ? 'seek' : activeTab === 'deal' ? 'deal' : 'yichen'
// 始终传递token,以便获取准确的matched_count
const headers = token ? { Authorization: `Bearer ${token}` } : {}
- const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}`, { headers })
+ const res = await fetch(`${API_BASE}/api/information/list?info_type=${type}&page=${currentPage}&page_size=50`, { headers })
+ // 从响应头获取总页数
+ const total = res.headers.get('X-Total-Pages') || res.headers.get('x-total-pages')
+ if (total) setTotalPages(parseInt(total))
// 检查响应状态
if (!res.ok) {
console.error('获取资讯列表失败:', res.status)
@@ -147,10 +154,11 @@ export default function News() {
const token = localStorage.getItem('token')
if (!token) { alert('请先登录'); return }
try {
+ const phone = getUserPhone()
const res = await fetch(`${API_BASE}/api/information/seek/match-confirm`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
- body: JSON.stringify({ info_id: infoId })
+ body: JSON.stringify({ info_id: infoId, contact: phone || '用户已同意' })
})
const data = await res.json()
if (data.message === '匹配成功,已通知发布者') {
@@ -188,7 +196,7 @@ export default function News() {
console.log('Matched user response:', res.status)
const data = await res.json()
console.log('Matched user data:', data)
- if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.matched_contact || '未提供'}`})
+ if (data.user_name) setCustomModal({show: true, title: '匹配者信息', content: `用户名: ${data.user_name}\n手机号: ${data.phone || '未提供'}\n\n免责声明及风险提示:所有用户信息仅作参考,交易请走正规平台,如产生经济损失与本站无关,后果自负。`})
else if (data.detail) alert(data.detail)
} catch (e) { console.error(e) }
}
@@ -201,7 +209,7 @@ export default function News() {
console.log('Publisher response:', res.status)
const data = await res.json()
console.log('Publisher data:', data)
- if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n联系方式: ${data.contact || '未提供'}`})
+ if (data.user_name) setCustomModal({show: true, title: '发布者信息', content: `用户名: ${data.user_name}\n手机号: ${data.phone || '未提供'}\n\n免责声明及风险提示:所有用户信息仅作参考,交易请走正规平台,如产生经济损失与本站无关,后果自负。`})
else if (data.detail) alert(data.detail)
} catch (e) { console.error(e) }
}
@@ -224,6 +232,20 @@ export default function News() {
}
}
+ // 获取网络数据匹配列表
+ const fetchNetworkMatchCollections = async (infoId) => {
+ try {
+ const res = await fetch(`${API_BASE}/api/information/seek/network-match/${infoId}`)
+ const data = await res.json()
+ console.log('网络数据匹配结果:', data)
+ setNetworkMatchCollections(data.collections || [])
+ setShowNetworkMatchList(true)
+ } catch (e) {
+ console.error('获取网络匹配藏品失败:', e)
+ alert('获取网络匹配藏品失败: ' + e.message)
+ }
+ }
+
// 获取当前用户ID
const currentUserId = getStoredUserId()
@@ -523,12 +545,20 @@ export default function News() {
{/* 配号结果 - 仅寻配号显示 */}
{activeTab === 'seek' && (
- fetchMatchCollections(item.id)}
>
- 配号结果: {item.matched_count || 0}条藏品匹配成功
+ 自有{item.matched_count || 0}条藏品匹配成功
+ {item.network_matched_count !== undefined && (
+ fetchNetworkMatchCollections(item.id)}
+ >
+ 网络数据{item.network_matched_count}条匹配成功
+
+ )}
)}
{/* 正文 - 默认收起,点击展开 */}
@@ -615,23 +645,24 @@ export default function News() {
)}
+ {/* 网络数据匹配藏品列表弹窗 */}
+ {showNetworkMatchList && (
+
+
+
+
匹配藏品清单(网络数据)
+
+
+ {networkMatchCollections.length === 0 ? (
+
暂无匹配藏品
+ ) : (
+
+ {networkMatchCollections.map(c => (
+
{
+ if (c.post_url) {
+ window.open(c.post_url, '_blank')
+ }
+ setShowNetworkMatchList(false)
+ }}
+ >
+
+ 名称: {c.name || '-'}
+
+
+ 冠字号: {c.crown_code}
+
+ {c.price && (
+
+ 价格: ¥{c.price}
+
+ )}
+
+ 点击查看原帖 →
+
+
+ ))}
+
+ )}
+
+
+ )}
+
+ {/* 分页组件 */}
+ {totalPages > 1 && (
+
+
+
+ 第 {currentPage} / {totalPages} 页
+
+
+
+ )}
+
{/* 我的寻号弹窗 */}
{showMySeeks && (