v1.2.50 阶段性完善版本:寻配号优化、网络数据匹配、分页功能、logo修复

This commit is contained in:
龙大 2026-04-07 15:49:35 +08:00
parent 693e13bbab
commit e8c3069bfd
4 changed files with 277 additions and 14 deletions

View File

@ -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

View File

@ -1 +1 @@
VERSION=1.2.49
VERSION=1.2.50

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<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">
<title>甲辰收藏 v1.2.49</title>
<title>甲辰收藏 v1.2.50</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="/static/icons/favicon.png" />

View File

@ -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' && (
<div style={{ marginBottom: '8px' }}>
<span
<span
style={{ color: '#10b981', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer' }}
onClick={() => fetchMatchCollections(item.id)}
>
配号结果: {item.matched_count || 0}条藏品匹配成功
自有{item.matched_count || 0}条藏品匹配成功
</span>
{item.network_matched_count !== undefined && (
<span
style={{ color: '#3b82f6', fontSize: '13px', fontWeight: 'bold', cursor: 'pointer', marginLeft: '12px' }}
onClick={() => fetchNetworkMatchCollections(item.id)}
>
网络数据{item.network_matched_count}条匹配成功
</span>
)}
</div>
)}
{/* 正文 - 默认收起,点击展开 */}
@ -615,23 +645,24 @@ export default function News() {
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<button
onClick={() => {
if (item.is_matched === 'matched') return
if (item.matched_count > 0) {
matchAndContact(item.id)
} else {
setCustomModal({show: true, title: '提示', content: '暂无匹配藏品,无法匹配'})
}
}}
disabled={item.matched_count === 0}
disabled={item.matched_count === 0 || item.is_matched === 'matched'}
style={{
flex: 1,
padding: '8px 12px',
borderRadius: '6px',
border: 'none',
fontSize: '12px',
background: item.matched_count > 0 ? '#3b82f6' : '#4b5563',
background: (item.matched_count > 0 && item.is_matched !== 'matched') ? '#3b82f6' : '#4b5563',
color: '#fff',
cursor: item.matched_count > 0 ? 'pointer' : 'not-allowed',
opacity: item.matched_count > 0 ? 1 : 0.5
cursor: (item.matched_count > 0 && item.is_matched !== 'matched') ? 'pointer' : 'not-allowed',
opacity: (item.matched_count > 0 && item.is_matched !== 'matched') ? 1 : 0.5
}}
>
匹配并联系藏友
@ -753,6 +784,74 @@ export default function News() {
</div>
)}
{/* 网络数据匹配藏品列表弹窗 */}
{showNetworkMatchList && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ background: '#1e293b', borderRadius: '12px', padding: '20px', width: '90%', maxHeight: '80vh', overflow: 'auto' }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h3 style={{ color: '#fff', margin: 0 }}>匹配藏品清单网络数据</h3>
<button onClick={() => setShowNetworkMatchList(false)} style={{ background: 'transparent', border: 'none', color: '#94a3b8', fontSize: '20px', cursor: 'pointer' }}>×</button>
</div>
{networkMatchCollections.length === 0 ? (
<div style={{ color: '#94a3b8', textAlign: 'center', padding: '20px' }}>暂无匹配藏品</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{networkMatchCollections.map(c => (
<div
key={c.id}
style={{ background: '#0f172a', borderRadius: '8px', padding: '12px', cursor: 'pointer' }}
onClick={() => {
if (c.post_url) {
window.open(c.post_url, '_blank')
}
setShowNetworkMatchList(false)
}}
>
<div style={{ color: '#e2e8f0', fontSize: '13px' }}>
<span style={{ color: '#94a3b8' }}>名称: </span>{c.name || '-'}
</div>
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
<span style={{ color: '#94a3b8' }}>冠字号: </span>{c.crown_code}
</div>
{c.price && (
<div style={{ color: '#e2e8f0', fontSize: '13px', marginTop: '4px' }}>
<span style={{ color: '#94a3b8' }}>价格: </span>¥{c.price}
</div>
)}
<div style={{ color: '#3b82f6', fontSize: '12px', marginTop: '6px' }}>
点击查看原帖
</div>
</div>
))}
</div>
)}
</div>
</div>
)}
{/* 分页组件 */}
{totalPages > 1 && (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', gap: '8px', padding: '16px', marginTop: '8px' }}>
<button
onClick={() => { if (currentPage > 1) { setCurrentPage(currentPage - 1); fetchInfoList() } }}
disabled={currentPage === 1}
style={{ padding: '8px 16px', borderRadius: '6px', border: 'none', background: currentPage === 1 ? '#374151' : '#3b82f6', color: currentPage === 1 ? '#6b7280' : '#fff', cursor: currentPage === 1 ? 'not-allowed' : 'pointer', fontSize: '13px' }}
>
上一页
</button>
<span style={{ color: '#94a3b8', fontSize: '13px' }}>
{currentPage} / {totalPages}
</span>
<button
onClick={() => { if (currentPage < totalPages) { setCurrentPage(currentPage + 1); fetchInfoList() } }}
disabled={currentPage >= totalPages}
style={{ padding: '8px 16px', borderRadius: '6px', border: 'none', background: currentPage >= totalPages ? '#374151' : '#3b82f6', color: currentPage >= totalPages ? '#6b7280' : '#fff', cursor: currentPage >= totalPages ? 'not-allowed' : 'pointer', fontSize: '13px' }}
>
下一页
</button>
</div>
)}
{/* 我的寻号弹窗 */}
{showMySeeks && (
<div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>