v0.0.5 - 寻配号功能优化

This commit is contained in:
甲辰生产 2026-04-23 23:26:00 +08:00
parent 6667671f46
commit d47a002495
15 changed files with 1795 additions and 1482 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

File diff suppressed because it is too large Load Diff

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,230 +1,384 @@
# seek - 寻号匹配路由 # seek - 寻号匹配路由
# Version: 0.0.1 # Version: 0.0.1
# 更新:
from fastapi import APIRouter, Depends, Query, HTTPException from fastapi import APIRouter, Depends, Query, HTTPException
# 更新:
# Version: 1.2.x # 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.routers.information import match_collections_count_from_coolbot
# 更新:
router = APIRouter(prefix="/api/seek", tags=["寻配号"]) router = APIRouter(prefix="/api/seek", tags=["寻配号"])
# 更新:
# 更新:
# ============ Schema ============ # ============ Schema ============
# 更新:
class SeekInfoCreate(BaseModel): class SeekInfoCreate(BaseModel):
# 更新:
title: str title: str
# 更新:
content: Optional[str] = None content: Optional[str] = None
# 更新:
expect_category: Optional[str] = None expect_category: Optional[str] = None
# 更新:
expect_version: Optional[str] = None expect_version: Optional[str] = None
# 更新:
expect_packaging: Optional[str] = None expect_packaging: Optional[str] = None
# 更新:
expect_number: Optional[str] = None expect_number: Optional[str] = None
# 更新:
expect_price_min: Optional[float] = None expect_price_min: Optional[float] = None
# 更新:
expect_price_max: Optional[float] = None expect_price_max: Optional[float] = None
# 更新:
# 更新:
class SeekInfoUpdate(BaseModel): class SeekInfoUpdate(BaseModel):
# 更新:
title: Optional[str] = None title: Optional[str] = None
# 更新:
content: Optional[str] = None content: Optional[str] = None
# 更新:
expect_category: Optional[str] = None expect_category: Optional[str] = None
# 更新:
expect_version: Optional[str] = None expect_version: Optional[str] = None
# 更新:
expect_packaging: Optional[str] = None expect_packaging: Optional[str] = None
# 更新:
expect_number: Optional[str] = None expect_number: Optional[str] = None
# 更新:
expect_price_min: Optional[float] = None expect_price_min: Optional[float] = None
# 更新:
expect_price_max: Optional[float] = None expect_price_max: Optional[float] = None
# 更新:
status: Optional[str] = None status: Optional[str] = None
# 更新:
# 更新:
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]
# 更新:
expect_version: Optional[str] expect_version: Optional[str]
# 更新:
expect_packaging: Optional[str] expect_packaging: Optional[str]
# 更新:
expect_number: Optional[str] expect_number: Optional[str]
# 更新:
expect_price_min: Optional[float] expect_price_min: Optional[float]
# 更新:
expect_price_max: Optional[float] expect_price_max: Optional[float]
# 更新:
status: str status: str
# 更新:
is_matched: Optional[str] is_matched: Optional[str]
# 更新:
matched_user_id: Optional[str] matched_user_id: Optional[str]
# 更新:
matched_contact: Optional[str] matched_contact: Optional[str]
# 更新:
view_count: int view_count: int
# 更新:
contact_count: int contact_count: int
# 更新:
created_at: Optional[datetime] created_at: Optional[datetime]
# 更新:
updated_at: Optional[datetime] updated_at: Optional[datetime]
# 更新:
# 更新:
class Config: class Config:
# 更新:
from_attributes = True from_attributes = True
# 更新:
# 更新:
# ============ API ============ # ============ API ============
# 更新:
@router.get("/list", response_model=list[SeekInfoResponse]) @router.get("/list", response_model=list[SeekInfoResponse])
# 更新:
def get_seek_list( def get_seek_list(
# 更新:
status: str = Query("active"), status: str = Query("active"),
# 更新:
page: int = Query(1, ge=1), page: int = Query(1, ge=1),
# 更新:
page_size: int = Query(20, ge=1, le=1000), page_size: int = Query(20, ge=1, le=1000),
# 更新:
user_only: bool = Query(False), user_only: bool = Query(False),
# 更新:
current_user: Optional = Depends(get_current_user), current_user: Optional = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号列表""" """获取寻配号列表"""
# 更新:
query = db.query(SeekInfo).filter(SeekInfo.status == status) query = db.query(SeekInfo).filter(SeekInfo.status == status)
# 更新:
# 更新:
# 我的寻配号:只查看自己的 # 我的寻配号:只查看自己的
# 更新:
if user_only and current_user: if user_only and current_user:
# 更新:
query = query.filter(SeekInfo.user_id == current_user.f99_90_id) query = query.filter(SeekInfo.user_id == current_user.f99_90_id)
# 更新:
# 更新:
# 排序 # 排序
# 更新:
query = query.order_by(SeekInfo.created_at.desc()) query = query.order_by(SeekInfo.created_at.desc())
# 更新:
# 更新:
# 分页 # 分页
# 更新:
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()
# 更新:
# 关联查询用户名 # 更新:
result = [] return items
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
# 计算网络数据匹配数
network_matched_count = 0
if 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,
})
return result
# 更新:
@router.get("/stats") @router.get("/stats")
# 更新:
def get_seek_stats( def get_seek_stats(
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号统计""" """获取寻配号统计"""
# 更新:
total = db.query(SeekInfo).filter(SeekInfo.status == "active").count() total = db.query(SeekInfo).filter(SeekInfo.status == "active").count()
# 更新:
matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count() matched = db.query(SeekInfo).filter(SeekInfo.is_matched == "true").count()
# 更新:
# 更新:
return { return {
# 更新:
"total": total, "total": total,
# 更新:
"matched": matched, "matched": matched,
# 更新:
"unmatched": total - matched "unmatched": total - matched
# 更新:
} }
# 更新:
# 更新:
@router.post("", response_model=SeekInfoResponse) @router.post("", response_model=SeekInfoResponse)
# 更新:
def create_seek( def create_seek(
# 更新:
data: SeekInfoCreate, data: SeekInfoCreate,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""创建寻配号""" """创建寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
seek = SeekInfo( seek = SeekInfo(
# 更新:
user_id=current_user.f99_90_id, user_id=current_user.f99_90_id,
# 更新:
title=data.title, title=data.title,
# 更新:
content=data.content, content=data.content,
# 更新:
expect_category=data.expect_category, expect_category=data.expect_category,
# 更新:
expect_version=data.expect_version, expect_version=data.expect_version,
# 更新:
expect_packaging=data.expect_packaging, expect_packaging=data.expect_packaging,
# 更新:
expect_number=data.expect_number, expect_number=data.expect_number,
# 更新:
expect_price_min=data.expect_price_min, expect_price_min=data.expect_price_min,
# 更新:
expect_price_max=data.expect_price_max, expect_price_max=data.expect_price_max,
# 更新:
status="active" status="active"
# 更新:
) )
# 更新:
db.add(seek) db.add(seek)
# 更新:
db.commit() db.commit()
# 更新:
db.refresh(seek) db.refresh(seek)
# 更新:
return seek return seek
# 更新:
# 更新:
@router.get("/{seek_id}", response_model=SeekInfoResponse) @router.get("/{seek_id}", response_model=SeekInfoResponse)
# 更新:
def get_seek( def get_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""获取寻配号详情""" """获取寻配号详情"""
# 更新:
seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first() seek = db.query(SeekInfo).filter(SeekInfo.id == seek_id).first()
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
# 更新:
# 更新:
# 增加浏览数 # 增加浏览数
# 更新:
seek.view_count += 1 seek.view_count += 1
# 更新:
db.commit() db.commit()
# 更新:
# 更新:
return seek return seek
# 更新:
# 更新:
@router.put("/{seek_id}", response_model=SeekInfoResponse) @router.put("/{seek_id}", response_model=SeekInfoResponse)
# 更新:
def update_seek( def update_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
data: SeekInfoUpdate, data: SeekInfoUpdate,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""更新寻配号""" """更新寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
seek = db.query(SeekInfo).filter( seek = db.query(SeekInfo).filter(
# 更新:
SeekInfo.id == seek_id, SeekInfo.id == seek_id,
# 更新:
SeekInfo.user_id == current_user.f99_90_id SeekInfo.user_id == current_user.f99_90_id
# 更新:
).first() ).first()
# 更新:
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
# 更新:
# 更新:
for key, value in data.model_dump(exclude_unset=True).items(): for key, value in data.model_dump(exclude_unset=True).items():
# 更新:
setattr(seek, key, value) setattr(seek, key, value)
# 更新:
# 更新:
db.commit() db.commit()
# 更新:
db.refresh(seek) db.refresh(seek)
# 更新:
return seek return seek
# 更新:
# 更新:
@router.delete("/{seek_id}") @router.delete("/{seek_id}")
# 更新:
def delete_seek( def delete_seek(
# 更新:
seek_id: str, seek_id: str,
# 更新:
current_user = Depends(get_current_user), current_user = Depends(get_current_user),
# 更新:
db: Session = Depends(get_db) db: Session = Depends(get_db)
# 更新:
): ):
# 更新:
"""删除寻配号""" """删除寻配号"""
# 更新:
if not current_user: if not current_user:
# 更新:
raise HTTPException(status_code=401, detail="请先登录") raise HTTPException(status_code=401, detail="请先登录")
# 更新:
# 更新:
seek = db.query(SeekInfo).filter( seek = db.query(SeekInfo).filter(
# 更新:
SeekInfo.id == seek_id, SeekInfo.id == seek_id,
# 更新:
SeekInfo.user_id == current_user.f99_90_id SeekInfo.user_id == current_user.f99_90_id
# 更新:
).first() ).first()
# 更新:
# 更新:
if not seek: if not seek:
# 更新:
raise HTTPException(status_code=404, detail="寻配号不存在") raise HTTPException(status_code=404, detail="寻配号不存在")
# 更新:
# 更新:
seek.status = "deleted" seek.status = "deleted"
# 更新:
db.commit() db.commit()
# 更新:
# 更新:
return {"message": "删除成功"} return {"message": "删除成功"}
# 更新:

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.3

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.3</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'
@ -330,7 +330,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 +352,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 +370,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) }
} }