Compare commits

...

2 Commits

15 changed files with 251 additions and 1485 deletions

1
VERSION Normal file
View File

@ -0,0 +1 @@
1.2.98

48
VERSION.json Normal file
View File

@ -0,0 +1,48 @@
{
"version": "1.2.98",
"updated": "2026-04-19",
"modules": {
"frontend": {
"version": "1.2.98",
"pages": {
"Add": "1.2.90",
"Admin": "1.2.98",
"Detail": "1.2.90",
"Edit": "1.2.90",
"Home": "1.2.95",
"List": "1.2.93",
"Login": "1.2.85",
"News": "1.2.80",
"News_YichensBoard": "1.2.75",
"Settings": "1.2.75",
"Stats": "1.2.70",
"YichensBoard": "1.2.70"
},
"config": {
"version": "1.2.98"
}
},
"backend": {
"version": "1.2.98",
"routers": {
"auth": "1.2.90",
"collections": "1.2.95",
"deal": "1.2.85",
"information": "1.2.92",
"news": "1.2.80",
"ocr": "1.2.75",
"operations": "1.2.70",
"seek": "1.2.70",
"users": "1.2.98",
"yichens": "1.2.70"
},
"app": {
"core": "1.2.0",
"models": "1.2.0",
"schemas": "1.2.0",
"services": "1.2.0",
"utils": "1.2.0"
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,4 @@
# information - 资讯路由
# Version: 0.0.1
# 资讯API路由
from fastapi import APIRouter, Depends, HTTPException, Query, Response, Body
from sqlalchemy.orm import Session, joinedload
from sqlalchemy import text
@ -13,7 +11,6 @@ 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
from app.models.seek_info import SeekInfo
router = APIRouter(prefix="/api/information", tags=["资讯"])
@ -460,7 +457,7 @@ def create_information(
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}%')
@ -1155,25 +1152,50 @@ def get_yichen_posts(category: str = None, search: str = None, page: int = 1, pa
@router.get("/seek/stats")
def get_seek_stats(
current_user = Depends(get_current_user),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取寻配号统计数据 - 使用seek_info表"""
# 寻号需求数seek_info表中status=active的总数
seek_count = db.query(SeekInfo).filter(SeekInfo.status == "active").count()
"""获取寻配号统计数据"""
# 寻号需求数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(SeekInfo).filter(
SeekInfo.user_id == current_user.f99_90_id,
SeekInfo.is_matched.in_(["true", "false"])
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()
# 总共匹配:所有已匹配成功的寻号数量
total_matched_count = db.query(SeekInfo).filter(
SeekInfo.is_matched.in_(["true", "false"])
).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,

View File

@ -1,210 +1,104 @@
# operations - 运营操作路由
# Version: 0.0.1
# 更新:
# 操作路由
from typing import List, Optional
# 更新:
from fastapi import APIRouter, Depends, HTTPException, status, Query
# 更新:
from sqlalchemy.orm import Session
# 更新:
from app.core.database import get_db
# 更新:
from app.core.auth import get_current_user
# 更新:
from app.models.models import User, Collection, Operation
# 更新:
from app.schemas.schemas import OperationCreate, OperationResponse
# 更新:
# 更新:
router = APIRouter(prefix="/api", tags=["操作"])
# 更新:
# 更新:
# 更新:
@router.get("/operations", response_model=List[OperationResponse])
# 更新:
def get_operations(
# 更新:
collection_id: Optional[str] = None,
# 更新:
page: int = Query(1, ge=1),
# 更新:
limit: int = Query(50, ge=1, le=100),
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""获取操作历史"""
# 更新:
query = db.query(Operation).filter(Operation.user_id == current_user.id)
# 更新:
# 更新:
if collection_id:
# 更新:
query = query.filter(Operation.collection_id == collection_id)
# 更新:
# 更新:
operations = query.order_by(Operation.created_at.desc()) \
# 更新:
.offset((page - 1) * limit) \
# 更新:
.limit(limit) \
# 更新:
.all()
# 更新:
# 更新:
return operations
# 更新:
# 更新:
# 更新:
@router.get("/operations/history")
# 更新:
def get_operation_history(
# 更新:
collection_id: Optional[str] = None,
# 更新:
type: Optional[str] = None,
# 更新:
start_date: Optional[str] = None,
# 更新:
end_date: Optional[str] = None,
# 更新:
page: int = Query(1, ge=1),
# 更新:
limit: int = Query(50, ge=1, le=100),
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""获取操作历史(带统计)"""
# 更新:
query = db.query(Operation).filter(Operation.user_id == current_user.id)
# 更新:
# 更新:
if collection_id:
# 更新:
query = query.filter(Operation.collection_id == collection_id)
# 更新:
if type:
# 更新:
query = query.filter(Operation.type == type)
# 更新:
if start_date:
# 更新:
query = query.filter(Operation.created_at >= start_date)
# 更新:
if end_date:
# 更新:
query = query.filter(Operation.created_at <= end_date)
# 更新:
# 更新:
total = query.count()
# 更新:
# 更新:
data = query.order_by(Operation.created_at.desc()) \
# 更新:
.offset((page - 1) * limit) \
# 更新:
.limit(limit) \
# 更新:
.all()
# 更新:
# 更新:
return {
# 更新:
"data": data,
# 更新:
"pagination": {
# 更新:
"page": page,
# 更新:
"limit": limit,
# 更新:
"total": total,
# 更新:
"pages": (total + limit - 1) // limit
# 更新:
}
# 更新:
}
# 更新:
# 更新:
# 更新:
@router.post("/operations", response_model=OperationResponse)
# 更新:
def create_operation(
# 更新:
operation_data: OperationCreate,
# 更新:
current_user: User = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db)
# 更新:
):
# 更新:
"""创建操作记录"""
# 更新:
# 验证藏品存在
# 更新:
collection = db.query(Collection).filter(
# 更新:
Collection.id == operation_data.collection_id,
# 更新:
Collection.user_id == current_user.id
# 更新:
).first()
# 更新:
# 更新:
if not collection:
# 更新:
raise HTTPException(status_code=404, detail="藏品不存在")
# 更新:
# 更新:
operation = Operation(
# 更新:
collection_id=operation_data.collection_id,
# 更新:
user_id=current_user.id,
# 更新:
type=operation_data.type,
# 更新:
price=operation_data.price,
# 更新:
note=operation_data.note
# 更新:
)
# 更新:
# 更新:
db.add(operation)
# 更新:
db.commit()
# 更新:
db.refresh(operation)
# 更新:
# 更新:
return operation
# 更新:

View File

@ -1,17 +1,13 @@
# seek - 寻号匹配路由
# Version: 0.0.1
from fastapi import APIRouter, Depends, Query, HTTPException
# Version: 1.2.x
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional, List
from typing import Optional
from datetime import datetime
from app.core.database import get_db
from app.core.auth import get_current_user
from app.models.seek_info import SeekInfo
from app.models.models import User
from app.routers.information import match_collections_count_from_coolbot
from app.models.models import User, Collection, Information
from sqlalchemy import text
router = APIRouter(prefix="/api/seek", tags=["寻配号"])
@ -40,8 +36,6 @@ class SeekInfoUpdate(BaseModel):
class SeekInfoResponse(BaseModel):
id: str
user_id: str
user_name: Optional[str] = None
network_matched_count: Optional[int] = 0
title: str
content: Optional[str]
expect_category: Optional[str]
@ -58,6 +52,9 @@ class SeekInfoResponse(BaseModel):
contact_count: int
created_at: Optional[datetime]
updated_at: Optional[datetime]
user_name: Optional[str] = None # 发布者用户名
matched_count: Optional[int] = 0 # 自有匹配数量
network_matched_count: Optional[int] = 0 # 网络匹配数量
class Config:
from_attributes = True
@ -86,38 +83,45 @@ def get_seek_list(
offset = (page - 1) * page_size
items = query.offset(offset).limit(page_size).all()
# 关联查询用户名
# 添加用户名和匹配数量
from app.routers.information import match_collections_count, match_collections_count_from_coolbot
result = []
for item in items:
user = db.query(User).filter(User.f99_90_id == item.user_id).first()
user_name = user.f01_01_name if user else None
# 计算网络数据匹配数
user_name = user.f01_01_name if user else '匿名用户'
# 计算匹配数量
matched_count = 0
network_matched_count = 0
if item.expect_number:
if item.expect_number and len(item.expect_number) == 10:
if current_user and current_user.f99_90_id:
matched_count = match_collections_count(db, current_user.f99_90_id, item.expect_number)
network_matched_count = match_collections_count_from_coolbot(item.expect_number)
result.append({
"id": item.id,
"user_id": item.user_id,
"user_name": user_name,
"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,
"network_matched_count": network_matched_count,
"created_at": item.created_at.isoformat() if item.created_at else None,
"updated_at": item.updated_at.isoformat() if item.updated_at else None,
})
# 构建响应
result.append(SeekInfoResponse(
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,
updated_at=item.updated_at,
user_name=user_name,
matched_count=matched_count,
network_matched_count=network_matched_count
))
return result
@ -228,3 +232,70 @@ def delete_seek(
db.commit()
return {"message": "删除成功"}
# 获取自有藏品匹配列表
@router.get("/my-match")
def get_seek_match(
info_id: str,
current_user = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取符合条件的我的藏品推荐"""
if not current_user:
raise HTTPException(status_code=401, detail="请先登录")
info = db.query(SeekInfo).filter(SeekInfo.id == info_id).first()
if not info:
raise HTTPException(status_code=404, detail="寻配号信息不存在")
# 获取用户所有藏品
collections = db.query(Collection).filter(
Collection.f99_91_user_id == current_user.f99_90_id,
Collection.f01_04_status == "in_collection"
).all()
# 按号码特征模式匹配
from app.routers.information import match_pattern
matched = []
if info.expect_number and len(info.expect_number) == 10:
pattern = info.expect_number[2:]
for c in collections:
number = c.f02_10_prefix_serial or ''
if len(number) >= 10 and number.startswith('J0'):
col_pattern = number[2:10]
if match_pattern(col_pattern, pattern):
matched.append(c)
elif len(number) >= 8:
col_pattern = number[:8]
if match_pattern(col_pattern, pattern):
matched.append(c)
else:
matched = collections
return {
"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}
for c in matched[:20]
]
}
# 获取网络数据匹配列表
@router.get("/network-match/{info_id}")
def get_network_match(
info_id: str,
limit: int = Query(20, ge=1, le=100),
db: Session = Depends(get_db)
):
"""获取一尘数据库中匹配的藏品列表"""
from app.routers.information import match_collections_list_from_coolbot
info = db.query(SeekInfo).filter(SeekInfo.id == info_id).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}

View File

@ -7,7 +7,7 @@ from fastapi import APIRouter, Depends, HTTPException, status, Query, Body
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.auth import get_current_user
from app.models.models import User, Collection, Information
from app.models.models import User, Collection
from app.schemas.schemas import UserResponse, UserUpdate
router = APIRouter(prefix="/api", tags=["用户"])
@ -100,7 +100,7 @@ def get_users(
db: Session = Depends(get_db)
):
"""获取用户列表(仅管理员)"""
if not current_user or current_user.role not in ["admin", "editor"]:
if current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="无权访问")
total = db.query(User).count()
@ -152,7 +152,7 @@ def get_user(
db: Session = Depends(get_db)
):
"""获取单个用户信息"""
if not current_user or current_user.role not in ["admin", "editor"]:
if current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="无权访问")
user = db.query(User).filter(User.id == user_id).first()
@ -177,7 +177,7 @@ def get_user_collections(
db: Session = Depends(get_db)
):
"""获取指定用户的藏品列表"""
if not current_user or current_user.role not in ["admin", "editor"]:
if current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="无权访问")
collections = db.query(Collection).filter(
@ -194,7 +194,7 @@ def get_user_collection_count(
db: Session = Depends(get_db)
):
"""获取指定用户的藏品数量"""
if not current_user or current_user.role not in ["admin", "editor"]:
if current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
count = db.query(Collection).filter(Collection.user_id == user_id).count()
@ -219,7 +219,7 @@ def update_user(
db: Session = Depends(get_db)
):
"""更新用户信息(仅管理员)"""
if not current_user or current_user.role not in ["admin", "editor"]:
if current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
user = db.query(User).filter(User.f99_90_id == user_id).first()
@ -297,7 +297,7 @@ def delete_user(
db: Session = Depends(get_db)
):
"""删除用户(仅管理员)"""
if not current_user or current_user.role not in ["admin", "editor"]:
if current_user.role not in ["admin", "editor"]:
raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
# 不能删除自己

View File

@ -1,760 +1,377 @@
# yichens - 一尘数据路由
# Version: 0.0.1
# 更新:
from fastapi import APIRouter, Depends, Query
# 更新:
# Version: 1.2.x
# 更新:
from sqlalchemy import func, text
# 更新:
from sqlalchemy.orm import Session
# 更新:
from pydantic import BaseModel
# 更新:
from typing import Optional, List
# 更新:
from datetime import datetime, date
# 更新:
from app.core.coolbot_db import get_coolbot_db
# 更新:
# 更新:
router = APIRouter(prefix="/api/yichens", tags=["一尘看板"])
# 更新:
# 更新:
# ============ 数据模型 ============
# 更新:
class YichensPostStats(BaseModel):
# 更新:
total_posts: int
# 更新:
total_deals: int # 出售
# 更新:
total_wants: int # 求购
# 更新:
total_replies: int
# 更新:
total_views: int
# 更新:
avg_price: Optional[float]
# 更新:
# 更新:
class CategoryStat(BaseModel):
# 更新:
category: str
# 更新:
count: int
# 更新:
# 更新:
class PostItem(BaseModel):
# 更新:
post_id: str
# 更新:
title: str
# 更新:
category: Optional[str]
# 更新:
post_type: str
# 更新:
price: Optional[float]
# 更新:
author_username: str
# 更新:
post_time: str
# 更新:
reply_count: int
# 更新:
view_count: int
# 更新:
url: Optional[str]
# 更新:
content: Optional[str]
# 更新:
# 更新:
class UserStat(BaseModel):
# 更新:
total_users: int
# 更新:
new_users_today: int
# 更新:
sellers: int
# 更新:
# 更新:
class UserItem(BaseModel):
# 更新:
user_id: str
# 更新:
username: str
# 更新:
avatar_url: Optional[str]
# 更新:
content: Optional[str]
# 更新:
credit_level: Optional[str]
# 更新:
credit_score: Optional[int]
# 更新:
post_count: int
# 更新:
is_seller: bool
# 更新:
registration_date: Optional[str]
# 更新:
# 更新:
# ============ 统计接口 ============
# 更新:
@router.get("/stats/posts", response_model=YichensPostStats)
# 更新:
def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取帖子统计"""
# 更新:
result = db.execute(text("""
# 更新:
SELECT
# 更新:
COUNT(*) as total_posts,
# 更新:
COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals,
# 更新:
COUNT(*) FILTER (WHERE post_type = 'want') as total_wants,
# 更新:
COALESCE(SUM(reply_count), 0) as total_replies,
# 更新:
COALESCE(SUM(view_count), 0) as total_views,
# 更新:
AVG(price) as avg_price
# 更新:
FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 day' * :days
# 更新:
"""), {"days": days}).fetchone()
# 更新:
# 更新:
return YichensPostStats(
# 更新:
total_posts=result[0] or 0,
# 更新:
total_deals=result[1] or 0,
# 更新:
total_wants=result[2] or 0,
# 更新:
total_replies=result[3] or 0,
# 更新:
total_views=result[4] or 0,
# 更新:
avg_price=float(result[5]) if result[5] else None
# 更新:
)
# 更新:
# 更新:
@router.get("/stats/categories", response_model=List[CategoryStat])
# 更新:
def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
# 更新:
"""按分类统计帖子数量"""
# 更新:
results = db.execute(text("""
# 更新:
SELECT category, COUNT(*) as count
# 更新:
FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 day' * :days
# 更新:
GROUP BY category
# 更新:
ORDER BY count DESC
# 更新:
"""), {"days": days}).fetchall()
# 更新:
# 更新:
return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results]
# 更新:
# 更新:
@router.get("/stats/users", response_model=UserStat)
# 更新:
def get_user_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取用户统计"""
# 更新:
result = db.execute(text("""
# 更新:
SELECT
# 更新:
COUNT(*) as total_users,
# 更新:
COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today,
# 更新:
COUNT(*) FILTER (WHERE is_seller = true) as sellers
# 更新:
FROM yichens_users
# 更新:
""")).fetchone()
# 更新:
# 更新:
return UserStat(
# 更新:
total_users=result[0] or 0,
# 更新:
new_users_today=result[1] or 0,
# 更新:
sellers=result[2] or 0
# 更新:
)
# 更新:
# 更新:
@router.get("/posts")
# 更新:
def get_posts(
# 更新:
limit: int = Query(20, ge=1, le=500),
# 更新:
offset: int = Query(0, ge=0),
# 更新:
category: Optional[str] = None,
# 更新:
post_type: Optional[str] = None,
# 更新:
keyword: Optional[str] = None,
# 更新:
db: Session = Depends(get_coolbot_db)
# 更新:
):
# 更新:
"""获取帖子列表 - 支持全局搜索,返回总数和分页信息"""
# 更新:
# 构建WHERE条件
# 更新:
where_clauses = ["1=1"]
# 更新:
params = {"limit": limit, "offset": offset}
# 更新:
# 更新:
if category:
# 更新:
where_clauses.append("category = :category")
# 更新:
params["category"] = category
# 更新:
# 更新:
if post_type:
# 更新:
where_clauses.append("post_type = :post_type")
# 更新:
params["post_type"] = post_type
# 更新:
# 更新:
# 全局搜索
# 更新:
if keyword:
# 更新:
where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)")
# 更新:
params["keyword"] = f"%{keyword}%"
# 更新:
# 更新:
where_sql = " AND ".join(where_clauses)
# 更新:
# 更新:
# 查询总数
# 更新:
count_query = f"SELECT COUNT(*) FROM yichens_posts WHERE {where_sql}"
# 更新:
total_count = db.execute(text(count_query), {k: v for k, v in params.items() if k not in ['limit', 'offset']}).scalar() or 0
# 更新:
# 更新:
# 查询数据 - 有post_time时按post_time排序没有时按crawled_at排序
# 更新:
data_query = f"""
# 更新:
SELECT post_id, title, content, category, post_type, price,
# 更新:
author_username, post_time, reply_count, view_count, url
# 更新:
FROM yichens_posts
# 更新:
WHERE {where_sql}
# 更新:
ORDER BY COALESCE(post_time, crawled_at) DESC LIMIT :limit OFFSET :offset
# 更新:
"""
# 更新:
results = db.execute(text(data_query), params).fetchall()
# 更新:
# 更新:
posts = [PostItem(
# 更新:
post_id=r[0],
# 更新:
title=r[1] or "",
# 更新:
content=r[2] or "",
# 更新:
category=r[3],
# 更新:
post_type=r[4] or "",
# 更新:
price=float(r[5]) if r[5] else None,
# 更新:
author_username=r[6] or "",
# 更新:
post_time=str(r[7]) if r[7] else "",
# 更新:
reply_count=r[8] or 0,
# 更新:
view_count=r[9] or 0,
# 更新:
url=r[10]
# 更新:
) for r in results]
# 更新:
# 更新:
return {
# 更新:
"posts": posts,
# 更新:
"total": total_count,
# 更新:
"page": offset // limit + 1,
# 更新:
"page_size": limit
# 更新:
}
# 更新:
# 更新:
@router.get("/users", response_model=List[UserItem])
# 更新:
def get_users(
# 更新:
limit: int = Query(20, ge=1, le=500),
# 更新:
offset: int = Query(0, ge=0),
# 更新:
is_seller: Optional[bool] = None,
# 更新:
db: Session = Depends(get_coolbot_db)
# 更新:
):
# 更新:
"""获取用户列表"""
# 更新:
query = """
# 更新:
SELECT user_id, username, avatar_url, credit_level, credit_score,
# 更新:
post_count, is_seller, registration_date
# 更新:
FROM yichens_users
# 更新:
WHERE 1=1
# 更新:
"""
# 更新:
params = {"limit": limit, "offset": offset}
# 更新:
# 更新:
if is_seller is not None:
# 更新:
query += " AND is_seller = :is_seller"
# 更新:
params["is_seller"] = is_seller
# 更新:
# 更新:
query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset"
# 更新:
# 更新:
results = db.execute(text(query), params).fetchall()
# 更新:
# 更新:
return [UserItem(
# 更新:
user_id=r[0],
# 更新:
username=r[1] or "",
# 更新:
avatar_url=r[2],
# 更新:
credit_level=r[3],
# 更新:
credit_score=r[4],
# 更新:
post_count=r[5] or 0,
# 更新:
is_seller=r[6] or False,
# 更新:
registration_date=str(r[7]) if r[7] else None
# 更新:
) for r in results]
# 更新:
# 更新:
# 更新:
@router.get("/stats/today")
# 更新:
async def get_today_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取今日新增帖子统计"""
# 更新:
query = """
# 更新:
SELECT
# 更新:
COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants,
# 更新:
SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes,
# 更新:
SUM(CASE WHEN title LIKE '%天马%' OR content LIKE '%天马%' THEN 1 ELSE 0 END) as tianma
# 更新:
FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE
# 更新:
"""
# 更新:
result = db.execute(text(query)).fetchone()
# 更新:
return {
# 更新:
"total": result[0] or 0,
# 更新:
"deals": result[1] or 0,
# 更新:
"wants": result[2] or 0,
# 更新:
"others": result[3] or 0,
# 更新:
"dragons": result[4] or 0,
# 更新:
"horses": result[5] or 0,
# 更新:
"snakes": result[6] or 0,
# 更新:
"tianma": result[7] or 0
# 更新:
}
# 更新:
# 更新:
@router.get("/stats/hour")
# 更新:
async def get_hour_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取近一个小时新增帖子统计"""
# 更新:
query = """
# 更新:
SELECT
# 更新:
COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants,
# 更新:
SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes
# 更新:
FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 hour'
# 更新:
"""
# 更新:
result = db.execute(text(query)).fetchone()
# 更新:
return {
# 更新:
"total": result[0] or 0,
# 更新:
"deals": result[1] or 0,
# 更新:
"wants": result[2] or 0,
# 更新:
"others": result[3] or 0,
# 更新:
"dragons": result[3] or 0,
# 更新:
"horses": result[4] or 0,
# 更新:
"snakes": result[5] or 0
# 更新:
}
# 更新:
# 更新:
@router.get("/stats/today-category")
# 更新:
async def get_today_category_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取今日帖子分类统计"""
# 更新:
query = """
# 更新:
SELECT category, COUNT(*) as count
# 更新:
FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE
# 更新:
GROUP BY category
# 更新:
ORDER BY count DESC
# 更新:
"""
# 更新:
results = db.execute(text(query)).fetchall()
# 更新:
return [{"category": r[0] or "未分类", "count": r[1]} for r in results]
# 更新:
# 更新:
# 更新:
@router.get("/stats/dragons-today")
# 更新:
def get_dragons_stats_today(
# 更新:
db: Session = Depends(get_coolbot_db)
# 更新:
):
# 更新:
"""获取今日龙钞详细统计数据(按号码分类)- 就高不就低"""
# 更新:
from sqlalchemy import text
# 更新:
# 更新:
# 1. 带4包含"带4"、"带四"、"通货"
# 更新:
dai4 = db.execute(text("""
# 更新:
SELECT
# 更新:
COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%'
# 更新:
AND (
# 更新:
content LIKE '%带4%' OR title LIKE '%带4%'
# 更新:
OR content LIKE '%带四%' OR title LIKE '%带四%'
# 更新:
OR content LIKE '%通货%' OR title LIKE '%通货%'
# 更新:
)
# 更新:
""")).fetchone()
# 更新:
# 更新:
# 2. 无4包含"无4"、"无四",排除"无47"、"无247"、"无四七"、"永恒"
# 更新:
wu4 = db.execute(text("""
# 更新:
SELECT
# 更新:
COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%'
# 更新:
AND (
# 更新:
content LIKE '%无4%' OR title LIKE '%无4%'
# 更新:
OR content LIKE '%无四%' OR title LIKE '%无四%'
# 更新:
)
# 更新:
AND content NOT LIKE '%无47%' AND title NOT LIKE '%无47%'
# 更新:
AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%'
# 更新:
AND content NOT LIKE '%永恒%' AND title NOT LIKE '%永恒%'
# 更新:
AND content NOT LIKE '%无四七%' AND title NOT LIKE '%无四七%'
# 更新:
""")).fetchone()
# 更新:
# 更新:
# 3. 无47包含"无47"、"永恒"、"无四七",排除"无247"
# 更新:
wu47 = db.execute(text("""
# 更新:
SELECT
# 更新:
COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%'
# 更新:
AND (
# 更新:
content LIKE '%无47%' OR title LIKE '%无47%'
# 更新:
OR content LIKE '%永恒%' OR title LIKE '%永恒%'
# 更新:
OR content LIKE '%无四七%' OR title LIKE '%无四七%'
# 更新:
)
# 更新:
AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%'
# 更新:
""")).fetchone()
# 更新:
# 更新:
# 4. 无247包含"无247"、"天马"、"金山",排除"无347"
# 更新:
wu247 = db.execute(text("""
# 更新:
SELECT
# 更新:
COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%'
# 更新:
AND (
# 更新:
content LIKE '%无247%' OR title LIKE '%无247%'
# 更新:
OR content LIKE '%天马%' OR title LIKE '%天马%'
# 更新:
OR content LIKE '%金山%' OR title LIKE '%金山%'
# 更新:
)
# 更新:
AND content NOT LIKE '%无347%' AND title NOT LIKE '%无347%'
# 更新:
""")).fetchone()
# 更新:
# 更新:
# 5. 无347包含"无347"、"钻石"、"金马"、"魅力"、"朦胧"
# 更新:
wu347 = db.execute(text("""
# 更新:
SELECT
# 更新:
COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
# 更新:
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%'
# 更新:
AND (
# 更新:
content LIKE '%无347%' OR title LIKE '%无347%'
# 更新:
OR content LIKE '%钻石%' OR title LIKE '%钻石%'
# 更新:
OR content LIKE '%金马%' OR title LIKE '%金马%'
# 更新:
OR content LIKE '%魅力%' OR title LIKE '%魅力%'
# 更新:
OR content LIKE '%朦胧%' OR title LIKE '%朦胧%'
# 更新:
)
# 更新:
""")).fetchone()
# 更新:
# 更新:
return {
# 更新:
"dai4": {"total": dai4[0] or 0, "deals": dai4[1] or 0, "wants": dai4[2] or 0},
# 更新:
"wu4": {"total": wu4[0] or 0, "deals": wu4[1] or 0, "wants": wu4[2] or 0},
# 更新:
"wu47": {"total": wu47[0] or 0, "deals": wu47[1] or 0, "wants": wu47[2] or 0},
# 更新:
"wu247": {"total": wu247[0] or 0, "deals": wu247[1] or 0, "wants": wu247[2] or 0},
# 更新:
"wu347": {"total": wu347[0] or 0, "deals": wu347[1] or 0, "wants": wu347[2] or 0}
# 更新:
}
# 更新:
# 更新:

View File

@ -1,6 +1,6 @@
# Pydantic Schema - 使用字段编码并支持 camelCase
from typing import Optional, List
from pydantic import BaseModel, EmailStr, Field
from pydantic import BaseModel, EmailStr, Field, ConfigDict
from datetime import datetime
@ -14,9 +14,7 @@ class UserBase(BaseModel):
address: Optional[str] = None
bio: Optional[str] = None
class Config:
from_attributes = True
populate_by_name = True
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
class UserCreate(UserBase):
@ -33,9 +31,7 @@ class UserUpdate(BaseModel):
bio: Optional[str] = None
password: Optional[str] = None
class Config:
from_attributes = True
populate_by_name = True
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
class UserResponse(UserBase):
@ -63,9 +59,7 @@ class UserResponse(UserBase):
f99_92_created_at: Optional[datetime] = Field(None, alias="created_at")
f99_93_updated_at: Optional[datetime] = Field(None, alias="updated_at")
class Config:
from_attributes = True
populate_by_name = True
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
# ============ 藏品相关 ============
@ -110,9 +104,7 @@ class CollectionBase(BaseModel):
# f06 其他信息
f06_50_purpose: Optional[str] = Field(None, alias="purpose")
class Config:
from_attributes = True
populate_by_name = True
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
class CollectionCreate(CollectionBase):
@ -159,9 +151,7 @@ class CollectionUpdate(BaseModel):
# f06 其他信息
f06_50_purpose: Optional[str] = Field(None, alias="purpose")
class Config:
from_attributes = True
populate_by_name = True
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
class CollectionImageResponse(BaseModel):
@ -171,8 +161,7 @@ class CollectionImageResponse(BaseModel):
path: Optional[str] = None
f99_92_created_at: Optional[datetime] = None
class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
class CollectionResponse(CollectionBase):
@ -182,9 +171,7 @@ class CollectionResponse(CollectionBase):
f99_93_updated_at: Optional[datetime] = Field(None, alias="updatedAt")
images: List[CollectionImageResponse] = []
class Config:
from_attributes = True
populate_by_name = True
model_config = ConfigDict(from_attributes=True, populate_by_name=True)
class CollectionListResponse(BaseModel):
@ -209,8 +196,7 @@ class OperationResponse(OperationBase):
f99_91_user_id: str
f99_93_created_at: Optional[datetime] = None
class Config:
from_attributes = True
model_config = ConfigDict(from_attributes=True)
# ============ OCR 相关 ============

View File

@ -1 +1 @@
1.2.98
VERSION=v0.0.5

6
frontend/fix_version.js Normal file
View File

@ -0,0 +1,6 @@
const fs = require('fs');
const htmlPath = './index.html';
let html = fs.readFileSync(htmlPath, 'utf-8');
html = html.replace('v=0.0.3', 'v=0.0.4');
fs.writeFileSync(htmlPath, html);
console.log('Done');

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>甲辰收藏 v=1.2.97</title>
<title>甲辰收藏 v=0.0.5</title>
<!-- Favicon -->
<link rel="icon" type="image/png" href="/static/icons/favicon.png" />

View File

@ -3,7 +3,7 @@
// 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx
// 从环境变量读取vite.config.js 注入)
export const APP_VERSION = import.meta.env.APP_VERSION || '1.2.82'
export const APP_VERSION = import.meta.env.APP_VERSION || '0.0.3'
// 版本信息
export const VERSION_INFO = {

View File

@ -1,7 +1,7 @@
/**
* Home - 首页
* Version: 0.0.3
* 更新修复寻配号stats接口调用2026-04-20
* Version: 0.0.2
* 更新快捷操作改为黑色背景2026-04-20
*/
import React, { useState, useEffect } from 'react'
@ -84,7 +84,7 @@ export default function Home() {
}).catch(() => {})
//
fetch('/api/information/seek/stats?_=1776700744').then(res => res.json()).then(data => {
fetch('/api/information/seek/stats').then(res => res.json()).then(data => {
setSeekStats(data || {})
}).catch(() => {})
}, [])

View File

@ -1,7 +1,7 @@
/**
* News - 资讯列表页面
* Version: 0.0.2
* 更新修复寻配号API调用使用/api/seek接口2026-04-20
* Version: 0.0.1
* 更新
*/
import React, { useState, useEffect } from 'react'
@ -71,9 +71,10 @@ export default function News() {
fetchInfoList()
}, [activeTab, dealDate])
//
// 8
useEffect(() => {
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${seekForm.features || 'XXXXXXXX'}`
const features = (seekForm.features || '').slice(0, 8).padEnd(8, 'X')
const title = `「寻号 ${seekForm.edition || '龙钞,马钞,蛇钞'} J0${features}`
setSeekForm(prev => ({...prev, title}))
}, [seekForm.edition, seekForm.features])
@ -274,7 +275,7 @@ export default function News() {
//
const fetchNetworkMatchCollections = async (infoId) => {
try {
const res = await fetch(`${API_BASE}/api/information/seek/network-match/${infoId}`)
const res = await fetch(`${API_BASE}/api/seek/network-match/${infoId}`)
const data = await res.json()
console.log('网络数据匹配结果:', data)
setNetworkMatchCollections(data.collections || [])
@ -330,7 +331,7 @@ export default function News() {
try {
//
const content = (editingSeek.content || '') + '\n联系方式: ' + editingSeek.contact
await fetch(`${API_BASE}/api/seek/${editingSeek.id}`, {
await fetch(`${API_BASE}/api/information/${editingSeek.id}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
@ -352,14 +353,16 @@ export default function News() {
const content = (seekForm.content ? seekForm.content + '\n' : '') + (seekForm.price ? `价格: ${seekForm.price}` : '') + (seekForm.features ? '\n号码特征: J0' + seekForm.features : '') + '\n联系方式: ' + finalContact
try {
const token = localStorage.getItem('token')
// /api/seek
const res = await fetch(`${API_BASE}/api/seek`, {
// editioncategory
const categoryMap = { '龙钞': '龙钞', '马钞': '马钞', '蛇钞': '蛇钞' }
const res = await fetch(`${API_BASE}/api/information/`, {
method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
title: seekForm.title,
content,
info_type: 'seek',
expect_category: seekForm.edition,
expect_number: seekForm.features ? 'J0' + seekForm.features : null
expect_number: seekForm.features ? 'J0' + seekForm.features : None
})
})
const data = await res.json()
@ -368,7 +371,7 @@ export default function News() {
setShowSeekPublish(false)
setSeekForm({ edition: '龙钞', type: '', category: '寻号', price: '', matchType: '模糊', features: '', content: '', title: '', contact: getUserPhone() || '' })
fetchInfoList()
} else { alert(data.detail || data.message || '发布失败') }
} else { alert(data.message || '发布失败') }
} catch (e) { alert('发布失败: ' + e.message) }
}
@ -449,12 +452,22 @@ export default function News() {
onChange={(e) => {
if (i < 2) return
const val = e.target.value.toUpperCase().replace(/[^0-9XABCDEFG]/g, '')
const newFeatures = (seekForm.features || '').split('')
if (!val) {
//
const newFeatures = (seekForm.features || '').split('')
newFeatures[i - 2] = ''
setSeekForm({...seekForm, features: newFeatures.join('').slice(0, 8)})
return
}
// 8
let currentLen = (seekForm.features || '').length
if (currentLen >= 8 && i - 2 >= currentLen) return // 8
const newFeatures = (seekForm.features || '').slice(0, 8).split('')
while (newFeatures.length < 8) newFeatures.push('')
newFeatures[i - 2] = val
setSeekForm({...seekForm, features: newFeatures.join('')})
setSeekForm({...seekForm, features: newFeatures.join('').slice(0, 8)})
//
if (val && i < 9) {
if (i < 9) {
setTimeout(() => {
const nextInput = document.querySelector(`input[data-index="${i+1}"]`)
if (nextInput) nextInput.focus()
@ -750,7 +763,7 @@ export default function News() {
</div>
{/* 创建日期 + 用户名 */}
<div style={{ color: '#64748b', fontSize: '12px', marginBottom: '8px' }}>
📅 {formatDate(item.created_at)} &nbsp;|&nbsp; 👤 {(item.content && item.content.match(/发布者: (.+)/)?.[1]) || item.user_name || '匿名用户'}
📅 {formatDate(item.created_at)} &nbsp;|&nbsp; 👤 {item.user_name || '匿名用户'}
</div>
{/* 号码特征 */}
{features && (