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 - 资讯路由 # 资讯API路由
# Version: 0.0.1
from fastapi import APIRouter, Depends, HTTPException, Query, Response, Body from fastapi import APIRouter, Depends, HTTPException, Query, Response, Body
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from sqlalchemy import text 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.auth import get_current_user
from app.core.coolbot_db import coolbot_engine from app.core.coolbot_db import coolbot_engine
from app.models.models import User, Information, Collection from app.models.models import User, Information, Collection
from app.models.seek_info import SeekInfo
router = APIRouter(prefix="/api/information", tags=["资讯"]) router = APIRouter(prefix="/api/information", tags=["资讯"])
@ -460,7 +457,7 @@ def create_information(
if data.info_type == 'deal': if data.info_type == 'deal':
today = datetime.now().strftime('%Y%m%d') today = datetime.now().strftime('%Y%m%d')
# 查询当天已有行情数量 # 查询当天已有行情数量
from app.models.models import Information
count_today = db.query(Information).filter( count_today = db.query(Information).filter(
Information.info_type == 'deal', Information.info_type == 'deal',
Information.deal_no.like(f'DJ{today}%') 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") @router.get("/seek/stats")
def 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) db: Session = Depends(get_db)
): ):
"""获取寻配号统计数据 - 使用seek_info表""" """获取寻配号统计数据"""
# 寻号需求数seek_info表中status=active的总数 # 寻号需求数seek类型且expect_number不为空的总数
seek_count = db.query(SeekInfo).filter(SeekInfo.status == "active").count() 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 user_matched_count = 0
if current_user: if current_user:
user_matched_count = db.query(SeekInfo).filter( user_matched_count = db.query(Information).filter(
SeekInfo.user_id == current_user.f99_90_id, Information.info_type == 'seek',
SeekInfo.is_matched.in_(["true", "false"]) Information.expect_number.isnot(None),
Information.expect_number != '',
Information.matched_user_id == current_user.f99_90_id,
Information.is_matched == 'confirmed'
).count() ).count()
# 总共匹配:所有已匹配成功的寻号数量 # 总共匹配:自有匹配成功 + 网络数据匹配成功
total_matched_count = db.query(SeekInfo).filter( # 自有匹配成功is_matched = 'confirmed'
SeekInfo.is_matched.in_(["true", "false"]) # 网络数据匹配成功查询每个帖子的network_matched_count并求和
).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 { return {
"seekCount": seek_count, "seekCount": seek_count,

View File

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

View File

@ -1,17 +1,13 @@
# seek - 寻号匹配路由
# Version: 0.0.1
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
# Version: 1.2.x
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional, List from typing import Optional
from datetime import datetime from datetime import datetime
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.seek_info import SeekInfo from app.models.seek_info import SeekInfo
from app.models.models import User from app.models.models import User, Collection, Information
from app.routers.information import match_collections_count_from_coolbot from sqlalchemy import text
router = APIRouter(prefix="/api/seek", tags=["寻配号"]) router = APIRouter(prefix="/api/seek", tags=["寻配号"])
@ -40,8 +36,6 @@ class SeekInfoUpdate(BaseModel):
class SeekInfoResponse(BaseModel): class SeekInfoResponse(BaseModel):
id: str id: str
user_id: str user_id: str
user_name: Optional[str] = None
network_matched_count: Optional[int] = 0
title: str title: str
content: Optional[str] content: Optional[str]
expect_category: Optional[str] expect_category: Optional[str]
@ -58,6 +52,9 @@ class SeekInfoResponse(BaseModel):
contact_count: int contact_count: int
created_at: Optional[datetime] created_at: Optional[datetime]
updated_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: class Config:
from_attributes = True from_attributes = True
@ -86,38 +83,45 @@ def get_seek_list(
offset = (page - 1) * page_size offset = (page - 1) * page_size
items = query.offset(offset).limit(page_size).all() items = query.offset(offset).limit(page_size).all()
# 关联查询用户名 # 添加用户名和匹配数量
from app.routers.information import match_collections_count, match_collections_count_from_coolbot
result = [] result = []
for item in items: for item in items:
user = db.query(User).filter(User.f99_90_id == item.user_id).first() 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 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) network_matched_count = match_collections_count_from_coolbot(item.expect_number)
result.append({ # 构建响应
"id": item.id, result.append(SeekInfoResponse(
"user_id": item.user_id, id=item.id,
"user_name": user_name, user_id=item.user_id,
"title": item.title, title=item.title,
"content": item.content, content=item.content,
"expect_category": item.expect_category, expect_category=item.expect_category,
"expect_version": item.expect_version, expect_version=item.expect_version,
"expect_packaging": item.expect_packaging, expect_packaging=item.expect_packaging,
"expect_number": item.expect_number, expect_number=item.expect_number,
"expect_price_min": item.expect_price_min, expect_price_min=item.expect_price_min,
"expect_price_max": item.expect_price_max, expect_price_max=item.expect_price_max,
"status": item.status, status=item.status,
"is_matched": item.is_matched, is_matched=item.is_matched,
"matched_user_id": item.matched_user_id, matched_user_id=item.matched_user_id,
"matched_contact": item.matched_contact, matched_contact=item.matched_contact,
"view_count": item.view_count, view_count=item.view_count,
"contact_count": item.contact_count, contact_count=item.contact_count,
"network_matched_count": network_matched_count, created_at=item.created_at,
"created_at": item.created_at.isoformat() if item.created_at else None, updated_at=item.updated_at,
"updated_at": item.updated_at.isoformat() if item.updated_at else None, user_name=user_name,
}) matched_count=matched_count,
network_matched_count=network_matched_count
))
return result return result
@ -228,3 +232,70 @@ def delete_seek(
db.commit() db.commit()
return {"message": "删除成功"} 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 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, Information from app.models.models import User, Collection
from app.schemas.schemas import UserResponse, UserUpdate from app.schemas.schemas import UserResponse, UserUpdate
router = APIRouter(prefix="/api", tags=["用户"]) router = APIRouter(prefix="/api", tags=["用户"])
@ -100,7 +100,7 @@ def get_users(
db: Session = Depends(get_db) 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="无权访问") raise HTTPException(status_code=403, detail="无权访问")
total = db.query(User).count() total = db.query(User).count()
@ -152,7 +152,7 @@ def get_user(
db: Session = Depends(get_db) 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="无权访问") 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()
@ -177,7 +177,7 @@ def get_user_collections(
db: Session = Depends(get_db) 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="无权访问") raise HTTPException(status_code=403, detail="无权访问")
collections = db.query(Collection).filter( collections = db.query(Collection).filter(
@ -194,7 +194,7 @@ def get_user_collection_count(
db: Session = Depends(get_db) 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: 仅管理员可访问") 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()
@ -219,7 +219,7 @@ def update_user(
db: Session = Depends(get_db) 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: 仅管理员可访问") 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()
@ -297,7 +297,7 @@ def delete_user(
db: Session = Depends(get_db) 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: 仅管理员可访问") raise HTTPException(status_code=403, detail="E00050: 仅管理员可访问")
# 不能删除自己 # 不能删除自己

View File

@ -1,760 +1,377 @@
# yichens - 一尘数据路由
# Version: 0.0.1
# 更新:
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
# 更新:
# Version: 1.2.x
# 更新:
from sqlalchemy import func, text from sqlalchemy import func, text
# 更新:
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
# 更新:
from pydantic import BaseModel from pydantic import BaseModel
# 更新:
from typing import Optional, List from typing import Optional, List
# 更新:
from datetime import datetime, date from datetime import datetime, date
# 更新:
from app.core.coolbot_db import get_coolbot_db from app.core.coolbot_db import get_coolbot_db
# 更新:
# 更新:
router = APIRouter(prefix="/api/yichens", tags=["一尘看板"]) router = APIRouter(prefix="/api/yichens", tags=["一尘看板"])
# 更新:
# 更新:
# ============ 数据模型 ============ # ============ 数据模型 ============
# 更新:
class YichensPostStats(BaseModel): class YichensPostStats(BaseModel):
# 更新:
total_posts: int total_posts: int
# 更新:
total_deals: int # 出售 total_deals: int # 出售
# 更新:
total_wants: int # 求购 total_wants: int # 求购
# 更新:
total_replies: int total_replies: int
# 更新:
total_views: int total_views: int
# 更新:
avg_price: Optional[float] avg_price: Optional[float]
# 更新:
# 更新:
class CategoryStat(BaseModel): class CategoryStat(BaseModel):
# 更新:
category: str category: str
# 更新:
count: int count: int
# 更新:
# 更新:
class PostItem(BaseModel): class PostItem(BaseModel):
# 更新:
post_id: str post_id: str
# 更新:
title: str title: str
# 更新:
category: Optional[str] category: Optional[str]
# 更新:
post_type: str post_type: str
# 更新:
price: Optional[float] price: Optional[float]
# 更新:
author_username: str author_username: str
# 更新:
post_time: str post_time: str
# 更新:
reply_count: int reply_count: int
# 更新:
view_count: int view_count: int
# 更新:
url: Optional[str] url: Optional[str]
# 更新:
content: Optional[str] content: Optional[str]
# 更新:
# 更新:
class UserStat(BaseModel): class UserStat(BaseModel):
# 更新:
total_users: int total_users: int
# 更新:
new_users_today: int new_users_today: int
# 更新:
sellers: int sellers: int
# 更新:
# 更新:
class UserItem(BaseModel): class UserItem(BaseModel):
# 更新:
user_id: str user_id: str
# 更新:
username: str username: str
# 更新:
avatar_url: Optional[str] avatar_url: Optional[str]
# 更新:
content: Optional[str] content: Optional[str]
# 更新:
credit_level: Optional[str] credit_level: Optional[str]
# 更新:
credit_score: Optional[int] credit_score: Optional[int]
# 更新:
post_count: int post_count: int
# 更新:
is_seller: bool is_seller: bool
# 更新:
registration_date: Optional[str] registration_date: Optional[str]
# 更新:
# 更新:
# ============ 统计接口 ============ # ============ 统计接口 ============
# 更新:
@router.get("/stats/posts", response_model=YichensPostStats) @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)): def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取帖子统计""" """获取帖子统计"""
# 更新:
result = db.execute(text(""" result = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total_posts, COUNT(*) as total_posts,
# 更新:
COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals, COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals,
# 更新:
COUNT(*) FILTER (WHERE post_type = 'want') as total_wants, COUNT(*) FILTER (WHERE post_type = 'want') as total_wants,
# 更新:
COALESCE(SUM(reply_count), 0) as total_replies, COALESCE(SUM(reply_count), 0) as total_replies,
# 更新:
COALESCE(SUM(view_count), 0) as total_views, COALESCE(SUM(view_count), 0) as total_views,
# 更新:
AVG(price) as avg_price AVG(price) as avg_price
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 day' * :days WHERE post_time >= NOW() - INTERVAL '1 day' * :days
# 更新:
"""), {"days": days}).fetchone() """), {"days": days}).fetchone()
# 更新:
# 更新:
return YichensPostStats( return YichensPostStats(
# 更新:
total_posts=result[0] or 0, total_posts=result[0] or 0,
# 更新:
total_deals=result[1] or 0, total_deals=result[1] or 0,
# 更新:
total_wants=result[2] or 0, total_wants=result[2] or 0,
# 更新:
total_replies=result[3] or 0, total_replies=result[3] or 0,
# 更新:
total_views=result[4] or 0, total_views=result[4] or 0,
# 更新:
avg_price=float(result[5]) if result[5] else None avg_price=float(result[5]) if result[5] else None
# 更新:
) )
# 更新:
# 更新:
@router.get("/stats/categories", response_model=List[CategoryStat]) @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)): def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
# 更新:
"""按分类统计帖子数量""" """按分类统计帖子数量"""
# 更新:
results = db.execute(text(""" results = db.execute(text("""
# 更新:
SELECT category, COUNT(*) as count SELECT category, COUNT(*) as count
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 day' * :days WHERE post_time >= NOW() - INTERVAL '1 day' * :days
# 更新:
GROUP BY category GROUP BY category
# 更新:
ORDER BY count DESC ORDER BY count DESC
# 更新:
"""), {"days": days}).fetchall() """), {"days": days}).fetchall()
# 更新:
# 更新:
return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results] return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results]
# 更新:
# 更新:
@router.get("/stats/users", response_model=UserStat) @router.get("/stats/users", response_model=UserStat)
# 更新:
def get_user_stats(db: Session = Depends(get_coolbot_db)): def get_user_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取用户统计""" """获取用户统计"""
# 更新:
result = db.execute(text(""" result = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total_users, COUNT(*) as total_users,
# 更新:
COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today, COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today,
# 更新:
COUNT(*) FILTER (WHERE is_seller = true) as sellers COUNT(*) FILTER (WHERE is_seller = true) as sellers
# 更新:
FROM yichens_users FROM yichens_users
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
return UserStat( return UserStat(
# 更新:
total_users=result[0] or 0, total_users=result[0] or 0,
# 更新:
new_users_today=result[1] or 0, new_users_today=result[1] or 0,
# 更新:
sellers=result[2] or 0 sellers=result[2] or 0
# 更新:
) )
# 更新:
# 更新:
@router.get("/posts") @router.get("/posts")
# 更新:
def get_posts( def get_posts(
# 更新:
limit: int = Query(20, ge=1, le=500), limit: int = Query(20, ge=1, le=500),
# 更新:
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
# 更新:
category: Optional[str] = None, category: Optional[str] = None,
# 更新:
post_type: Optional[str] = None, post_type: Optional[str] = None,
# 更新:
keyword: Optional[str] = None, keyword: Optional[str] = None,
# 更新:
db: Session = Depends(get_coolbot_db) db: Session = Depends(get_coolbot_db)
# 更新:
): ):
# 更新:
"""获取帖子列表 - 支持全局搜索,返回总数和分页信息""" """获取帖子列表 - 支持全局搜索,返回总数和分页信息"""
# 更新:
# 构建WHERE条件 # 构建WHERE条件
# 更新:
where_clauses = ["1=1"] where_clauses = ["1=1"]
# 更新:
params = {"limit": limit, "offset": offset} params = {"limit": limit, "offset": offset}
# 更新:
# 更新:
if category: if category:
# 更新:
where_clauses.append("category = :category") where_clauses.append("category = :category")
# 更新:
params["category"] = category params["category"] = category
# 更新:
# 更新:
if post_type: if post_type:
# 更新:
where_clauses.append("post_type = :post_type") where_clauses.append("post_type = :post_type")
# 更新:
params["post_type"] = post_type params["post_type"] = post_type
# 更新:
# 更新:
# 全局搜索 # 全局搜索
# 更新:
if keyword: if keyword:
# 更新:
where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)") where_clauses.append("(title ILIKE :keyword OR content ILIKE :keyword OR category ILIKE :keyword OR author_username ILIKE :keyword)")
# 更新:
params["keyword"] = f"%{keyword}%" params["keyword"] = f"%{keyword}%"
# 更新:
# 更新:
where_sql = " AND ".join(where_clauses) where_sql = " AND ".join(where_clauses)
# 更新:
# 更新:
# 查询总数 # 查询总数
# 更新:
count_query = f"SELECT COUNT(*) FROM yichens_posts WHERE {where_sql}" 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 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排序 # 查询数据 - 有post_time时按post_time排序没有时按crawled_at排序
# 更新:
data_query = f""" data_query = f"""
# 更新:
SELECT post_id, title, content, category, post_type, price, SELECT post_id, title, content, category, post_type, price,
# 更新:
author_username, post_time, reply_count, view_count, url author_username, post_time, reply_count, view_count, url
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE {where_sql} WHERE {where_sql}
# 更新:
ORDER BY COALESCE(post_time, crawled_at) DESC LIMIT :limit OFFSET :offset ORDER BY COALESCE(post_time, crawled_at) DESC LIMIT :limit OFFSET :offset
# 更新:
""" """
# 更新:
results = db.execute(text(data_query), params).fetchall() results = db.execute(text(data_query), params).fetchall()
# 更新:
# 更新:
posts = [PostItem( posts = [PostItem(
# 更新:
post_id=r[0], post_id=r[0],
# 更新:
title=r[1] or "", title=r[1] or "",
# 更新:
content=r[2] or "", content=r[2] or "",
# 更新:
category=r[3], category=r[3],
# 更新:
post_type=r[4] or "", post_type=r[4] or "",
# 更新:
price=float(r[5]) if r[5] else None, price=float(r[5]) if r[5] else None,
# 更新:
author_username=r[6] or "", author_username=r[6] or "",
# 更新:
post_time=str(r[7]) if r[7] else "", post_time=str(r[7]) if r[7] else "",
# 更新:
reply_count=r[8] or 0, reply_count=r[8] or 0,
# 更新:
view_count=r[9] or 0, view_count=r[9] or 0,
# 更新:
url=r[10] url=r[10]
# 更新:
) for r in results] ) for r in results]
# 更新:
# 更新:
return { return {
# 更新:
"posts": posts, "posts": posts,
# 更新:
"total": total_count, "total": total_count,
# 更新:
"page": offset // limit + 1, "page": offset // limit + 1,
# 更新:
"page_size": limit "page_size": limit
# 更新:
} }
# 更新:
# 更新:
@router.get("/users", response_model=List[UserItem]) @router.get("/users", response_model=List[UserItem])
# 更新:
def get_users( def get_users(
# 更新:
limit: int = Query(20, ge=1, le=500), limit: int = Query(20, ge=1, le=500),
# 更新:
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
# 更新:
is_seller: Optional[bool] = None, is_seller: Optional[bool] = None,
# 更新:
db: Session = Depends(get_coolbot_db) db: Session = Depends(get_coolbot_db)
# 更新:
): ):
# 更新:
"""获取用户列表""" """获取用户列表"""
# 更新:
query = """ query = """
# 更新:
SELECT user_id, username, avatar_url, credit_level, credit_score, SELECT user_id, username, avatar_url, credit_level, credit_score,
# 更新:
post_count, is_seller, registration_date post_count, is_seller, registration_date
# 更新:
FROM yichens_users FROM yichens_users
# 更新:
WHERE 1=1 WHERE 1=1
# 更新:
""" """
# 更新:
params = {"limit": limit, "offset": offset} params = {"limit": limit, "offset": offset}
# 更新:
# 更新:
if is_seller is not None: if is_seller is not None:
# 更新:
query += " AND is_seller = :is_seller" query += " AND is_seller = :is_seller"
# 更新:
params["is_seller"] = is_seller params["is_seller"] = is_seller
# 更新:
# 更新:
query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset" query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset"
# 更新:
# 更新:
results = db.execute(text(query), params).fetchall() results = db.execute(text(query), params).fetchall()
# 更新:
# 更新:
return [UserItem( return [UserItem(
# 更新:
user_id=r[0], user_id=r[0],
# 更新:
username=r[1] or "", username=r[1] or "",
# 更新:
avatar_url=r[2], avatar_url=r[2],
# 更新:
credit_level=r[3], credit_level=r[3],
# 更新:
credit_score=r[4], credit_score=r[4],
# 更新:
post_count=r[5] or 0, post_count=r[5] or 0,
# 更新:
is_seller=r[6] or False, is_seller=r[6] or False,
# 更新:
registration_date=str(r[7]) if r[7] else None registration_date=str(r[7]) if r[7] else None
# 更新:
) for r in results] ) for r in results]
# 更新:
# 更新:
# 更新:
@router.get("/stats/today") @router.get("/stats/today")
# 更新:
async def get_today_stats(db: Session = Depends(get_coolbot_db)): async def get_today_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取今日新增帖子统计""" """获取今日新增帖子统计"""
# 更新:
query = """ query = """
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, 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 = 'want' THEN 1 ELSE 0 END) as wants,
# 更新:
SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others, 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 dragons,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses, 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 category LIKE '%%' THEN 1 ELSE 0 END) as snakes,
# 更新:
SUM(CASE WHEN title LIKE '%天马%' OR content LIKE '%天马%' THEN 1 ELSE 0 END) as tianma SUM(CASE WHEN title LIKE '%天马%' OR content LIKE '%天马%' THEN 1 ELSE 0 END) as tianma
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
""" """
# 更新:
result = db.execute(text(query)).fetchone() result = db.execute(text(query)).fetchone()
# 更新:
return { return {
# 更新:
"total": result[0] or 0, "total": result[0] or 0,
# 更新:
"deals": result[1] or 0, "deals": result[1] or 0,
# 更新:
"wants": result[2] or 0, "wants": result[2] or 0,
# 更新:
"others": result[3] or 0, "others": result[3] or 0,
# 更新:
"dragons": result[4] or 0, "dragons": result[4] or 0,
# 更新:
"horses": result[5] or 0, "horses": result[5] or 0,
# 更新:
"snakes": result[6] or 0, "snakes": result[6] or 0,
# 更新:
"tianma": result[7] or 0 "tianma": result[7] or 0
# 更新:
} }
# 更新:
# 更新:
@router.get("/stats/hour") @router.get("/stats/hour")
# 更新:
async def get_hour_stats(db: Session = Depends(get_coolbot_db)): async def get_hour_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取近一个小时新增帖子统计""" """获取近一个小时新增帖子统计"""
# 更新:
query = """ query = """
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, 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 = 'want' THEN 1 ELSE 0 END) as wants,
# 更新:
SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others, 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 dragons,
# 更新:
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses, 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 category LIKE '%%' THEN 1 ELSE 0 END) as snakes
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= NOW() - INTERVAL '1 hour' WHERE post_time >= NOW() - INTERVAL '1 hour'
# 更新:
""" """
# 更新:
result = db.execute(text(query)).fetchone() result = db.execute(text(query)).fetchone()
# 更新:
return { return {
# 更新:
"total": result[0] or 0, "total": result[0] or 0,
# 更新:
"deals": result[1] or 0, "deals": result[1] or 0,
# 更新:
"wants": result[2] or 0, "wants": result[2] or 0,
# 更新:
"others": result[3] or 0, "others": result[3] or 0,
# 更新:
"dragons": result[3] or 0, "dragons": result[3] or 0,
# 更新:
"horses": result[4] or 0, "horses": result[4] or 0,
# 更新:
"snakes": result[5] or 0 "snakes": result[5] or 0
# 更新:
} }
# 更新:
# 更新:
@router.get("/stats/today-category") @router.get("/stats/today-category")
# 更新:
async def get_today_category_stats(db: Session = Depends(get_coolbot_db)): async def get_today_category_stats(db: Session = Depends(get_coolbot_db)):
# 更新:
"""获取今日帖子分类统计""" """获取今日帖子分类统计"""
# 更新:
query = """ query = """
# 更新:
SELECT category, COUNT(*) as count SELECT category, COUNT(*) as count
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
GROUP BY category GROUP BY category
# 更新:
ORDER BY count DESC ORDER BY count DESC
# 更新:
""" """
# 更新:
results = db.execute(text(query)).fetchall() results = db.execute(text(query)).fetchall()
# 更新:
return [{"category": r[0] or "未分类", "count": r[1]} for r in results] return [{"category": r[0] or "未分类", "count": r[1]} for r in results]
# 更新:
# 更新:
# 更新:
@router.get("/stats/dragons-today") @router.get("/stats/dragons-today")
# 更新:
def get_dragons_stats_today( def get_dragons_stats_today(
# 更新:
db: Session = Depends(get_coolbot_db) db: Session = Depends(get_coolbot_db)
# 更新:
): ):
# 更新:
"""获取今日龙钞详细统计数据(按号码分类)- 就高不就低""" """获取今日龙钞详细统计数据(按号码分类)- 就高不就低"""
# 更新:
from sqlalchemy import text from sqlalchemy import text
# 更新:
# 更新:
# 1. 带4包含"带4"、"带四"、"通货" # 1. 带4包含"带4"、"带四"、"通货"
# 更新:
dai4 = db.execute(text(""" dai4 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, 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 = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%带4%' OR title LIKE '%带4%' content LIKE '%带4%' OR title LIKE '%带4%'
# 更新:
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() """)).fetchone()
# 更新:
# 更新:
# 2. 无4包含"无4"、"无四",排除"无47"、"无247"、"无四七"、"永恒" # 2. 无4包含"无4"、"无四",排除"无47"、"无247"、"无四七"、"永恒"
# 更新:
wu4 = db.execute(text(""" wu4 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, 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 = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无4%' OR title LIKE '%无4%' content LIKE '%无4%' OR title LIKE '%无4%'
# 更新:
OR content LIKE '%无四%' OR title LIKE '%无四%' OR content LIKE '%无四%' OR title LIKE '%无四%'
# 更新:
) )
# 更新:
AND content NOT LIKE '%无47%' AND title NOT LIKE '%无47%' AND content NOT LIKE '%无47%' AND title NOT LIKE '%无47%'
# 更新:
AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%' 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 '%永恒%'
# 更新:
AND content NOT LIKE '%无四七%' AND title NOT LIKE '%无四七%' AND content NOT LIKE '%无四七%' AND title NOT LIKE '%无四七%'
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 3. 无47包含"无47"、"永恒"、"无四七",排除"无247" # 3. 无47包含"无47"、"永恒"、"无四七",排除"无247"
# 更新:
wu47 = db.execute(text(""" wu47 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, 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 = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无47%' OR title LIKE '%无47%' content LIKE '%无47%' OR title LIKE '%无47%'
# 更新:
OR content LIKE '%永恒%' OR title LIKE '%永恒%' OR content LIKE '%永恒%' OR title LIKE '%永恒%'
# 更新:
OR content LIKE '%无四七%' OR title LIKE '%无四七%' OR content LIKE '%无四七%' OR title LIKE '%无四七%'
# 更新:
) )
# 更新:
AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%' AND content NOT LIKE '%无247%' AND title NOT LIKE '%无247%'
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 4. 无247包含"无247"、"天马"、"金山",排除"无347" # 4. 无247包含"无247"、"天马"、"金山",排除"无347"
# 更新:
wu247 = db.execute(text(""" wu247 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, 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 = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无247%' OR title LIKE '%无247%' content LIKE '%无247%' OR title LIKE '%无247%'
# 更新:
OR content LIKE '%天马%' OR title LIKE '%天马%' OR content LIKE '%天马%' OR title LIKE '%天马%'
# 更新:
OR content LIKE '%金山%' OR title LIKE '%金山%' OR content LIKE '%金山%' OR title LIKE '%金山%'
# 更新:
) )
# 更新:
AND content NOT LIKE '%无347%' AND title NOT LIKE '%无347%' AND content NOT LIKE '%无347%' AND title NOT LIKE '%无347%'
# 更新:
""")).fetchone() """)).fetchone()
# 更新:
# 更新:
# 5. 无347包含"无347"、"钻石"、"金马"、"魅力"、"朦胧" # 5. 无347包含"无347"、"钻石"、"金马"、"魅力"、"朦胧"
# 更新:
wu347 = db.execute(text(""" wu347 = db.execute(text("""
# 更新:
SELECT SELECT
# 更新:
COUNT(*) as total, COUNT(*) as total,
# 更新:
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals, 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 = 'want' THEN 1 ELSE 0 END) as wants
# 更新:
FROM yichens_posts FROM yichens_posts
# 更新:
WHERE post_time >= CURRENT_DATE WHERE post_time >= CURRENT_DATE
# 更新:
AND category LIKE '%%' AND category LIKE '%%'
# 更新:
AND ( AND (
# 更新:
content LIKE '%无347%' OR title LIKE '%无347%' 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 '%金马%'
# 更新:
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() """)).fetchone()
# 更新:
# 更新:
return { return {
# 更新:
"dai4": {"total": dai4[0] or 0, "deals": dai4[1] or 0, "wants": dai4[2] or 0}, "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}, "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}, "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}, "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} "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 # Pydantic Schema - 使用字段编码并支持 camelCase
from typing import Optional, List from typing import Optional, List
from pydantic import BaseModel, EmailStr, Field from pydantic import BaseModel, EmailStr, Field, ConfigDict
from datetime import datetime from datetime import datetime
@ -14,9 +14,7 @@ class UserBase(BaseModel):
address: Optional[str] = None address: Optional[str] = None
bio: Optional[str] = None bio: Optional[str] = None
class Config: model_config = ConfigDict(from_attributes=True, populate_by_name=True)
from_attributes = True
populate_by_name = True
class UserCreate(UserBase): class UserCreate(UserBase):
@ -33,9 +31,7 @@ class UserUpdate(BaseModel):
bio: Optional[str] = None bio: Optional[str] = None
password: Optional[str] = None password: Optional[str] = None
class Config: model_config = ConfigDict(from_attributes=True, populate_by_name=True)
from_attributes = True
populate_by_name = True
class UserResponse(UserBase): class UserResponse(UserBase):
@ -63,9 +59,7 @@ class UserResponse(UserBase):
f99_92_created_at: Optional[datetime] = Field(None, alias="created_at") f99_92_created_at: Optional[datetime] = Field(None, alias="created_at")
f99_93_updated_at: Optional[datetime] = Field(None, alias="updated_at") f99_93_updated_at: Optional[datetime] = Field(None, alias="updated_at")
class Config: model_config = ConfigDict(from_attributes=True, populate_by_name=True)
from_attributes = True
populate_by_name = True
# ============ 藏品相关 ============ # ============ 藏品相关 ============
@ -110,9 +104,7 @@ class CollectionBase(BaseModel):
# f06 其他信息 # f06 其他信息
f06_50_purpose: Optional[str] = Field(None, alias="purpose") f06_50_purpose: Optional[str] = Field(None, alias="purpose")
class Config: model_config = ConfigDict(from_attributes=True, populate_by_name=True)
from_attributes = True
populate_by_name = True
class CollectionCreate(CollectionBase): class CollectionCreate(CollectionBase):
@ -159,9 +151,7 @@ class CollectionUpdate(BaseModel):
# f06 其他信息 # f06 其他信息
f06_50_purpose: Optional[str] = Field(None, alias="purpose") f06_50_purpose: Optional[str] = Field(None, alias="purpose")
class Config: model_config = ConfigDict(from_attributes=True, populate_by_name=True)
from_attributes = True
populate_by_name = True
class CollectionImageResponse(BaseModel): class CollectionImageResponse(BaseModel):
@ -171,8 +161,7 @@ class CollectionImageResponse(BaseModel):
path: Optional[str] = None path: Optional[str] = None
f99_92_created_at: Optional[datetime] = None f99_92_created_at: Optional[datetime] = None
class Config: model_config = ConfigDict(from_attributes=True)
from_attributes = True
class CollectionResponse(CollectionBase): class CollectionResponse(CollectionBase):
@ -182,9 +171,7 @@ class CollectionResponse(CollectionBase):
f99_93_updated_at: Optional[datetime] = Field(None, alias="updatedAt") f99_93_updated_at: Optional[datetime] = Field(None, alias="updatedAt")
images: List[CollectionImageResponse] = [] images: List[CollectionImageResponse] = []
class Config: model_config = ConfigDict(from_attributes=True, populate_by_name=True)
from_attributes = True
populate_by_name = True
class CollectionListResponse(BaseModel): class CollectionListResponse(BaseModel):
@ -209,8 +196,7 @@ class OperationResponse(OperationBase):
f99_91_user_id: str f99_91_user_id: str
f99_93_created_at: Optional[datetime] = None f99_93_created_at: Optional[datetime] = None
class Config: model_config = ConfigDict(from_attributes=True)
from_attributes = True
# ============ OCR 相关 ============ # ============ 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 charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <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"> <meta name="apple-mobile-web-app-capable" content="yes">
<title>甲辰收藏 v=1.2.97</title> <title>甲辰收藏 v=0.0.5</title>
<!-- Favicon --> <!-- Favicon -->
<link rel="icon" type="image/png" href="/static/icons/favicon.png" /> <link rel="icon" type="image/png" href="/static/icons/favicon.png" />

View File

@ -3,7 +3,7 @@
// 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx // 修改版本号只需修改 ../../VERSION 文件中的 VERSION=xxx
// 从环境变量读取vite.config.js 注入) // 从环境变量读取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 = { export const VERSION_INFO = {

View File

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

View File

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