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 && (