v1.2.50 阶段性完善版本:寻配号优化、网络数据匹配、分页功能、logo修复
This commit is contained in:
parent
693e13bbab
commit
e8c3069bfd
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
VERSION=1.2.49
|
||||
VERSION=1.2.50
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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' }}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue