Compare commits

...

3 Commits

Author SHA1 Message Date
龙大 9e237c88a4 添加批量行情解析API和分页功能修复 2026-04-17 00:52:40 +08:00
龙大 446cd32484 修复三个问题:
1. 成交行情标题去掉括号
2. 每页改为500条
3. 管理页面显示行情数并可点击跳转
2026-04-17 00:40:29 +08:00
龙大 65890e775c 分页功能修复:
- 成交行情每页改为300条
- 寻配号每页改为300条
- 后端返回X-Total-Pages头支持分页
2026-04-16 23:43:12 +08:00
6 changed files with 222 additions and 15 deletions

View File

@ -0,0 +1,2 @@
from app.models.deal_info import DealInfo
from app.models.models import User, Collection

View File

@ -110,9 +110,16 @@ def get_deal_list(
# 分页 # 分页
offset = (page - 1) * page_size offset = (page - 1) * page_size
total_count = query.count()
total_pages = (total_count + page_size - 1) // page_size
items = query.offset(offset).limit(page_size).all() items = query.offset(offset).limit(page_size).all()
return items # 返回Response对象以添加自定义头
from fastapi.responses import JSONResponse
return JSONResponse(
content=[DealInfoResponse.model_validate(item).model_dump(mode='json') for item in items],
headers={"X-Total-Pages": str(total_pages), "X-Total-Count": str(total_count)}
)
@router.get("/stats") @router.get("/stats")
def get_deal_stats( def get_deal_stats(

View File

@ -1,5 +1,5 @@
# 资讯API路由 # 资讯API路由
from fastapi import APIRouter, Depends, HTTPException, Query from fastapi import APIRouter, Depends, HTTPException, Query, Body
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
@ -1019,6 +1019,8 @@ def get_seek_list_all(
status: str = Query("active"), status: str = Query("active"),
user_id: Optional[str] = None, user_id: Optional[str] = None,
user_only: bool = Query(False), user_only: bool = Query(False),
page: int = Query(1, ge=1),
page_size: int = Query(100, ge=1, le=1000),
current_user: Optional[User] = Depends(get_current_user), current_user: Optional[User] = Depends(get_current_user),
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
@ -1034,7 +1036,11 @@ def get_seek_list_all(
if user_id: if user_id:
query = query.filter(Information.user_id == user_id) query = query.filter(Information.user_id == user_id)
items = query.order_by(Information.created_at.desc()).all() # 计算总数和分页
total_count = query.count()
total_pages = (total_count + page_size - 1) // page_size
offset = (page - 1) * page_size
items = query.order_by(Information.created_at.desc()).offset(offset).limit(page_size).all()
result = [] result = []
for item in items: for item in items:
@ -1071,4 +1077,184 @@ def get_seek_list_all(
"network_matched_count": network_matched_count "network_matched_count": network_matched_count
}) })
return result from fastapi.responses import JSONResponse
return JSONResponse(
content=result,
headers={"X-Total-Pages": str(total_pages), "X-Total-Count": str(total_count)}
)
def parse_deals_locally(text: str, default_packaging: str = '', default_date: str = '', default_platform: str = ''):
"""本地正则解析批量行情文本"""
import re
from datetime import datetime
results = []
# 尝试从文本中提取日期(可能出现在标题或时间戳中)
# 格式如: 3月29日, 2026年3月29日, 2026-03-29
date_patterns = [
r'(\d{1,2})月(\d{1,2})日',
r'(\d{4})年(\d{1,2})月(\d{1,2})日',
r'(\d{4})-(\d{1,2})-(\d{1,2})'
]
extracted_date = None
for pattern in date_patterns:
match = re.search(pattern, text)
if match:
try:
if len(match.groups()) == 2:
# 3月29日 - 使用当前年份
extracted_date = f"2026-{int(match.group(1)):02d}-{int(match.group(2)):02d}"
elif len(match.groups()) == 3:
if int(match.group(1)) > 2000:
# 2026年3月29日
extracted_date = f"{match.group(1)}-{int(match.group(2)):02d}-{int(match.group(3)):02d}"
else:
# 3月29日格式
extracted_date = f"2026-{int(match.group(1)):02d}-{int(match.group(2)):02d}"
break
except:
pass
# 默认使用今天
default_date = datetime.now().strftime('%Y-%m-%d')
deal_date = extracted_date or default_date
lines = text.strip().split('\n')
for line in lines:
line = line.strip()
if not line or 'J0' not in line:
continue
# 提取冠字号 J0 + 8-9位数字
serial_match = re.search(r'J0(\d{8,9})', line)
if not serial_match:
continue
serial_num = serial_match.group(1)
if len(serial_num) == 9:
serial_num = serial_num[:8]
serial = 'J0' + serial_num
# 提取价格 ¥xxx,xxx 或 xxx,xxx必须在J0之后
serial_pos = line.find(serial)
after_serial = line[serial_pos + len(serial):]
price_match = re.search(r'[¥¥]?\s*([1-9][\d,]{2,})', after_serial)
if not price_match:
continue
price = int(price_match.group(1).replace(',', ''))
# 提取卖家(价格后面的中文字符)
after_price_pos = after_serial.find(price_match.group(0)) + len(price_match.group(0))
after_price = after_serial[after_price_pos:]
seller_match = re.search(r'([^\d\s¥¥]+?)(?:\s*$|\s*\n)', after_price)
seller = seller_match.group(1).strip() if seller_match else ''
# 分类判断
digits = serial_num
d = digits
category = '通货'
if not any(c in d for c in ['1','2','3','4','5','7']): category = '圆圆号'
elif not any(c in d for c in ['2','3','4','5','7']): category = '倒置号'
elif not any(c in d for c in ['1','2','3','4','7']): category = '金马王'
elif not any(c in d for c in ['2','3','4','7']): category = '金马号'
elif not any(c in d for c in ['1','2','4','5','7']): category = '金山王'
elif not any(c in d for c in ['1','2','4','7']): category = '天马王'
elif not any(c in d for c in ['2','4','5','7']): category = '金山号'
elif not any(c in d for c in ['2','4','7']): category = '天马号'
elif not any(c in d for c in ['1','3','4','5','7']): category = '朦胧王'
elif not any(c in d for c in ['3','4','5','7']): category = '朦胧号'
elif not any(c in d for c in ['1','3','4','7']): category = '如意号'
elif not any(c in d for c in ['3','4','7']): category = '钻石号'
elif not any(c in d for c in ['4','7']): category = '永恒号'
elif '4' not in d: category = '带7号'
# 提取评级机构 PCGS/PMG/ACG/爱藏
grade = ''
grading_company = ''
packaging = '单张'
if 'PC69' in line or 'PC68' in line or 'PC67' in line:
grade_match = re.search(r'PC(6[789]|5\d?)', line)
grade = 'PC' + grade_match.group(1) if grade_match else ''
grading_company = 'PCGS'
elif 'PMG68' in line or 'PMG67' in line:
grade_match = re.search(r'PMG(6[789]|5\d?)', line)
grade = 'PMG' + grade_match.group(1) if grade_match else ''
grading_company = 'PMG'
elif 'ACG' in line:
grade_match = re.search(r'ACG(6[789]|5\d?)', line)
grade = 'ACG' + grade_match.group(1) if grade_match else ''
grading_company = 'ACG'
elif '爱藏67+' in line:
grade = '67+'
grading_company = '爱藏'
elif '爱藏67' in line:
grade = '67'
grading_company = '爱藏'
# 判断包装类型
packaging = '单张'
# 如果传入了默认包装类型,先使用默认
if default_packaging:
packaging = default_packaging
# 简化识别:带"刀"字=标百,带"标"字=标十
if '' in line:
packaging = '标百'
elif '' in line:
packaging = '标十'
# 尾号判断如果冠字号尾号是01/11/21/31/41/51/61/71/81/91且有刀/标字样,基本确认是标百
if len(serial_num) >= 2:
tail = serial_num[-2:]
if tail in ['01', '11', '21', '31', '41', '51', '61', '71', '81', '91']:
if '' in line or ('' in line and packaging == '单张'):
packaging = '标百'
packaging = '标百'
# 如果没有刀/标字样但尾号是01且没有其他特征可能是标百
if packaging == '单张' and len(serial_num) >= 2:
tail = serial_num[-2:]
if tail == '01':
# 检查是否在特定语境下
packaging = '标百'
# 计算尾号和大小号
tail_number = ''
size_type = ''
if packaging == '标十' and len(serial_num) >= 2:
tail_number = serial_num[-2:]
size_type = tail_number in ['01','11','21','31','41','51'] and '小号' or '大号'
elif packaging == '标百' and len(serial_num) >= 3:
tail_number = serial_num[-3:]
size_type = tail_number in ['101','201','301','401','501'] and '小号' or '大号'
results.append({
'serial': serial,
'price': price,
'category': category,
'seller': seller,
'packaging': packaging,
'grade': grade,
'grading_company': grading_company,
'deal_date': deal_date or default_date, # 成交时间
'entry_date': default_date, # 录入时间
'is_graded': bool(grade),
'tail_number': tail_number, # 尾号
'size_type': size_type, # 大小号
'platform': default_platform # 平台
})
return results
@router.post("/batch-parse-local")
async def batch_parse_deals_local(request: dict = Body(...)):
"""本地正则解析批量行情文本无需AI"""
text = request.get('text', '')
default_packaging = request.get('defaultPackaging', '')
default_date = request.get('defaultDate', '')
default_platform = request.get('defaultPlatform', '')
results = parse_deals_locally(text, default_packaging, default_date, default_platform)
return {"success": True, "data": results}

View File

@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db from app.core.database import get_db
from app.core.auth import get_current_user from app.core.auth import get_current_user
from app.models.models import User, Collection from app.models.models import User, Collection
from app.models.deal_info import DealInfo
from app.schemas.schemas import UserResponse, UserUpdate from app.schemas.schemas import UserResponse, UserUpdate
router = APIRouter(prefix="/api", tags=["用户"]) router = APIRouter(prefix="/api", tags=["用户"])
@ -97,7 +98,7 @@ def get_users(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取用户列表(仅管理员)""" """获取用户列表(仅管理员)"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="无权访问") raise HTTPException(status_code=403, detail="无权访问")
total = db.query(User).count() total = db.query(User).count()
@ -114,6 +115,8 @@ def get_users(
for u in users: for u in users:
# 统计每个用户的藏品数量 # 统计每个用户的藏品数量
count = db.query(Collection).filter(Collection.f99_91_user_id == u.f99_90_id).count() count = db.query(Collection).filter(Collection.f99_91_user_id == u.f99_90_id).count()
# 统计每个用户的行情数量
deal_count = db.query(DealInfo).filter(DealInfo.user_id == u.f99_90_id).count()
user_list.append({ user_list.append({
"id": u.f99_90_id, "id": u.f99_90_id,
"username": u.f01_01_name, "username": u.f01_01_name,
@ -123,6 +126,7 @@ def get_users(
"user_code": u.user_code, "user_code": u.user_code,
"created_at": u.f99_92_created_at.isoformat() if u.f99_92_created_at else None, "created_at": u.f99_92_created_at.isoformat() if u.f99_92_created_at else None,
"collectionCount": count, "collectionCount": count,
"dealCount": deal_count,
"level": u.f99_94_level, "level": u.f99_94_level,
"aiCount": u.f99_95_ai_count, "aiCount": u.f99_95_ai_count,
"searchCount": u.f99_96_search_count, "searchCount": u.f99_96_search_count,
@ -143,7 +147,7 @@ def get_user(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取单个用户信息""" """获取单个用户信息"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="无权访问") raise HTTPException(status_code=403, detail="无权访问")
user = db.query(User).filter(User.id == user_id).first() user = db.query(User).filter(User.id == user_id).first()
@ -168,7 +172,7 @@ def get_user_collections(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取指定用户的藏品列表""" """获取指定用户的藏品列表"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="无权访问") raise HTTPException(status_code=403, detail="无权访问")
collections = db.query(Collection).filter( collections = db.query(Collection).filter(
@ -185,7 +189,7 @@ def get_user_collection_count(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""获取指定用户的藏品数量""" """获取指定用户的藏品数量"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问") raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
count = db.query(Collection).filter(Collection.user_id == user_id).count() count = db.query(Collection).filter(Collection.user_id == user_id).count()
@ -210,7 +214,7 @@ def update_user(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""更新用户信息(仅管理员)""" """更新用户信息(仅管理员)"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问") raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
user = db.query(User).filter(User.f99_90_id == user_id).first() user = db.query(User).filter(User.f99_90_id == user_id).first()
@ -288,7 +292,7 @@ def delete_user(
db: Session = Depends(get_db) db: Session = Depends(get_db)
): ):
"""删除用户(仅管理员)""" """删除用户(仅管理员)"""
if current_user.role not in ["admin", "editor"]: if not current_user or current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问") raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
# 不能删除自己 # 不能删除自己

View File

@ -221,8 +221,11 @@ export default function Admin() {
</div> </div>
</div> </div>
<div style={{ textAlign: 'right' }}> <div style={{ textAlign: 'right' }}>
<div style={{ color: '#94a3b8', fontSize: '12px' }}>藏品数</div> <div style={{ color: '#94a3b8', fontSize: '12px' }}>藏品数 | 行情数</div>
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => window.location.hash = '#/list?userId=' + user.id} title='点击查看'>{user.collectionCount || 0}</div> <div style={{ display: 'flex', gap: '12px', justifyContent: 'flex-end', alignItems: 'center' }}>
<div style={{ color: '#60a5fa', fontSize: '20px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => window.location.hash = '#/list?userId=' + user.id} title='点击查看藏品'>{user.collectionCount || 0}</div>
<div style={{ color: '#22c55e', fontSize: '20px', fontWeight: 'bold', cursor: 'pointer' }} onClick={() => window.location.hash = '#/news?userId=' + user.id} title='点击查看行情'>{user.dealCount || 0}</div>
</div>
</div> </div>
</div> </div>
{/* 更多字段 */} {/* 更多字段 */}

View File

@ -47,6 +47,10 @@ export default function News() {
const [expandedItems, setExpandedItems] = useState({}) // const [expandedItems, setExpandedItems] = useState({}) //
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
const [urlUserId, setUrlUserId] = useState(() => {
const params = new URLSearchParams(window.location.hash.split("?")[1] || "")
return params.get("userId") || null
})
const [totalPages, setTotalPages] = useState(1) const [totalPages, setTotalPages] = useState(1)
const API_BASE = localStorage.getItem('API_BASE') || '' const API_BASE = localStorage.getItem('API_BASE') || ''
@ -88,12 +92,13 @@ export default function News() {
// 100100 // 100100
if (activeTab === 'deal') { if (activeTab === 'deal') {
url += (url.includes('?') ? '&' : '?') + 'page_size=100' url += (url.includes('?') ? '&' : '?') + 'page_size=500'
if (urlUserId) { url += "&user_id=" + urlUserId }
if (dealDate) { if (dealDate) {
url += '&deal_date=' + dealDate url += '&deal_date=' + dealDate
} }
} else if (activeTab === 'seek') { } else if (activeTab === 'seek') {
url += (url.includes("?") ? "&" : "?") + "page=" + currentPage + "&page_size=100" url += (url.includes("?") ? "&" : "?") + "page=" + currentPage + "&page_size=500"
} }
const res = await fetch(url, { headers }) const res = await fetch(url, { headers })
@ -492,7 +497,7 @@ export default function News() {
</div> </div>
)} )}
<h3 style={{ color: '#fff', marginBottom: '16px' }}> <h3 style={{ color: '#fff', marginBottom: '16px' }}>
{activeTab === 'seek' ? '寻配号信息' : activeTab === 'deal' ? `成交行情信息 (${dealDate})` : '一尘看板'} {activeTab === 'seek' ? '寻配号信息' : activeTab === 'deal' ? `成交行情信息 ${dealDate}` : '一尘看板'}
{activeTab === 'seek' && ( {activeTab === 'seek' && (
<div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}> <div style={{ display: 'flex', gap: '8px', marginLeft: 'auto' }}>
<button onClick={() => { setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部</button> <button onClick={() => { setViewMode('all'); fetchInfoList(); }} style={{ padding: '6px 12px', background: viewMode==='all'?'#3b82f6':'#1e293b', border: '1px solid #334155', borderRadius: '6px', color: '#fff', fontSize: '12px' }}>全部</button>