diff --git a/VERSION b/VERSION index d659080..1ce7217 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -VERSION=1.2.59 +VERSION=1.2.41 diff --git a/backend/app/main.py b/backend/app/main.py index 1b33e39..2ec184f 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -52,11 +52,10 @@ app = FastAPI( # 设置全局错误处理器 setup_error_handlers(app) -# CORS 配置 - 生产环境限制域名 -ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://47.103.29.111,http://120.55.81.21,https://socoolbot.com").split(",") +# CORS 配置 app.add_middleware( CORSMiddleware, - allow_origins=ALLOWED_ORIGINS, # 生产环境限制域名 + allow_origins=["*"], # 生产环境应该限制域名 allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -67,10 +66,10 @@ uploads_dir = "uploads" os.makedirs(uploads_dir, exist_ok=True) app.mount("/uploads", StaticFiles(directory=uploads_dir), name="uploads") -# 挂载项目静态资源目录(可选,生产环境建议用 Nginx) -# static_dir = Path(__file__).parent.parent.parent / "static" -# if static_dir.exists(): -# app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") +# 挂载项目静态资源目录 +static_dir = Path(__file__).parent.parent / "static" +if static_dir.exists(): + app.mount("/static", StaticFiles(directory=str(static_dir)), name="static") # 添加日志中间件 app.middleware("http")(logging_middleware) @@ -82,7 +81,7 @@ app.include_router(operations.router) app.include_router(ocr_router.router) # OCR 识别 app.include_router(users_router.router) # 当前用户接口 app.include_router(users_router.admin_router) # 管理员用户管理 -app.include_router(information_router.router) # 资讯 +app.include_router(information_router.router) app.include_router(yichens_router.router) # 一尘看板 diff --git a/backend/app/models/models.py b/backend/app/models/models.py index 40811e7..387f670 100644 --- a/backend/app/models/models.py +++ b/backend/app/models/models.py @@ -163,7 +163,7 @@ class Information(Base): # 内容描述 content = Column(Text, nullable=True) - + # 关联藏品ID collection_id = Column(String(36), ForeignKey("collections.f99_90_id", ondelete="SET NULL"), nullable=True) diff --git a/backend/app/routers/information.py b/backend/app/routers/information.py index 7fb9484..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=["资讯"]) @@ -16,7 +18,7 @@ router = APIRouter(prefix="/api/information", tags=["资讯"]) class InformationCreate(BaseModel): info_type: str # seek-寻配号, deal-成交数据, publish-发布 title: str - content: Optional[str] = None + content: Optional[str] collection_id: Optional[str] = None expect_category: Optional[str] = None expect_version: Optional[str] = None @@ -30,7 +32,7 @@ class InformationCreate(BaseModel): class InformationUpdate(BaseModel): title: Optional[str] = None - content: Optional[str] = None + content: Optional[str] status: Optional[str] = None expect_category: Optional[str] = None expect_version: Optional[str] = None @@ -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 @@ -891,3 +1055,18 @@ def get_publisher_info( "contact": contact, "created_at": info.created_at.isoformat() if info.created_at else None } + + +@router.get("/yichen-posts") +def get_yichen_posts(category: str = None, search: str = None, page: int = 1, page_size: int = 20): + from app.models.models import Information + from sqlalchemy import desc + query = db.query(Information).filter(Information.info_type == 'yichen') + if category: + query = query.filter(Information.expect_category == category) + if search: + query = query.filter(Information.title.contains(search)) + total = query.count() + offset = (page - 1) * page_size + items = query.order_by(desc(Information.created_at)).offset(offset).limit(page_size).all() + return {"data": items, "pagination": {"page": page, "limit": page_size, "total": total}} diff --git a/config/VERSION b/config/VERSION index d659080..532db09 100644 --- a/config/VERSION +++ b/config/VERSION @@ -1 +1 @@ -VERSION=1.2.59 +VERSION=1.2.50 diff --git a/frontend/index.html b/frontend/index.html index 2af0426..6525875 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - 甲辰收藏 v1.2.53 + 甲辰收藏 v1.2.50 diff --git a/frontend/src/pages/Home.jsx b/frontend/src/pages/Home.jsx index dc0262b..1a044ca 100644 --- a/frontend/src/pages/Home.jsx +++ b/frontend/src/pages/Home.jsx @@ -5,6 +5,8 @@ export default function Home() { const [user, setUser] = useState(null) const [stats, setStats] = useState({ totalCount: 0, totalCost: 0, totalRevenue: 0, expectedProfit: 0, totalProfit: 0, gradedCount: 0 }) const [recentCollections, setRecentCollections] = useState([]) + const [yichensStats, setYichensStats] = useState({ total: 0, deals: 0, wants: 0, dragons: 0, horses: 0, tianma: 0 }) + const [recentPosts, setRecentPosts] = useState([]) const currentPath = window.location.hash.slice(1) || '/' useEffect(() => { @@ -54,6 +56,18 @@ export default function Home() { } }, []) + // 获取一尘看板数据 + useEffect(() => { + fetch('/api/yichens/stats/today').then(res => res.json()).then(data => { + setYichensStats(data || {}) + }).catch(() => {}) + + // 获取最新一尘帖子 + fetch('/api/yichens/posts?limit=10&offset=0').then(res => res.json()).then(data => { + setRecentPosts(Array.isArray(data) ? data : []) + }).catch(() => {}) + }, []) + // 检查是否为管理员 const isAdmin = user && user.role === 'admin' @@ -96,7 +110,7 @@ export default function Home() { backdropFilter: 'blur(10px)' }}>
- 甲辰收藏 + 甲辰收藏
{user?.username || '用户'}
欢迎回来 👋
@@ -175,11 +189,42 @@ export default function Home() {
- {/* 最近藏品 */} + {/* 一尘今日数据 */} +
+
📊 一尘今日数据
+
+
+
{yichensStats.total || 0}
+
总帖子
+
+
+
{yichensStats.deals || 0}
+
出售
+
+
+
{yichensStats.wants || 0}
+
求购
+
+
+
{yichensStats.dragons || 0}
+
龙钞
+
+
+
{yichensStats.horses || 0}
+
马钞
+
+
+
{yichensStats.tianma || 0}
+
天马
+
+
+
+ + {/* 最新一尘发帖 */}
-
最近藏品
-
window.location.hash = '#/list'} style={{ color: '#3b82f6', fontSize: '12px', cursor: 'pointer' }}>查看全部 →
+
📝 最新一尘发帖
+
window.location.hash = '#/news'} style={{ color: '#3b82f6', fontSize: '12px', cursor: 'pointer' }}>查看全部 →
- {recentCollections.length === 0 ? ( -
暂无藏品
+ {recentPosts.length === 0 ? ( +
暂无帖子
) : ( - recentCollections.map((item, idx) => ( -
window.location.hash = '#/detail?id=' + item.id} style={{ + recentPosts.map((item, idx) => ( +
window.location.hash = '#/news'} style={{ padding: '12px 16px', - borderBottom: idx < recentCollections.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none', + borderBottom: idx < recentPosts.length - 1 ? '1px solid rgba(255,255,255,0.05)' : 'none', cursor: 'pointer', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> -
-
{item.code || '-'} {item.prefixSerial || ''}
-
{item.version || '-'} · {item.status === 'sold' ? '已售' : item.status === 'in_collection' ? '收藏中' : item.status}
+
+
{item.title || '无标题'}
+
{item.category || '-'} · {item.author_username || '未知'} · {item.post_time?.substring(0, 16) || ''}
-
- {item.costPrice ? '¥' + item.costPrice : '-'} +
+ {item.post_type === 'deal' ? '出售' : item.post_type === 'want' ? '求购' : '其他'}
)) diff --git a/frontend/src/pages/Info.jsx b/frontend/src/pages/Info.jsx index 390bdb8..17c7baf 100644 --- a/frontend/src/pages/Info.jsx +++ b/frontend/src/pages/Info.jsx @@ -672,6 +672,9 @@ export default function Info() {
)} -
+
) } + + + diff --git a/frontend/src/pages/News.jsx b/frontend/src/pages/News.jsx index 04cb53f..b316cc6 100644 --- a/frontend/src/pages/News.jsx +++ b/frontend/src/pages/News.jsx @@ -1,4 +1,5 @@ import React, { useState, useEffect } from 'react' +import YichensBoard from './YichensBoard' // 本地获取用户手机号 - 添加异常处理 const getUserPhone = () => { @@ -22,7 +23,7 @@ const getStoredUserId = () => { // 资讯页面 - 展示寻配号和行情信息(所有人可见) export default function News() { - const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'seek') + const [activeTab, setActiveTab] = useState(localStorage.getItem('news_activeTab') || 'yichen') const [showSeekPublish, setShowSeekPublish] = useState(false) const [showMySeeks, setShowMySeeks] = useState(false) // 我的寻号 const [showContactId, setShowContactId] = useState(null) // 显示联系方式的寻号ID @@ -32,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() || '' @@ -40,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 @@ -69,10 +74,13 @@ export default function News() { // 寻配号对所有人公开,无需登录即可查看 const token = localStorage.getItem('token') // 根据tab获取不同类型的数据 - const type = activeTab === 'seek' ? 'seek' : 'deal' + 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) @@ -146,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 === '匹配成功,已通知发布者') { @@ -187,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) } } @@ -200,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) } } @@ -223,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() @@ -315,7 +338,8 @@ export default function News() { // Tab切换 const tabs = [ { key: 'seek', label: '🔍 寻配号' }, - { key: 'deal', label: '💰 成交行情' } + { key: 'deal', label: '💰 成交行情' }, + { key: 'yichen', label: '📊 一尘看板' } ] const formatDate = (dateStr) => { @@ -387,7 +411,7 @@ export default function News() { readOnly={isFixed} onChange={(e) => { if (i < 2) return - const val = e.target.value.toUpperCase().replace(/[^0-9XABCFG]/g, '') + const val = e.target.value.toUpperCase().replace(/[^0-9XABCDEFG]/g, '') const newFeatures = (seekForm.features || '').split('') while (newFeatures.length < 8) newFeatures.push('') newFeatures[i - 2] = val @@ -435,7 +459,7 @@ export default function News() {
)}

- {activeTab === 'seek' ? '🔍 寻配号信息' : '💰 成交行情信息'} + {activeTab === 'seek' ? '🔍 寻配号信息' : activeTab === 'deal' ? '💰 成交行情信息' : '📊 一尘看板'} {activeTab === 'seek' && (
@@ -450,11 +474,11 @@ export default function News() {
加载中...
) : infoList.length === 0 ? (
- 暂无{activeTab === 'seek' ? '寻配号' : '成交行情'}信息 + 暂无{activeTab === 'seek' ? '寻配号' : activeTab === 'deal' ? '成交行情' : '一尘看板'}信息
) : (
- {infoList.map(item => { + {activeTab === 'yichen' ? : infoList.map(item => { // 解析正文中的号码特征和联系方式 const content = item.content || '' const featuresMatch = content.match(/号码特征[:\s]*(.+?)(?:\n|$)/) @@ -472,7 +496,7 @@ export default function News() {
{/* 创建日期 + 用户名 */}
- 📅 {formatDate(item.created_at)}  |  👤 {item.user_name || '匿名用户'} + 📅 {formatDate(item.created_at)}  |  👤 {(item.content && item.content.match(/发布者: (.+)/)?.[1]) || item.user_name || '匿名用户'}
{/* 号码特征 */} {features && ( @@ -521,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}条匹配成功 + + )}
)} {/* 正文 - 默认收起,点击展开 */} @@ -613,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 && (