jiachenlong/backend/app/routers/information.py

2949 lines
70 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# information - 资讯路由
# Version: 1.2.92
# 更新:
from fastapi import APIRouter, Depends, HTTPException, Query, Response, Body
# 更新:
from sqlalchemy.orm import Session, joinedload
# 更新:
from sqlalchemy import text
# 更新:
from typing import List, Optional
# 更新:
from pydantic import BaseModel
# 更新:
from datetime import datetime, date
# 更新:
import os
# 更新:
# 更新:
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=["资讯"])
# 更新:
# 更新:
# 更新:
# Schema
# 更新:
class InformationCreate(BaseModel):
# 更新:
info_type: str # seek-寻配号, deal-成交数据, publish-发布
# 更新:
title: str
# 更新:
content: Optional[str]
# 更新:
collection_id: Optional[str] = None
# 更新:
expect_category: Optional[str] = None
# 更新:
expect_version: Optional[str] = None
# 更新:
expect_packaging: Optional[str] = None
# 更新:
expect_number: Optional[str] = None
# 更新:
expect_price_min: Optional[float] = None
# 更新:
expect_price_max: Optional[float] = None
# 更新:
deal_price: Optional[float] = None
# 更新:
deal_date: Optional[date] = None
# 更新:
packaging: Optional[str] = None
# 更新:
is_graded: Optional[bool] = False
# 更新:
grading_company: Optional[str] = None
# 更新:
grading_score: Optional[str] = None
# 更新:
category: Optional[str] = None
# 更新:
deal_no: Optional[str] = None
# 更新:
# 更新:
# 更新:
class InformationUpdate(BaseModel):
# 更新:
title: Optional[str] = None
# 更新:
content: Optional[str] = None
# 更新:
status: Optional[str] = None
# 更新:
expect_category: Optional[str] = None
# 更新:
expect_version: Optional[str] = None
# 更新:
expect_packaging: Optional[str] = None
# 更新:
expect_number: Optional[str] = None
# 更新:
expect_price_min: Optional[float] = None
# 更新:
expect_price_max: Optional[float] = None
# 更新:
deal_price: Optional[float] = None
# 更新:
deal_date: Optional[date] = None
# 更新:
packaging: Optional[str] = None
# 更新:
is_graded: Optional[bool] = None
# 更新:
grading_company: Optional[str] = None
# 更新:
grading_score: Optional[str] = None
# 更新:
# 更新:
# 更新:
class InformationResponse(BaseModel):
# 更新:
id: str
# 更新:
user_id: str
# 更新:
info_type: str
# 更新:
title: str
# 更新:
content: Optional[str]
# 更新:
collection_id: Optional[str]
# 更新:
expect_category: Optional[str]
# 更新:
expect_version: Optional[str]
# 更新:
expect_packaging: Optional[str]
# 更新:
expect_number: Optional[str]
# 更新:
expect_price_min: Optional[float]
# 更新:
expect_price_max: Optional[float]
# 更新:
deal_price: Optional[float]
# 更新:
deal_date: Optional[date]
# 更新:
status: str
# 更新:
is_matched: Optional[str] = "pending"
# 更新:
matched_user_id: Optional[str] = None
# 更新:
matched_contact: Optional[str] = None
# 更新:
view_count: int
# 更新:
contact_count: int
# 更新:
created_at: datetime
# 更新:
# 评级相关字段
# 更新:
packaging: Optional[str] = None
# 更新:
is_graded: Optional[bool] = False
# 更新:
grading_company: Optional[str] = None
# 更新:
grading_score: Optional[str] = None
# 更新:
category: Optional[str] = None
# 更新:
deal_no: Optional[str] = None
# 更新:
# 用户信息
# 更新:
user_name: Optional[str] = None
# 更新:
user_avatar: Optional[str] = None
# 更新:
# 关联藏品信息
# 更新:
collection_name: Optional[str] = None
# 更新:
collection_category: Optional[str] = None
# 更新:
collection_version: Optional[str] = None
# 更新:
collection_number: Optional[str] = None
# 更新:
# 匹配数量(我的藏品中满足条件的数量)
# 更新:
matched_count: Optional[int] = 0
# 更新:
# 网络数据匹配数量coolbot_data数据库中满足条件的数量
# 更新:
network_matched_count: Optional[int] = 0
# 更新:
# 更新:
class Config:
# 更新:
from_attributes = True
# 更新:
# 更新:
# 更新:
# 资讯列表
# 更新:
@router.get("/list", response_model=List[InformationResponse])
# 更新:
def get_information_list(
# 更新:
info_type: Optional[str] = Query(None, description="类型: seek/deal/publish"),
# 更新:
status: str = Query("active", description="状态: active/closed/expired"),
# 更新:
user_id: Optional[str] = Query(None, description="用户ID用于获取该用户的行情"),
# 更新:
deal_date: Optional[str] = Query(None, description="成交日期过滤格式YYYY-MM-DD"),
# 更新:
page: int = Query(1, ge=1),
# 更新:
page_size: int = Query(20, ge=1, le=500),
# 更新:
current_user: Optional[User] = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db),
# 更新:
response: Response = None
# 更新:
):
# 更新:
"""获取资讯列表(公开,无需登录)"""
# 更新:
query = db.query(Information).options(
# 更新:
joinedload(Information.user),
# 更新:
joinedload(Information.collection)
# 更新:
).filter(Information.status == status)
# 更新:
# 更新:
if info_type:
# 更新:
query = query.filter(Information.info_type == info_type)
# 更新:
# 更新:
# 如果传入了user_id只返回该用户的行情
# 更新:
if user_id:
# 更新:
query = query.filter(Information.user_id == user_id)
# 更新:
# 更新:
# 成交日期过滤
# 更新:
if deal_date:
# 更新:
from datetime import date
# 更新:
deal_date_obj = date.fromisoformat(deal_date)
# 更新:
query = query.filter(Information.deal_date == deal_date_obj)
# 更新:
# 更新:
# 按创建时间倒序
# 更新:
query = query.order_by(Information.deal_date.desc().nullslast(), Information.deal_no.desc().nullslast())
# 更新:
# 更新:
# 分页
# 更新:
offset = (page - 1) * page_size
# 更新:
items = query.offset(offset).limit(page_size).all()
# 更新:
# 更新:
# 转换结果
# 更新:
result = []
# 更新:
for item in items:
# 更新:
# 计算匹配数量仅对seek类型且用户登录时
# 更新:
matched_count = 0
# 更新:
if item.info_type == 'seek' and item.expect_number and current_user:
# 更新:
matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number)
# 更新:
# 更新:
result.append(InformationResponse(
# 更新:
id=item.id,
# 更新:
user_id=item.user_id,
# 更新:
info_type=item.info_type,
# 更新:
title=item.title,
# 更新:
content=item.content,
# 更新:
collection_id=item.collection_id,
# 更新:
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,
# 更新:
deal_price=item.deal_price,
# 更新:
deal_date=item.deal_date,
# 更新:
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,
# 更新:
user_name=item.user.f01_01_name if item.user else None,
# 更新:
user_avatar=item.user.avatar if item.user else None,
# 更新:
collection_name=item.collection.f01_01_name if item.collection else None,
# 更新:
collection_category=item.collection.f01_03_category if item.collection else None,
# 更新:
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,
# 更新:
packaging=item.packaging,
# 更新:
is_graded=item.is_graded or False,
# 更新:
grading_company=item.grading_company,
# 更新:
grading_score=item.grading_score,
# 更新:
category=item.category,
# 更新:
deal_no=item.deal_no,
# 更新:
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
# 更新:
# 更新:
# 更新:
def match_collections_count(db: Session, user_id: str, expect_number: str) -> int:
# 更新:
"""根据号码特征计算匹配藏品数量"""
# 更新:
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
# 更新:
# 更新:
# 获取用户所有藏品
# 更新:
collections = db.query(Collection).filter(
# 更新:
Collection.f99_91_user_id == user_id,
# 更新:
Collection.f01_04_status == "in_collection"
# 更新:
).all()
# 更新:
# 更新:
count = 0
# 更新:
for c in collections:
# 更新:
number = c.f02_10_prefix_serial or ''
# 更新:
# 去掉J0前缀后取前8位
# 更新:
if len(number) >= 10 and number.startswith('J0'):
# 更新:
col_pattern = number[2:10]
# 更新:
if match_pattern(col_pattern, pattern):
# 更新:
count += 1
# 更新:
elif len(number) >= 8:
# 更新:
col_pattern = number[:8]
# 更新:
if match_pattern(col_pattern, pattern):
# 更新:
count += 1
# 更新:
# 更新:
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 = 任意数字
# 更新:
# A = 非4
# 更新:
# B = 非47
# 更新:
# C = 非347
# 更新:
# D = 非247
# 更新:
# E = 非2347
# 更新:
# F = 非23457
# 更新:
# G = 非123457
# 更新:
# 更新:
# 注意col_number已经是去掉J0前缀后的8位号码不需要再处理
# 更新:
col_num = col_number
# 更新:
# 更新:
for i, p in enumerate(pattern):
# 更新:
if i >= len(col_num):
# 更新:
return False
# 更新:
# 更新:
c = col_num[i]
# 更新:
# 更新:
if p == 'X':
# 更新:
if not c.isdigit():
# 更新:
return False
# 更新:
elif p == 'A':
# 更新:
if c == '4':
# 更新:
return False
# 更新:
elif p == 'B':
# 更新:
if c in '47':
# 更新:
return False
# 更新:
elif p == 'C':
# 更新:
if c in '347':
# 更新:
return False
# 更新:
elif p == 'D':
# 更新:
if c in '247':
# 更新:
return False
# 更新:
elif p == 'E':
# 更新:
if c in '2347':
# 更新:
return False
# 更新:
elif p == 'F':
# 更新:
if c in '23457':
# 更新:
return False
# 更新:
elif p == 'G':
# 更新:
if c in '123457':
# 更新:
return False
# 更新:
else:
# 更新:
# 数字或字母必须完全匹配
# 更新:
if p != c:
# 更新:
return False
# 更新:
# 更新:
return True
# 更新:
# 更新:
# 更新:
# 获取单条资讯
# 更新:
@router.get("/{info_id}", response_model=InformationResponse)
# 更新:
def get_information(
# 更新:
info_id: str,
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""获取资讯详情"""
# 更新:
item = db.query(Information).options(
# 更新:
joinedload(Information.user),
# 更新:
joinedload(Information.collection)
# 更新:
).filter(Information.id == info_id).first()
# 更新:
# 更新:
if not item:
# 更新:
raise HTTPException(status_code=404, detail="资讯不存在")
# 更新:
# 更新:
# 增加浏览次数
# 更新:
item.view_count += 1
# 更新:
db.commit()
# 更新:
# 更新:
return InformationResponse(
# 更新:
id=item.id,
# 更新:
user_id=item.user_id,
# 更新:
info_type=item.info_type,
# 更新:
title=item.title,
# 更新:
content=item.content,
# 更新:
collection_id=item.collection_id,
# 更新:
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,
# 更新:
deal_price=item.deal_price,
# 更新:
deal_date=item.deal_date,
# 更新:
status=item.status,
# 更新:
view_count=item.view_count,
# 更新:
contact_count=item.contact_count,
# 更新:
created_at=item.created_at,
# 更新:
packaging=item.packaging,
# 更新:
is_graded=item.is_graded or False,
# 更新:
grading_company=item.grading_company,
# 更新:
grading_score=item.grading_score,
# 更新:
category=item.category,
# 更新:
user_name=item.user.f01_01_name if item.user else None,
# 更新:
user_avatar=item.user.avatar if item.user else None,
# 更新:
collection_name=item.collection.f01_01_name if item.collection else None,
# 更新:
collection_category=item.collection.f01_03_category if item.collection else None,
# 更新:
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,
# 更新:
)
# 更新:
# 更新:
# 更新:
# 发布资讯
# 更新:
@router.post("/", response_model=InformationResponse)
# 更新:
def create_information(
# 更新:
data: InformationCreate,
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""发布资讯"""
# 更新:
# 生成行情编号:日期 + 5位自然数从00001开始
# 更新:
deal_no = None
# 更新:
if data.info_type == 'deal':
# 更新:
today = datetime.now().strftime('%Y%m%d')
# 更新:
# 查询当天已有行情数量
# 更新:
from app.models.models import Information
# 更新:
count_today = db.query(Information).filter(
# 更新:
Information.info_type == 'deal',
# 更新:
Information.deal_no.like(f'DJ{today}%')
# 更新:
).count()
# 更新:
# 编号 = 日期 + 5位自然数如 DJ2026041100001
# 更新:
seq = count_today + 1
# 更新:
deal_no = f"{today[2:]}{seq:04d}"
# 更新:
# 更新:
info = Information(
# 更新:
user_id=current_user.f99_90_id,
# 更新:
info_type=data.info_type,
# 更新:
title=data.title,
# 更新:
content=data.content,
# 更新:
collection_id=data.collection_id,
# 更新:
expect_category=data.expect_category,
# 更新:
expect_version=data.expect_version,
# 更新:
expect_packaging=data.expect_packaging,
# 更新:
expect_number=data.expect_number,
# 更新:
expect_price_min=data.expect_price_min,
# 更新:
expect_price_max=data.expect_price_max,
# 更新:
deal_price=data.deal_price,
# 更新:
deal_date=data.deal_date,
# 更新:
packaging=data.packaging,
# 更新:
is_graded=data.is_graded or False,
# 更新:
grading_company=data.grading_company,
# 更新:
grading_score=data.grading_score,
# 更新:
category=data.category,
# 更新:
deal_no=deal_no,
# 更新:
status="active"
# 更新:
)
# 更新:
db.add(info)
# 更新:
db.commit()
# 更新:
db.refresh(info)
# 更新:
# 更新:
return InformationResponse(
# 更新:
id=info.id,
# 更新:
user_id=info.user_id,
# 更新:
info_type=info.info_type,
# 更新:
title=info.title,
# 更新:
content=info.content,
# 更新:
collection_id=info.collection_id,
# 更新:
expect_category=info.expect_category,
# 更新:
expect_version=info.expect_version,
# 更新:
expect_packaging=info.expect_packaging,
# 更新:
expect_number=info.expect_number,
# 更新:
expect_price_min=info.expect_price_min,
# 更新:
expect_price_max=info.expect_price_max,
# 更新:
deal_price=info.deal_price,
# 更新:
deal_date=info.deal_date,
# 更新:
status=info.status,
# 更新:
view_count=info.view_count,
# 更新:
contact_count=info.contact_count,
# 更新:
created_at=info.created_at,
# 更新:
user_name=current_user.f01_01_name,
# 更新:
user_avatar=current_user.avatar,
# 更新:
collection_name=None,
# 更新:
collection_category=None,
# 更新:
collection_version=None,
# 更新:
collection_number=None,
# 更新:
)
# 更新:
# 更新:
# 更新:
# 更新资讯
# 更新:
@router.put("/{info_id}", response_model=InformationResponse)
# 更新:
def update_information(
# 更新:
info_id: str,
# 更新:
data: InformationUpdate,
# 更新:
current_user: Optional[User] = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""更新资讯"""
# 更新:
if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 处理f99_90_id为None的情况
# 更新:
user_filter = current_user.f99_90_id if current_user and current_user.f99_90_id else Information.user_id
# 更新:
info = db.query(Information).filter(
# 更新:
Information.id == info_id,
# 更新:
Information.user_id == user_filter
# 更新:
).first()
# 更新:
# 更新:
if not info:
# 更新:
raise HTTPException(status_code=404, detail="资讯不存在或无权修改")
# 更新:
# 更新:
# 更新字段
# 更新:
if data.title is not None:
# 更新:
info.title = data.title
# 更新:
if data.content is not None:
# 更新:
info.content = data.content
# 更新:
if data.status is not None:
# 更新:
info.status = data.status
# 更新:
if data.expect_category is not None:
# 更新:
info.expect_category = data.expect_category
# 更新:
if data.expect_version is not None:
# 更新:
info.expect_version = data.expect_version
# 更新:
if data.expect_packaging is not None:
# 更新:
info.expect_packaging = data.expect_packaging
# 更新:
if data.expect_number is not None:
# 更新:
info.expect_number = data.expect_number
# 更新:
if data.expect_price_min is not None:
# 更新:
info.expect_price_min = data.expect_price_min
# 更新:
if data.expect_price_max is not None:
# 更新:
info.expect_price_max = data.expect_price_max
# 更新:
if data.deal_price is not None:
# 更新:
info.deal_price = data.deal_price
# 更新:
if data.deal_date is not None:
# 更新:
info.deal_date = data.deal_date
# 更新:
if data.packaging is not None:
# 更新:
info.packaging = data.packaging
# 更新:
if data.is_graded is not None:
# 更新:
info.is_graded = data.is_graded
# 更新:
if data.grading_company is not None:
# 更新:
info.grading_company = data.grading_company
# 更新:
if data.grading_score is not None:
# 更新:
info.grading_score = data.grading_score
# 更新:
# 更新:
db.commit()
# 更新:
db.refresh(info)
# 更新:
# 更新:
return InformationResponse(
# 更新:
id=info.id,
# 更新:
user_id=info.user_id,
# 更新:
info_type=info.info_type,
# 更新:
title=info.title,
# 更新:
content=info.content,
# 更新:
collection_id=info.collection_id,
# 更新:
expect_category=info.expect_category,
# 更新:
expect_version=info.expect_version,
# 更新:
expect_packaging=info.expect_packaging,
# 更新:
expect_number=info.expect_number,
# 更新:
expect_price_min=info.expect_price_min,
# 更新:
expect_price_max=info.expect_price_max,
# 更新:
deal_price=info.deal_price,
# 更新:
deal_date=info.deal_date,
# 更新:
status=info.status,
# 更新:
view_count=info.view_count,
# 更新:
contact_count=info.contact_count,
# 更新:
created_at=info.created_at,
# 更新:
packaging=info.packaging,
# 更新:
is_graded=info.is_graded or False,
# 更新:
grading_company=info.grading_company,
# 更新:
grading_score=info.grading_score,
# 更新:
category=info.category,
# 更新:
user_name=current_user.f01_01_name,
# 更新:
user_avatar=current_user.avatar,
# 更新:
collection_name=info.collection.f01_01_name if info.collection else None,
# 更新:
collection_category=info.collection.f01_03_category if info.collection else None,
# 更新:
collection_version=info.collection.f02_11_version if info.collection else None,
# 更新:
collection_number=info.collection.f02_10_prefix_serial if info.collection else None,
# 更新:
)
# 更新:
# 更新:
# 更新:
# 删除资讯
# 更新:
@router.delete("/{info_id}")
# 更新:
def delete_information(
# 更新:
info_id: str,
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""删除资讯"""
# 更新:
# 允许admin删除任何人的资讯
# 更新:
if current_user.role == "admin":
# 更新:
info = db.query(Information).filter(Information.id == info_id).first()
# 更新:
else:
# 更新:
info = db.query(Information).filter(
# 更新:
Information.id == info_id,
# 更新:
Information.user_id == current_user.f99_90_id
# 更新:
).first()
# 更新:
# 更新:
if not info:
# 更新:
raise HTTPException(status_code=404, detail="资讯不存在或无权删除")
# 更新:
# 更新:
db.delete(info)
# 更新:
db.commit()
# 更新:
# 更新:
return {"message": "删除成功"}
# 更新:
# 更新:
# 更新:
# 寻配号 - 自动匹配推荐藏品
# 更新:
@router.get("/seek/match")
# 更新:
def get_seek_match(
# 更新:
info_id: str,
# 更新:
current_user: Optional[User] = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""获取符合条件的我的藏品推荐"""
# 更新:
if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
info = db.query(Information).filter(
# 更新:
Information.id == info_id,
# 更新:
Information.info_type == "seek"
# 更新:
).first()
# 更新:
# 更新:
if not info:
# 更新:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 更新:
# 更新:
# 更新用户配号(寻号)次数
# 更新:
current_user.f99_96_search_count = (current_user.f99_96_search_count or 0) + 1
# 更新:
db.commit()
# 更新:
# 更新:
# 获取用户所有藏品
# 更新:
collections = db.query(Collection).filter(
# 更新:
Collection.f99_91_user_id == current_user.f99_90_id,
# 更新:
Collection.f01_04_status == "in_collection"
# 更新:
).all()
# 更新:
# 更新:
# 去掉版别筛选,因为藏品分类和发布需求的版别不同
# 更新:
# if info.expect_category:
# 更新:
# collections = [c for c in collections if c.f01_03_category == info.expect_category]
# 更新:
# 更新:
# 按号码特征模式匹配
# 更新:
matched = []
# 更新:
if info.expect_number and len(info.expect_number) == 10:
# 更新:
pattern = info.expect_number[2:] # 后8位
# 更新:
for c in collections:
# 更新:
number = c.f02_10_prefix_serial or ''
# 更新:
# 去掉J0前缀后取前8位
# 更新:
if len(number) >= 10 and number.startswith('J0'):
# 更新:
col_pattern = number[2:10] # 取J0后面的8位
# 更新:
if match_pattern(col_pattern, pattern):
# 更新:
matched.append(c)
# 更新:
elif len(number) >= 8:
# 更新:
col_pattern = number[:8] # 取前8位
# 更新:
if match_pattern(col_pattern, pattern):
# 更新:
matched.append(c)
# 更新:
else:
# 更新:
matched = collections
# 更新:
# 更新:
return {
# 更新:
"info_id": info_id,
# 更新:
"matched_count": len(matched),
# 更新:
"collections": [
# 更新:
{
# 更新:
"id": c.f99_90_id,
# 更新:
"code": c.f01_02_code or '',
# 更新:
"name": c.f01_01_name,
# 更新:
"number": c.f02_10_prefix_serial,
# 更新:
"status": c.f01_04_status,
# 更新:
"category": c.f01_03_category,
# 更新:
"version": c.f02_11_version,
# 更新:
"packaging": c.f02_12_packaging,
# 更新:
"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
# 更新:
}
# 更新:
# 更新:
# 更新:
# 我的寻号列表
# 更新:
@router.get("/my-seeks")
# 更新:
def get_my_seeks(
# 更新:
current_user: Optional[User] = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""获取当前用户发布的所有寻号信息"""
# 更新:
if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
items = db.query(Information).filter(
# 更新:
Information.user_id == current_user.f99_90_id,
# 更新:
Information.info_type == "seek",
# 更新:
Information.status == "active"
# 更新:
).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, item.expect_number)
# 更新:
# 更新:
result.append(InformationResponse(
# 更新:
id=item.id,
# 更新:
user_id=item.user_id,
# 更新:
info_type=item.info_type,
# 更新:
title=item.title,
# 更新:
content=item.content,
# 更新:
collection_id=item.collection_id,
# 更新:
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,
# 更新:
deal_price=item.deal_price,
# 更新:
deal_date=item.deal_date,
# 更新:
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,
# 更新:
user_name=item.user.f01_01_name if item.user else None,
# 更新:
user_avatar=item.user.avatar if item.user else None,
# 更新:
collection_name=item.collection.f01_01_name if item.collection else None,
# 更新:
collection_category=item.collection.f01_03_category if item.collection else None,
# 更新:
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
# 更新:
# 更新:
# 更新:
# 成交数据统计
# 更新:
@router.get("/deal/stats")
# 更新:
def get_deal_stats(
# 更新:
days: int = Query(7, ge=1, le=90, description="统计天数"),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""获取成交数据统计"""
# 更新:
from sqlalchemy import func
# 更新:
from datetime import timedelta
# 更新:
# 更新:
start_date = datetime.now() - timedelta(days=days)
# 更新:
# 更新:
# 按版别统计
# 更新:
by_version = db.query(
# 更新:
Information.expect_version,
# 更新:
func.count(Information.id).label("count"),
# 更新:
func.avg(Information.deal_price).label("avg_price"),
# 更新:
func.max(Information.deal_price).label("max_price"),
# 更新:
func.min(Information.deal_price).label("min_price")
# 更新:
).filter(
# 更新:
Information.info_type == "deal",
# 更新:
Information.status == "active",
# 更新:
Information.created_at >= start_date
# 更新:
).group_by(Information.expect_version).all()
# 更新:
# 更新:
# 按包装统计
# 更新:
by_packaging = db.query(
# 更新:
Information.expect_packaging,
# 更新:
func.count(Information.id).label("count"),
# 更新:
func.avg(Information.deal_price).label("avg_price")
# 更新:
).filter(
# 更新:
Information.info_type == "deal",
# 更新:
Information.status == "active",
# 更新:
Information.created_at >= start_date
# 更新:
).group_by(Information.expect_packaging).all()
# 更新:
# 更新:
# 按号码分类统计
# 更新:
by_number = db.query(
# 更新:
Information.expect_number,
# 更新:
func.count(Information.id).label("count"),
# 更新:
func.avg(Information.deal_price).label("avg_price")
# 更新:
).filter(
# 更新:
Information.info_type == "deal",
# 更新:
Information.status == "active",
# 更新:
Information.expect_number.isnot(None),
# 更新:
Information.created_at >= start_date
# 更新:
).group_by(Information.expect_number).all()
# 更新:
# 更新:
return {
# 更新:
"days": days,
# 更新:
"by_version": [
# 更新:
{"version": r[0] or "未知", "count": r[1], "avg_price": float(r[2] or 0), "max_price": float(r[3] or 0), "min_price": float(r[4] or 0)}
# 更新:
for r in by_version if r[0]
# 更新:
],
# 更新:
"by_packaging": [
# 更新:
{"packaging": r[0] or "未知", "count": r[1], "avg_price": float(r[2] or 0)}
# 更新:
for r in by_packaging if r[0]
# 更新:
],
# 更新:
"by_number": [
# 更新:
{"number": r[0], "count": r[1], "avg_price": float(r[2] or 0)}
# 更新:
for r in by_number
# 更新:
]
# 更新:
}
# 更新:
# 更新:
# 更新:
# 获取我的发布列表
# 更新:
@router.get("/my/list", response_model=List[InformationResponse])
# 更新:
def get_my_information_list(
# 更新:
page: int = Query(1, ge=1),
# 更新:
page_size: int = Query(20, ge=1, le=500),
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""获取我的发布列表"""
# 更新:
items = db.query(Information).options(
# 更新:
joinedload(Information.collection)
# 更新:
).filter(
# 更新:
Information.user_id == current_user.f99_90_id
# 更新:
).order_by(Information.created_at.desc()).offset((page-1)*page_size).limit(page_size).all()
# 更新:
# 更新:
result = []
# 更新:
for item in items:
# 更新:
result.append(InformationResponse(
# 更新:
id=item.id,
# 更新:
user_id=item.user_id,
# 更新:
info_type=item.info_type,
# 更新:
title=item.title,
# 更新:
content=item.content,
# 更新:
collection_id=item.collection_id,
# 更新:
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,
# 更新:
deal_price=item.deal_price,
# 更新:
deal_date=item.deal_date,
# 更新:
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,
# 更新:
user_name=current_user.f01_01_name,
# 更新:
user_avatar=current_user.avatar,
# 更新:
collection_name=item.collection.f01_01_name if item.collection else None,
# 更新:
collection_category=item.collection.f01_03_category if item.collection else None,
# 更新:
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,
# 更新:
))
# 更新:
# 更新:
return result
# 更新:
# 更新:
# ============ 获取当前用户发布的列表 ============
# 更新:
@router.get("/my")
# 更新:
def get_my_information(
# 更新:
page: int = Query(1, ge=1),
# 更新:
limit: int = Query(20, ge=1, le=100),
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""获取当前用户发布的信息列表"""
# 更新:
total = db.query(Information).filter(Information.author == current_user.f01_01_name).count()
# 更新:
infos = db.query(Information).filter(
# 更新:
Information.author == current_user.f01_01_name
# 更新:
).order_by(Information.created_at.desc()).offset((page-1)*limit).limit(limit).all()
# 更新:
# 更新:
return {
# 更新:
"total": total,
# 更新:
"list": [{
# 更新:
"id": i.id,
# 更新:
"title": i.title,
# 更新:
"content": i.content,
# 更新:
"info_type": i.info_type,
# 更新:
"author": i.author,
# 更新:
"created_at": i.created_at.isoformat() if i.created_at else None
# 更新:
} for i in infos]
# 更新:
}
# 更新:
# 更新:
# 更新:
# ============ 匹配寻号 ============
# 更新:
class MatchSeekRequest(BaseModel):
# 更新:
info_id: str
# 更新:
collection_id: Optional[str] = None
# 更新:
contact: Optional[str] = None
# 更新:
# 更新:
# 更新:
@router.post("/seek/match-confirm")
# 更新:
def match_seek(
# 更新:
request: MatchSeekRequest,
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""确认匹配寻号 - 用户愿意交换联系方式给发布者"""
# 更新:
info = db.query(Information).filter(
# 更新:
Information.id == request.info_id,
# 更新:
Information.info_type == "seek",
# 更新:
Information.status == "active"
# 更新:
).first()
# 更新:
# 更新:
if not info:
# 更新:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 更新:
# 更新:
# 检查是否已被匹配(一个寻号只能被一个用户匹配)
# 更新:
if info.is_matched == "matched":
# 更新:
raise HTTPException(status_code=400, detail="该寻号已被其他用户匹配")
# 更新:
# 更新:
# 更新匹配状态
# 更新:
info.is_matched = "matched"
# 更新:
info.matched_user_id = current_user.f99_90_id
# 更新:
# 保存匹配者的联系方式
# 更新:
info.matched_contact = request.contact or ''
# 更新:
# 更新:
# 更新发布寻号者的内容,显示有藏品被匹配
# 更新:
original_content = info.content or ""
# 更新:
# 添加匹配信息藏品被XX藏友匹配联系方式为xxx
# 更新:
match_info = f"\n藏品被{current_user.f01_01_name}匹配成功!联系方式:{request.collection_id or '已交换'}"
# 更新:
info.content = original_content + match_info
# 更新:
# 更新:
db.commit()
# 更新:
# 更新:
return {"message": "匹配成功,已通知发布者", "is_matched": "matched"}
# 更新:
# 更新:
# 更新:
# ============ 添加留言 ============
# 更新:
class CommentRequest(BaseModel):
# 更新:
information_id: str
# 更新:
content: str
# 更新:
# 更新:
# 更新:
@router.post("/comment")
# 更新:
def add_comment(
# 更新:
request: CommentRequest,
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""添加留言"""
# 更新:
info = db.query(Information).filter(
# 更新:
Information.id == request.information_id
# 更新:
).first()
# 更新:
# 更新:
if not info:
# 更新:
raise HTTPException(status_code=404, detail="资讯不存在")
# 更新:
# 更新:
# 创建留言
# 更新:
from app.models.models import InformationComment
# 更新:
comment = InformationComment(
# 更新:
information_id=request.information_id,
# 更新:
user_id=current_user.f99_90_id,
# 更新:
content=request.content
# 更新:
)
# 更新:
db.add(comment)
# 更新:
db.commit()
# 更新:
# 更新:
return {
# 更新:
"message": "留言成功",
# 更新:
"comment": {
# 更新:
"id": comment.id,
# 更新:
"content": comment.content,
# 更新:
"user_name": current_user.f01_01_name,
# 更新:
"user_avatar": current_user.avatar,
# 更新:
"created_at": comment.created_at
# 更新:
}
# 更新:
}
# 更新:
# 更新:
# 更新:
# ============ 获取评论列表 ============
# 更新:
@router.get("/comments/{information_id}")
# 更新:
def get_comments(
# 更新:
information_id: str,
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""获取资讯的评论列表"""
# 更新:
from app.models.models import InformationComment
# 更新:
comments = db.query(InformationComment).filter(
# 更新:
InformationComment.information_id == information_id
# 更新:
).order_by(InformationComment.created_at.desc()).all()
# 更新:
# 更新:
return [
# 更新:
{
# 更新:
"id": c.id,
# 更新:
"content": c.content,
# 更新:
"user_name": c.user.f01_01_name if c.user else '匿名用户',
# 更新:
"user_avatar": c.user.avatar if c.user else None,
# 更新:
"created_at": c.created_at
# 更新:
}
# 更新:
for c in comments
# 更新:
]
# 更新:
# 更新:
# 更新:
# ============ 获取匹配者信息 ============
# 更新:
@router.get("/seek/matched-user/{info_id}")
# 更新:
def get_matched_user(
# 更新:
info_id: str,
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
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 info.user_id != current_user.f99_90_id:
# 更新:
raise HTTPException(status_code=403, detail="无权访问")
# 更新:
# 更新:
if not info.matched_user_id:
# 更新:
return {"message": "暂无匹配者"}
# 更新:
# 更新:
# 获取匹配者信息
# 更新:
matched_user = db.query(User).filter(User.f99_90_id == info.matched_user_id).first()
# 更新:
if not matched_user:
# 更新:
return {"message": "匹配者不存在"}
# 更新:
# 更新:
return {
# 更新:
"matched_user_id": info.matched_user_id,
# 更新:
"user_name": matched_user.f01_01_name,
# 更新:
"phone": matched_user.phone,
# 更新:
"matched_contact": info.matched_contact,
# 更新:
"matched_at": info.updated_at.isoformat() if info.updated_at else None
# 更新:
}
# 更新:
# 更新:
# 更新:
# ============ 获取发布者信息 ============
# 更新:
@router.get("/seek/publisher/{info_id}")
# 更新:
def get_publisher_info(
# 更新:
info_id: str,
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
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.matched_user_id or info.matched_user_id != current_user.f99_90_id:
# 更新:
raise HTTPException(status_code=403, detail="无权访问")
# 更新:
# 更新:
# 获取发布者信息
# 更新:
publisher = db.query(User).filter(User.f99_90_id == info.user_id).first()
# 更新:
if not publisher:
# 更新:
return {"message": "发布者不存在"}
# 更新:
# 更新:
# 从content中解析联系方式
# 更新:
contact = ''
# 更新:
if info.content:
# 更新:
import re
# 更新:
match = re.search(r'联系方式[:\s]*(.+?)(?:\n|$)', info.content)
# 更新:
if match:
# 更新:
contact = match.group(1).strip()
# 更新:
# 更新:
return {
# 更新:
"user_id": info.user_id,
# 更新:
"user_name": publisher.f01_01_name,
# 更新:
"phone": publisher.phone,
# 更新:
"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}}
# 更新:
# 更新:
# 更新:
@router.get("/seek/stats")
# 更新:
def get_seek_stats(
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""获取寻配号统计数据"""
# 更新:
# 寻号需求数seek类型且expect_number不为空的总数
# 更新:
seek_count = db.query(Information).filter(
# 更新:
Information.info_type == 'seek',
# 更新:
Information.expect_number.isnot(None),
# 更新:
Information.expect_number != ''
# 更新:
).count()
# 更新:
# 更新:
# 我的匹配:自有藏品匹配成功的寻号帖子数量
# 更新:
# 即 is_matched = 'confirmed' 的记录用户ID等于当前用户
# 更新:
user_matched_count = 0
# 更新:
if current_user:
# 更新:
user_matched_count = db.query(Information).filter(
# 更新:
Information.info_type == 'seek',
# 更新:
Information.expect_number.isnot(None),
# 更新:
Information.expect_number != '',
# 更新:
Information.matched_user_id == current_user.f99_90_id,
# 更新:
Information.is_matched == 'confirmed'
# 更新:
).count()
# 更新:
# 更新:
# 总共匹配:自有匹配成功 + 网络数据匹配成功
# 更新:
# 自有匹配成功is_matched = 'confirmed'
# 更新:
# 网络数据匹配成功查询每个帖子的network_matched_count并求和
# 更新:
seeks = db.query(Information).filter(
# 更新:
Information.info_type == 'seek',
# 更新:
Information.expect_number.isnot(None),
# 更新:
Information.expect_number != ''
# 更新:
).all()
# 更新:
# 更新:
total_self_matched = 0
# 更新:
total_network_matched = 0
# 更新:
for seek in seeks:
# 更新:
# 自身匹配成功
# 更新:
if seek.is_matched == 'confirmed':
# 更新:
total_self_matched += 1
# 更新:
# 网络数据匹配成功通过coolbot数据库查询
# 更新:
if seek.expect_number:
# 更新:
network_count = match_collections_count_from_coolbot(seek.expect_number)
# 更新:
total_network_matched += network_count
# 更新:
# 更新:
total_matched_count = total_self_matched + total_network_matched
# 更新:
# 更新:
return {
# 更新:
"seekCount": seek_count,
# 更新:
"userMatchedCount": user_matched_count,
# 更新:
"totalMatchedCount": total_matched_count
# 更新:
}
# 更新:
# 更新:
# 批量解析行情数据API
# 更新:
@router.post("/batch-parse")
# 更新:
async def batch_parse_deals(text: str = Body(..., embed=True)):
# 更新:
"""使用AI智能解析批量行情文本"""
# 更新:
import httpx
# 更新:
import json
# 更新:
import re
# 更新:
# 更新:
# 使用阿里云百炼Coding Plan API
# 更新:
api_key = "sk-sp-d5ce68bb203e48ca857c2aea25255b26"
# 更新:
base_url = "https://coding.dashscope.aliyuncs.com/v1"
# 更新:
# 更新:
# 更详细的解析提示词
# 更新:
prompt = f"""你是一个专业的龙钞行情数据提取助手。请从以下文本中提取所有龙钞行情记录。
# 更新:
# 更新:
【解析规则】
# 更新:
1. 每条记录格式:冠字号 价格 评级/包装 出售者
# 更新:
2. 冠字号J0开头的9位数字如J0298810101
# 更新:
3. 价格¥xxx,xxx 格式,去掉逗号转为数字
# 更新:
4. 评级/包装PC69/PMG68/爱藏67+/爱藏67 标十 标百 单张
# 更新:
5. 出售者:人名
# 更新:
6. 号码分类:根据冠字号数字特征判断(圆圆号/倒置号/金马王/金马号/金山王/天马王/金山号/天马号/朦胧王/朦胧号/如意号/钻石号/永恒号/带7号/带4号
# 更新:
# 更新:
【输出格式】
# 更新:
返回JSON数组每条记录包含
# 更新:
- serial: 冠字号完整9位如J0298810101
# 更新:
- price: 价格(数字)
# 更新:
- grade: 评级如PC69, PMG68, 爱藏67+, 爱藏67
# 更新:
- packaging: 包装类型(标十/标百/单张)
# 更新:
- category: 号码分类
# 更新:
- seller: 出售者
# 更新:
- date: 交易日从文本中提取日期如2026-03-29
# 更新:
# 更新:
只返回JSON数组不要其他内容。
# 更新:
# 更新:
文本:
# 更新:
{text}"""
# 更新:
# 更新:
try:
# 更新:
async with httpx.AsyncClient(timeout=120.0) as client:
# 更新:
response = await client.post(
# 更新:
f"{base_url}/chat/completions",
# 更新:
json={
# 更新:
"model": "qwen3.6-plus",
# 更新:
"messages": [
# 更新:
{"role": "system", "content": "你是一个专业的收藏品行情数据提取助手擅长从文本中提取结构化的交易数据。只返回JSON数组。"},
# 更新:
{"role": "user", "content": prompt}
# 更新:
],
# 更新:
"temperature": 0.1
# 更新:
},
# 更新:
headers={
# 更新:
"Authorization": f"Bearer {api_key}",
# 更新:
"Content-Type": "application/json"
# 更新:
}
# 更新:
)
# 更新:
# 更新:
if response.status_code != 200:
# 更新:
return {"success": False, "error": f"API错误: {response.status_code}, {response.text[:200]}"}
# 更新:
# 更新:
result = response.json()
# 更新:
# 阿里云百炼OpenAI兼容格式
# 更新:
choices = result.get("choices", [])
# 更新:
content = ""
# 更新:
if choices and len(choices) > 0:
# 更新:
content = choices[0].get("message", {}).get("content", "")
# 更新:
# 更新:
# 解析JSON
# 更新:
try:
# 更新:
# 尝试提取JSON
# 更新:
if "```json" in content:
# 更新:
content = content.split("```json")[1].split("```")[0]
# 更新:
elif "```" in content:
# 更新:
content = content.split("```")[1].split("```")[0]
# 更新:
# 更新:
# 尝试直接解析
# 更新:
data = json.loads(content.strip())
# 更新:
return {"success": True, "data": data}
# 更新:
except json.JSONDecodeError:
# 更新:
# 尝试用正则提取
# 更新:
match = re.search(r'\[.*\]', content, re.DOTALL)
# 更新:
if match:
# 更新:
try:
# 更新:
data = json.loads(match.group())
# 更新:
return {"success": True, "data": data}
# 更新:
except:
# 更新:
pass
# 更新:
return {"success": False, "error": "解析失败", "raw": content[:500]}
# 更新:
# 更新:
except Exception as e:
# 更新:
return {"success": False, "error": str(e)}
# 更新:
# 更新:
# 本地正则解析函数
# 更新:
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}
# 更新: