Compare commits

...

14 Commits

Author SHA1 Message Date
甲辰生产 d35749e14b v1.2.57 - 统一版本号 2026-04-08 09:13:54 +08:00
甲辰生产 23aa5d3de1 v1.2.57 - 恢复yichens路由文件 2026-04-08 09:13:46 +08:00
甲辰生产 241385ef0c v1.2.56 - 统一版本号 2026-04-08 09:12:04 +08:00
甲辰生产 e962548ce3 v1.2.56 - 恢复一尘看板路由 2026-04-08 09:11:56 +08:00
甲辰生产 3f6296202b v1.2.55 - 统一版本号 2026-04-08 09:09:45 +08:00
甲辰生产 6b03f5e9c1 v1.2.55 - 修复stats接口类型错误 2026-04-08 09:09:39 +08:00
甲辰生产 1d8246402c v1.2.54 - 统一版本号 2026-04-08 09:06:55 +08:00
甲辰生产 88e734d6f7 v1.2.54 - 修复stats接口过滤条件bug 2026-04-08 09:06:48 +08:00
甲辰生产 e4ef142ad2 fix: VERSION文件格式 2026-04-08 08:22:24 +08:00
甲辰生产 e7039d6bfa v1.2.53 - 统一版本号 2026-04-08 08:13:09 +08:00
甲辰生产 404b966ba9 v1.2.53 - 修复编号并发问题+images字段映射 2026-04-08 08:13:04 +08:00
甲辰生产 2510f42b68 v1.2.52 - 统一后端版本号 2026-04-08 07:54:06 +08:00
甲辰生产 25fc88f683 v1.2.52 - 修复前端版本号 2026-04-08 07:48:25 +08:00
甲辰生产 6de39e55ae v1.2.52 - 安全修复:统一版本号、CORS配置、SECRET_KEY、Stats性能优化 2026-04-08 07:48:25 +08:00
9 changed files with 390 additions and 79 deletions

View File

@ -1 +1 @@
1.2.41
VERSION=1.2.57

View File

@ -1 +1 @@
1.2.38
1.2.57

View File

@ -11,7 +11,9 @@ from app.core.database import SessionLocal
from app.models.models import User
# 配置
SECRET_KEY = os.getenv("SECRET_KEY", "your-secret-key-change-in-production")
SECRET_KEY = os.getenv("SECRET_KEY")
if not SECRET_KEY:
raise ValueError("SECRET_KEY environment variable is not set. Please configure it in production!")
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "10080")) # 7天

View File

@ -16,6 +16,7 @@ from app.routers import auth, collections, operations
from app.routers import ocr as ocr_router
from app.routers import users as users_router
from app.routers import information as information_router
from app.routers import yichens as yichens_router
# 版本信息 - 从 config/VERSION 文件读取
def get_version():
@ -51,10 +52,11 @@ app = FastAPI(
# 设置全局错误处理器
setup_error_handlers(app)
# CORS 配置
# CORS 配置 - 生产环境限制域名
ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://47.103.29.111,http://120.55.81.21,https://socoolbot.com").split(",")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 生产环境应该限制域名
allow_origins=ALLOWED_ORIGINS, # 生产环境限制域名
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
@ -81,6 +83,7 @@ app.include_router(ocr_router.router) # OCR 识别
app.include_router(users_router.router) # 当前用户接口
app.include_router(users_router.admin_router) # 管理员用户管理
app.include_router(information_router.router) # 资讯
app.include_router(yichens_router.router) # 一尘看板
@app.get("/")

View File

@ -4,7 +4,8 @@ import uuid
import re
from typing import Optional, List
from fastapi import APIRouter, Depends, HTTPException, status, Query, UploadFile, File
from sqlalchemy import func, text
from sqlalchemy import func, text, case, nullsfirst
from sqlalchemy.dialects import postgresql as pg
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.auth import get_current_user
@ -56,6 +57,7 @@ def to_camel_case(data: dict) -> dict:
'f05_43_repair_fee': 'repairFee',
'f05_44_grading_fee': 'gradingFee',
'f06_50_purpose': 'purpose',
'images': 'images',
}
return {mapping.get(k, k): v for k, v in data.items()}
@ -63,23 +65,27 @@ def to_camel_case(data: dict) -> dict:
# 编码生成函数
def generate_code(version: str, user_id: str, db: Session) -> str:
"""自动生成藏品编号 - 按用户独立编码"""
import re
"""自动生成藏品编号 - 按用户独立编码,使用行锁防止并发冲突"""
from sqlalchemy import text
# 查询当前用户的非空编码(不与其他用户混算)
user_codes = db.query(Collection.f01_02_code).filter(
Collection.f01_02_code.isnot(None),
Collection.f99_91_user_id == user_id
).all()
# 使用 FOR UPDATE 行锁防止并发冲突
result = db.execute(
text("""
SELECT f01_02_code FROM collections
WHERE f99_91_user_id = :user_id
AND f01_02_code IS NOT NULL
AND f01_02_code ~ '^\\d{4,5}$'
ORDER BY f01_02_code::int DESC
LIMIT 1
FOR UPDATE
"""),
{"user_id": user_id}
).fetchone()
max_num = 0
for (code,) in user_codes:
# 处理纯数字编码支持4位和5位
if re.match(r'^\d{4,5}$', code):
if result and result[0]:
try:
num = int(code)
if num > max_num:
max_num = num
max_num = int(result[0])
except (ValueError, TypeError):
pass
@ -275,68 +281,117 @@ def get_stats(
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""获取藏品统计"""
# 获取所有藏品
if current_user.role == "admin":
all_collections = db.query(Collection).all()
else:
all_collections = db.query(Collection).filter(
Collection.f99_91_user_id == current_user.f99_90_id
).all()
"""获取藏品统计 - 使用数据库聚合查询优化性能"""
from sqlalchemy import case
# 总数
total_count = len(all_collections)
# 基础查询条件 - 管理员查看所有藏品,普通用户只看自己的
base_filter = True if current_user.role == "admin" else Collection.f99_91_user_id == current_user.f99_90_id
# 总数 - 使用 COUNT
total_count = db.query(func.count(Collection.f99_90_id)).filter(base_filter).scalar() or 0
# 按分类统计
from collections import Counter
by_category = Counter(c.f01_03_category for c in all_collections).items()
by_category = db.query(
Collection.f01_03_category,
func.count(Collection.f99_90_id)
).filter(base_filter).group_by(Collection.f01_03_category).all()
# 按状态统计
by_status = Counter(c.f01_04_status for c in all_collections).items()
by_status = db.query(
Collection.f01_04_status,
func.count(Collection.f99_90_id)
).filter(base_filter).group_by(Collection.f01_04_status).all()
# 按是否评级统计
by_graded = Counter(c.f03_20_is_graded for c in all_collections).items()
by_graded = db.query(
Collection.f03_20_is_graded,
func.count(Collection.f99_90_id)
).filter(base_filter).group_by(Collection.f03_20_is_graded).all()
# 新增8 个分布统计
by_packaging = Counter(c.f02_12_packaging for c in all_collections if c.f02_12_packaging).items()
by_rarity = Counter(c.f02_13_rarity for c in all_collections if c.f02_13_rarity).items()
by_version = Counter(c.f02_11_version for c in all_collections if c.f02_11_version).items()
by_grading_company = Counter(c.f03_21_grading_company for c in all_collections if c.f03_20_is_graded and c.f03_21_grading_company).items()
by_grading_score = Counter(c.f03_22_grading_score for c in all_collections if c.f03_20_is_graded and c.f03_22_grading_score).items()
by_special_mark = Counter(c.f04_30_special_mark for c in all_collections if c.f04_30_special_mark).items()
by_number_category = Counter(c.f02_14_number_category for c in all_collections if c.f02_14_number_category).items()
# 按包装统计
by_packaging = db.query(
Collection.f02_12_packaging,
func.count(Collection.f99_90_id)
).filter(base_filter, Collection.f02_12_packaging.isnot(None)).group_by(Collection.f02_12_packaging).all()
# 总成本: SUM(cost_price + repair_fee + grading_fee) for ALL collections
total_cost = sum(
(c.f05_40_cost_price or 0) + (c.f05_43_repair_fee or 0) + (c.f05_44_grading_fee or 0)
for c in all_collections
# 按稀有度统计
by_rarity = db.query(
Collection.f02_13_rarity,
func.count(Collection.f99_90_id)
).filter(base_filter, Collection.f02_13_rarity.isnot(None)).group_by(Collection.f02_13_rarity).all()
# 按版本统计
by_version = db.query(
Collection.f02_11_version,
func.count(Collection.f99_90_id)
).filter(base_filter, Collection.f02_11_version.isnot(None)).group_by(Collection.f02_11_version).all()
# 按评级公司统计
by_grading_company = db.query(
Collection.f03_21_grading_company,
func.count(Collection.f99_90_id)
).filter(base_filter, Collection.f03_20_is_graded == True, Collection.f03_21_grading_company.isnot(None)).group_by(Collection.f03_21_grading_company).all()
# 按评级分数统计
by_grading_score = db.query(
Collection.f03_22_grading_score,
func.count(Collection.f99_90_id)
).filter(base_filter, Collection.f03_20_is_graded == True, Collection.f03_22_grading_score.isnot(None)).group_by(Collection.f03_22_grading_score).all()
# 按特殊标记统计
by_special_mark = db.query(
Collection.f04_30_special_mark,
func.count(Collection.f99_90_id)
).filter(base_filter, Collection.f04_30_special_mark.isnot(None)).group_by(Collection.f04_30_special_mark).all()
# 按号码分类统计
by_number_category = db.query(
Collection.f02_14_number_category,
func.count(Collection.f99_90_id)
).filter(base_filter, Collection.f02_14_number_category.isnot(None)).group_by(Collection.f02_14_number_category).all()
# 财务统计 - 使用 SUM
cost_result = db.query(
func.sum(
(Collection.f05_40_cost_price or 0) +
(Collection.f05_43_repair_fee or 0) +
(Collection.f05_44_grading_fee or 0)
)
).filter(base_filter).scalar() or 0
# 预期利润: SUM(target_price - cost_price) for collections with target_price > 0
expected_profit = sum(
(c.f05_41_target_price or 0) - (c.f05_40_cost_price or 0)
for c in all_collections
if c.f05_41_target_price and c.f05_41_target_price > 0
target_result = db.query(
func.sum(Collection.f05_41_target_price)
).filter(base_filter, Collection.f05_41_target_price > 0).scalar() or 0
expected_profit = target_result - cost_result
# 已售商品统计 - 处理管理员和普通用户两种情况
if current_user.role == "admin":
sold_filter = (Collection.f01_04_status == 'sold') & (Collection.f05_42_goal_price > 0)
else:
sold_filter = (Collection.f99_91_user_id == current_user.f99_90_id) & (Collection.f01_04_status == 'sold') & (Collection.f05_42_goal_price > 0)
total_revenue = db.query(func.sum(Collection.f05_42_goal_price)).filter(sold_filter).scalar() or 0
from sqlalchemy import case
total_profit = db.query(
func.sum(
case(
(Collection.f05_42_goal_price.isnot(None),
Collection.f05_42_goal_price - func.coalesce(Collection.f05_40_cost_price, 0) - func.coalesce(Collection.f05_43_repair_fee, 0) - func.coalesce(Collection.f05_44_grading_fee, 0)),
else_=0
)
# 已售商品:状态为 sold 且出售价 > 0
sold_collections = [
c for c in all_collections
if c.f01_04_status == 'sold' and c.f05_42_goal_price and c.f05_42_goal_price > 0
]
# 总收入SUM(出售价) for 已售商品(售价>0
total_revenue = sum(
c.f05_42_goal_price or 0
for c in sold_collections
)
).filter(sold_filter).scalar() or 0
# 总利润已实现利润SUM(出售价 - 成本价 - 修复费 - 评级费) for 已售商品
# 单藏品总成本 = 成本价 + 修复费 + 评级费
total_profit = sum(
(c.f05_42_goal_price or 0) - (c.f05_40_cost_price or 0) - (c.f05_43_repair_fee or 0) - (c.f05_44_grading_fee or 0)
for c in sold_collections
)
# 盈亏统计
profit_count = db.query(func.count(Collection.f99_90_id)).filter(
sold_filter, Collection.f05_42_goal_price > Collection.f05_40_cost_price
).scalar() or 0
loss_count = db.query(func.count(Collection.f99_90_id)).filter(
sold_filter, Collection.f05_42_goal_price <= Collection.f05_40_cost_price
).scalar() or 0
return {
"totalCount": total_count,
@ -350,13 +405,12 @@ def get_stats(
"byGradingScore": [{"score": s, "count": n} for s, n in by_grading_score],
"bySpecialMark": [{"mark": m, "count": n} for m, n in by_special_mark],
"byNumberCategory": [{"numberCategory": n, "count": c} for n, c in by_number_category],
# 盈亏统计(只统计已售且有价格的藏品)
"byProfitLoss": [
{"type": "profit", "label": "盈利", "count": sum(1 for c in sold_collections if c.f05_42_goal_price and c.f05_40_cost_price and c.f05_42_goal_price > c.f05_40_cost_price)},
{"type": "loss", "label": "亏损", "count": sum(1 for c in sold_collections if c.f05_42_goal_price and c.f05_40_cost_price and c.f05_42_goal_price <= c.f05_40_cost_price)}
{"type": "profit", "label": "盈利", "count": profit_count},
{"type": "loss", "label": "亏损", "count": loss_count}
],
"totalCost": total_cost,
"totalTarget": sum(c.f05_41_target_price or 0 for c in all_collections),
"totalCost": cost_result,
"totalTarget": target_result,
"expectedProfit": expected_profit,
"totalRevenue": total_revenue,
"totalProfit": total_profit

View File

@ -0,0 +1,252 @@
from fastapi import APIRouter, Depends, Query
from sqlalchemy import func, text
from sqlalchemy.orm import Session
from pydantic import BaseModel
from typing import Optional, List
from datetime import datetime, date
from app.core.coolbot_db import get_coolbot_db
router = APIRouter(prefix="/api/yichens", tags=["一尘看板"])
# ============ 数据模型 ============
class YichensPostStats(BaseModel):
total_posts: int
total_deals: int # 出售
total_wants: int # 求购
total_replies: int
total_views: int
avg_price: Optional[float]
class CategoryStat(BaseModel):
category: str
count: int
class PostItem(BaseModel):
post_id: str
title: str
category: Optional[str]
post_type: str
price: Optional[float]
author_username: str
post_time: str
reply_count: int
view_count: int
url: Optional[str]
content: Optional[str]
class UserStat(BaseModel):
total_users: int
new_users_today: int
sellers: int
class UserItem(BaseModel):
user_id: str
username: str
avatar_url: Optional[str]
content: Optional[str]
credit_level: Optional[str]
credit_score: Optional[int]
post_count: int
is_seller: bool
registration_date: Optional[str]
# ============ 统计接口 ============
@router.get("/stats/posts", response_model=YichensPostStats)
def get_post_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
"""获取帖子统计"""
result = db.execute(text("""
SELECT
COUNT(*) as total_posts,
COUNT(*) FILTER (WHERE post_type = 'deal') as total_deals,
COUNT(*) FILTER (WHERE post_type = 'want') as total_wants,
COALESCE(SUM(reply_count), 0) as total_replies,
COALESCE(SUM(view_count), 0) as total_views,
AVG(price) as avg_price
FROM yichens_posts
WHERE post_time >= NOW() - INTERVAL '1 day' * :days
"""), {"days": days}).fetchone()
return YichensPostStats(
total_posts=result[0] or 0,
total_deals=result[1] or 0,
total_wants=result[2] or 0,
total_replies=result[3] or 0,
total_views=result[4] or 0,
avg_price=float(result[5]) if result[5] else None
)
@router.get("/stats/categories", response_model=List[CategoryStat])
def get_category_stats(days: int = Query(30, ge=1, le=365), db: Session = Depends(get_coolbot_db)):
"""按分类统计帖子数量"""
results = db.execute(text("""
SELECT category, COUNT(*) as count
FROM yichens_posts
WHERE post_time >= NOW() - INTERVAL '1 day' * :days
GROUP BY category
ORDER BY count DESC
"""), {"days": days}).fetchall()
return [CategoryStat(category=r[0] or "未知", count=r[1]) for r in results]
@router.get("/stats/users", response_model=UserStat)
def get_user_stats(db: Session = Depends(get_coolbot_db)):
"""获取用户统计"""
result = db.execute(text("""
SELECT
COUNT(*) as total_users,
COUNT(*) FILTER (WHERE created_at::date = CURRENT_DATE) as new_users_today,
COUNT(*) FILTER (WHERE is_seller = true) as sellers
FROM yichens_users
""")).fetchone()
return UserStat(
total_users=result[0] or 0,
new_users_today=result[1] or 0,
sellers=result[2] or 0
)
@router.get("/posts", response_model=List[PostItem])
def get_posts(
limit: int = Query(20, ge=1, le=500),
offset: int = Query(0, ge=0),
category: Optional[str] = None,
post_type: Optional[str] = None,
db: Session = Depends(get_coolbot_db)
):
"""获取帖子列表"""
query = """
SELECT post_id, title, content, category, post_type, price,
author_username, post_time, reply_count, view_count, url
FROM yichens_posts
WHERE 1=1
"""
params = {"limit": limit, "offset": offset}
if category:
query += " AND category = :category"
params["category"] = category
if post_type:
query += " AND post_type = :post_type"
params["post_type"] = post_type
query += " ORDER BY post_time DESC LIMIT :limit OFFSET :offset"
results = db.execute(text(query), params).fetchall()
return [PostItem(
post_id=r[0],
title=r[1] or "",
content=r[2] or "",
category=r[3],
post_type=r[4] or "",
price=float(r[5]) if r[5] else None,
author_username=r[6] or "",
post_time=str(r[7]) if r[7] else "",
reply_count=r[8] or 0,
view_count=r[9] or 0,
url=r[10]
) for r in results]
@router.get("/users", response_model=List[UserItem])
def get_users(
limit: int = Query(20, ge=1, le=500),
offset: int = Query(0, ge=0),
is_seller: Optional[bool] = None,
db: Session = Depends(get_coolbot_db)
):
"""获取用户列表"""
query = """
SELECT user_id, username, avatar_url, credit_level, credit_score,
post_count, is_seller, registration_date
FROM yichens_users
WHERE 1=1
"""
params = {"limit": limit, "offset": offset}
if is_seller is not None:
query += " AND is_seller = :is_seller"
params["is_seller"] = is_seller
query += " ORDER BY post_count DESC LIMIT :limit OFFSET :offset"
results = db.execute(text(query), params).fetchall()
return [UserItem(
user_id=r[0],
username=r[1] or "",
avatar_url=r[2],
credit_level=r[3],
credit_score=r[4],
post_count=r[5] or 0,
is_seller=r[6] or False,
registration_date=str(r[7]) if r[7] else None
) for r in results]
@router.get("/stats/today")
async def get_today_stats(db: Session = Depends(get_coolbot_db)):
"""获取今日新增帖子统计"""
query = """
SELECT
COUNT(*) as total,
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants,
SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others,
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons,
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses,
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes,
SUM(CASE WHEN title LIKE '%天马%' OR content LIKE '%天马%' THEN 1 ELSE 0 END) as tianma
FROM yichens_posts
WHERE post_time >= CURRENT_DATE
"""
result = db.execute(text(query)).fetchone()
return {
"total": result[0] or 0,
"deals": result[1] or 0,
"wants": result[2] or 0,
"others": result[3] or 0,
"dragons": result[4] or 0,
"horses": result[5] or 0,
"snakes": result[6] or 0,
"tianma": result[7] or 0
}
@router.get("/stats/hour")
async def get_hour_stats(db: Session = Depends(get_coolbot_db)):
"""获取近一个小时新增帖子统计"""
query = """
SELECT
COUNT(*) as total,
SUM(CASE WHEN post_type = 'deal' THEN 1 ELSE 0 END) as deals,
SUM(CASE WHEN post_type = 'want' THEN 1 ELSE 0 END) as wants,
SUM(CASE WHEN post_type = 'normal' THEN 1 ELSE 0 END) as others,
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as dragons,
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as horses,
SUM(CASE WHEN category LIKE '%%' THEN 1 ELSE 0 END) as snakes
FROM yichens_posts
WHERE post_time >= NOW() - INTERVAL '1 hour'
"""
result = db.execute(text(query)).fetchone()
return {
"total": result[0] or 0,
"deals": result[1] or 0,
"wants": result[2] or 0,
"others": result[3] or 0,
"dragons": result[3] or 0,
"horses": result[4] or 0,
"snakes": result[5] or 0
}
@router.get("/stats/today-category")
async def get_today_category_stats(db: Session = Depends(get_coolbot_db)):
"""获取今日帖子分类统计"""
query = """
SELECT category, COUNT(*) as count
FROM yichens_posts
WHERE post_time >= CURRENT_DATE
GROUP BY category
ORDER BY count DESC
"""
results = db.execute(text(query)).fetchall()
return [{"category": r[0] or "未分类", "count": r[1]} for r in results]

View File

@ -1 +1 @@
v1.2.41
VERSION=1.2.57

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "jiachenlong-frontend",
"version": "1.2.12",
"version": "1.2.57",
"private": true,
"description": "甲辰藏品管理系统 - 移动端前端",
"scripts": {