diff --git a/VERSION b/VERSION index dd91a8b..a67e110 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.98 +1.2.99 diff --git a/backend/VERSION b/backend/VERSION index dd91a8b..a67e110 100644 --- a/backend/VERSION +++ b/backend/VERSION @@ -1 +1 @@ -1.2.98 +1.2.99 diff --git a/backend/app/routers/information.py b/backend/app/routers/information.py index 7fb9484..46ed3b8 100644 --- a/backend/app/routers/information.py +++ b/backend/app/routers/information.py @@ -7,6 +7,8 @@ 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 sqlalchemy import text from app.models.models import User, Information, Collection router = APIRouter(prefix="/api/information", tags=["资讯"]) @@ -182,6 +184,88 @@ 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:] + 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数据库)""" + if not expect_number or len(expect_number) != 10: + return [] + if not expect_number.startswith('J0'): + return [] + pattern = expect_number[2:] + 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": str(row[8]) 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 = 任意数字 @@ -891,3 +975,91 @@ def get_publisher_info( "contact": contact, "created_at": info.created_at.isoformat() if info.created_at else None } + + +# 获取网络数据匹配列表 +@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 + } + + +# 获取所有seek列表(包含网络匹配数量) +@router.get("/seek/list") +def get_seek_list_all( + status: str = Query("active"), + user_id: Optional[str] = None, + user_only: bool = Query(False), + current_user: Optional[User] = Depends(get_current_user), + db: Session = Depends(get_db) +): + """获取寻配号列表(包含网络匹配数量)""" + query = db.query(Information).filter( + Information.info_type == "seek", + Information.status == status + ) + + if user_only and current_user: + query = query.filter(Information.user_id == current_user.f99_90_id) + + if user_id: + query = query.filter(Information.user_id == user_id) + + items = query.order_by(Information.created_at.desc()).all() + + result = [] + for item in items: + # 计算自有匹配数量 + matched_count = 0 + if item.expect_number: + matched_count = match_collections_count(db, current_user.f99_90_id if current_user else "", item.expect_number) + + # 计算网络匹配数量 + network_matched_count = 0 + if item.expect_number: + network_matched_count = match_collections_count_from_coolbot(item.expect_number) + + result.append({ + "id": item.id, + "user_id": item.user_id, + "title": item.title, + "content": item.content, + "expect_category": item.expect_category, + "expect_version": item.expect_version, + "expect_packaging": item.expect_packaging, + "expect_number": item.expect_number, + "expect_price_min": item.expect_price_min, + "expect_price_max": item.expect_price_max, + "status": item.status, + "is_matched": item.is_matched, + "matched_user_id": item.matched_user_id, + "matched_contact": item.matched_contact, + "view_count": item.view_count, + "contact_count": item.contact_count, + "created_at": item.created_at.isoformat() if item.created_at else None, + "user_name": item.user.f01_01_name if item.user else None, + "matched_count": matched_count, + "network_matched_count": network_matched_count + }) + + return result diff --git a/frontend/VERSION b/frontend/VERSION index dd91a8b..a67e110 100644 --- a/frontend/VERSION +++ b/frontend/VERSION @@ -1 +1 @@ -1.2.98 +1.2.99 diff --git a/frontend/index.html b/frontend/index.html index 7335504..ae69927 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - 甲辰收藏 v=1.2.98 + 甲辰收藏 v=1.2.99 diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index f6912a9..cc9365b 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -9,6 +9,9 @@ export default function Home() { const [seekStats, setSeekStats] = useState({ seekCount: 0, matchedCount: 0 }) const [recentPosts, setRecentPosts] = useState([]) const [dragonStats, setDragonStats] = useState({}) + const [dealVersion, setDealVersion] = useState('龙钞') + const [dealCategoryStats, setDealCategoryStats] = useState([]) + const [dealInfoList, setDealInfoList] = useState([]) const currentPath = window.location.hash.slice(1) || '/' useEffect(() => { @@ -78,8 +81,16 @@ export default function Home() { }).catch(() => {}) // 获取寻配号统计数据 - fetch('/api/information/seek/stats').then(res => res.json()).then(data => { - setSeekStats(data || {}) + // 获取寻配号统计数据 + fetch('/api/seek/stats').then(res => res.json()).then(data => { + setSeekStats({ seekCount: data.total || 0, userMatchedCount: data.matched || 0, totalMatchedCount: data.unmatched || 0 }) + }).catch(() => {}) + + // 获取成交行情分类统计(需要登录获取全部数据) + const token = localStorage.getItem('token') + const headers = token ? { 'Authorization': 'Bearer ' + token } : {} + fetch('/api/deal/list?page_size=500', { headers }).then(res => res.json()).then(data => { + setDealInfoList(Array.isArray(data) ? data : (data.items || [])) }).catch(() => {}) }, []) @@ -172,7 +183,7 @@ export default function Home() {
快捷操作
window.location.hash = '#/add?mode=ocr'} style={{ - background: 'linear-gradient(135deg, #22c55e 0%, #16a34a 100%)', + background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '12px', padding: '12px 16px', cursor: 'pointer', @@ -186,7 +197,7 @@ export default function Home() {
AI识别添加
window.location.hash = '#/add?mode=deal'} style={{ - background: 'linear-gradient(135deg, #ec4899 0%, #db2777 100%)', + background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '12px', padding: '12px 16px', cursor: 'pointer', @@ -200,7 +211,7 @@ export default function Home() {
录入成交行情
window.location.hash = '#/news?type=seek'} style={{ - background: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)', + background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '12px', padding: '12px 16px', cursor: 'pointer', @@ -214,7 +225,7 @@ export default function Home() {
发布寻配需求
window.location.hash = '#/add?mode=manual'} style={{ - background: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', + background: 'linear-gradient(135deg, rgba(255,255,255,0.08) 0%, rgba(255,255,255,0.02) 100%)', border: '1px solid rgba(255,255,255,0.08)', borderRadius: '12px', padding: '12px 16px', cursor: 'pointer', @@ -258,31 +269,141 @@ export default function Home() {
+ {/* 成交行情信息 */} +
+ {(() => { + const versions = ['龙钞', '马钞', '蛇钞', '其他'] + const packagings = ['标百', '标十', '单张'] + const categories = ['带4号', '带7号', '永恒号', '永恒', '钻石号', '钻石', '如意号', '朦胧号', '朦胧王', '天马号', '金山号', '金马号', '天马王', '金山王', '金马王', '倒置号', '圆圆号', '通货', '无4'] + + const normalizeCat = (c) => { + if (c === '通货') return '带4号' + if (c === '无4') return '带7号' + return c + } + + const filteredData = dealInfoList.filter(item => { + const serial = (item.title || '').split('-')[0] || '' + let version = '其他' + if (serial.startsWith('J0')) version = '龙钞' + else if (serial.startsWith('J1')) version = '马钞' + else if (serial.startsWith('J3')) version = '蛇钞' + if (version !== dealVersion) return false + return true + }) + + const calcAvg = (pkg, cat) => { + const items = filteredData.filter(item => { + const content = item.content || '' + const p = content.includes('包装:') ? content.split('包装:')[1].split('\n')[0].trim() : (item.packaging || '') + let c = content.includes('分类:') ? content.split('分类:')[1].split('\n')[0].trim() : (item.category || '') + c = normalizeCat(c) + return p === pkg && c === cat + }) + if (items.length === 0) return null + const sum = items.reduce((a, b) => a + (b.deal_price || 0), 0) + return { avg: Math.round(sum / items.length), count: items.length, items } + } + + return ( +
+
💰 龙钞成交数据分类汇总(均价)
+
+
+
+ {versions.map(v => ( + + ))} +
+
+ +
+ + + + + {packagings.map(p => ( + + ))} + + + + {(() => { + const catAvg = categories.map(cat => { + const prices = [] + packagings.forEach(pkg => { + const d = calcAvg(pkg, cat) + if (d) prices.push(d.avg) + }) + const avg = prices.length > 0 ? Math.round(prices.reduce((a,b) => a+b, 0) / prices.length) : 0 + return { cat, avg } + }).filter(c => c.avg > 0) + + catAvg.sort((a, b) => a.avg - b.avg) + const categoryOrder = ['带4号', '带7号', '永恒号', '天马号', '天马王', '金山号', '金山王', '钻石号', '朦胧号', '朦胧王', '金马号', '金马王', '倒置号', '圆圆号'] + const sortedCats = categoryOrder.filter(cat => catAvg.some(c => c.cat === cat)) + .concat(catAvg.filter(c => !categoryOrder.includes(c.cat)).map(c => c.cat)) + + return sortedCats.map(cat => { + const rowData = packagings.map(pkg => calcAvg(pkg, cat)) + const hasData = rowData.some(d => d !== null) + if (!hasData) return null + return ( + + + {rowData.map((d, i) => ( + + ))} + + ) + }) + })()} + +
{p}
{cat} + {d ? ( +
+ ¥{d.avg.toLocaleString()} + ({d.count}) +
+ ) : -} +
+
+
+
+ ) + })()} +
+ {/* 一尘今日数据 */}
📊 一尘今日连体纪念钞数据
-
+
{yichensStats.total || 0}
总帖子
-
+
{yichensStats.deals || 0}
出售
-
+
{yichensStats.wants || 0}
求购
-
+
{yichensStats.dragons || 0}
龙钞
-
+
{yichensStats.horses || 0}
马钞
-
+
{yichensStats.tianma || 0}
天马
@@ -292,43 +413,43 @@ export default function Home() { {/* 今日龙钞帖子数据统计 */}
🐉 今日龙钞帖子数据统计
-
+
{/* 表头 */}
-
出售
-
求购
+
出售
+
求购
合计
{/* 数据行 */}
-
带4
-
{dragonStats.dai4?.deals || 0}
-
{dragonStats.dai4?.wants || 0}
+
带4
+
{dragonStats.dai4?.deals || 0}
+
{dragonStats.dai4?.wants || 0}
{(dragonStats.dai4?.deals || 0) + (dragonStats.dai4?.wants || 0)}
-
带7号
-
{dragonStats.wu4?.deals || 0}
-
{dragonStats.wu4?.wants || 0}
+
带7号
+
{dragonStats.wu4?.deals || 0}
+
{dragonStats.wu4?.wants || 0}
{(dragonStats.wu4?.deals || 0) + (dragonStats.wu4?.wants || 0)}
-
无47
-
{dragonStats.wu47?.deals || 0}
-
{dragonStats.wu47?.wants || 0}
+
无47
+
{dragonStats.wu47?.deals || 0}
+
{dragonStats.wu47?.wants || 0}
{(dragonStats.wu47?.deals || 0) + (dragonStats.wu47?.wants || 0)}
-
无247
-
{dragonStats.wu247?.deals || 0}
-
{dragonStats.wu247?.wants || 0}
+
无247
+
{dragonStats.wu247?.deals || 0}
+
{dragonStats.wu247?.wants || 0}
{(dragonStats.wu247?.deals || 0) + (dragonStats.wu247?.wants || 0)}
-
无347
-
{dragonStats.wu347?.deals || 0}
-
{dragonStats.wu347?.wants || 0}
+
无347
+
{dragonStats.wu347?.deals || 0}
+
{dragonStats.wu347?.wants || 0}
{(dragonStats.wu347?.deals || 0) + (dragonStats.wu347?.wants || 0)}
diff --git a/frontend/src/pages/News.jsx b/frontend/src/pages/News.jsx index e54e7c9..7f827d9 100644 --- a/frontend/src/pages/News.jsx +++ b/frontend/src/pages/News.jsx @@ -87,7 +87,7 @@ export default function News() { let url = activeTab === 'yichen' ? `${API_BASE}/api/information/list?info_type=${activeTab}` : activeTab === 'seek' - ? `${API_BASE}/api/seek/list` + ? `${API_BASE}/api/information/seek/list` : `${API_BASE}/api/deal/list` // 成交行情和寻配号每页500条 @@ -303,7 +303,7 @@ export default function News() { const token = localStorage.getItem('token') if (!token) { alert('请先登录'); return } try { - const res = await fetch(`${API_BASE}/api/seek/list`, { + const res = await fetch(`${API_BASE}/api/information/seek/list`, { headers: { Authorization: `Bearer ${token}` } }) const data = await res.json() @@ -319,7 +319,7 @@ export default function News() { const token = localStorage.getItem('token') if (!token) { alert('请先登录'); return } try { - const res = await fetch(`${API_BASE}/api/seek/list`, { + const res = await fetch(`${API_BASE}/api/information/seek/list`, { headers: { Authorization: `Bearer ${token}` } }) const data = await res.json()