2026-04-04 18:23:00 +08:00
|
|
|
|
"""
|
|
|
|
|
|
CoolBotDataSys API 服务
|
|
|
|
|
|
酷博特数据分析系统 RESTful API
|
|
|
|
|
|
|
|
|
|
|
|
版本: 1.0.0
|
|
|
|
|
|
"""
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
|
from fastapi import FastAPI, Request, HTTPException
|
|
|
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
|
|
|
|
from fastapi.responses import JSONResponse
|
|
|
|
|
|
|
2026-04-04 18:26:29 +08:00
|
|
|
|
from api.routes import collections_router, prices_router, health_router, yichens_router
|
2026-04-04 18:23:00 +08:00
|
|
|
|
from api.middleware.response import ApiResponse
|
|
|
|
|
|
|
|
|
|
|
|
# 配置日志
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
|
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
|
|
|
|
)
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
|
async def lifespan(app: FastAPI):
|
|
|
|
|
|
"""应用生命周期管理"""
|
|
|
|
|
|
logger.info("🚀 CoolBotDataSys API 启动...")
|
|
|
|
|
|
yield
|
|
|
|
|
|
logger.info("👋 CoolBotDataSys API 关闭...")
|
|
|
|
|
|
|
|
|
|
|
|
# 创建 FastAPI 应用
|
|
|
|
|
|
app = FastAPI(
|
|
|
|
|
|
title="CoolBotDataSys API",
|
|
|
|
|
|
description="""
|
|
|
|
|
|
## CoolBotDataSys - 酷博特数据分析系统
|
|
|
|
|
|
|
|
|
|
|
|
### 功能模块
|
|
|
|
|
|
- **藏品管理** - 藏品的 CRUD 操作
|
|
|
|
|
|
- **价格追踪** - 历史价格查询和趋势分析
|
|
|
|
|
|
- **爬虫调度** - 数据采集任务管理
|
|
|
|
|
|
- **统计报表** - 数据统计和报表生成
|
|
|
|
|
|
|
|
|
|
|
|
### 认证方式
|
|
|
|
|
|
当前版本暂不需要认证,后续添加 API Key 认证。
|
|
|
|
|
|
""",
|
|
|
|
|
|
version="1.0.0",
|
|
|
|
|
|
lifespan=lifespan,
|
|
|
|
|
|
docs_url="/docs",
|
|
|
|
|
|
redoc_url="/redoc"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# CORS 配置
|
|
|
|
|
|
app.add_middleware(
|
|
|
|
|
|
CORSMiddleware,
|
|
|
|
|
|
allow_origins=["*"],
|
|
|
|
|
|
allow_credentials=True,
|
|
|
|
|
|
allow_methods=["*"],
|
|
|
|
|
|
allow_headers=["*"],
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 全局异常处理
|
|
|
|
|
|
@app.exception_handler(HTTPException)
|
|
|
|
|
|
async def http_exception_handler(request: Request, exc: HTTPException):
|
|
|
|
|
|
"""HTTP 异常处理"""
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=exc.status_code,
|
|
|
|
|
|
content=ApiResponse.error(
|
|
|
|
|
|
code=exc.status_code * 100,
|
|
|
|
|
|
message=exc.detail
|
|
|
|
|
|
).model_dump()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
@app.exception_handler(Exception)
|
|
|
|
|
|
async def general_exception_handler(request: Request, exc: Exception):
|
|
|
|
|
|
"""通用异常处理"""
|
|
|
|
|
|
logger.error(f"未处理异常: {exc}", exc_info=True)
|
|
|
|
|
|
return JSONResponse(
|
|
|
|
|
|
status_code=500,
|
|
|
|
|
|
content=ApiResponse.error(
|
|
|
|
|
|
code=50000,
|
|
|
|
|
|
message="Internal server error"
|
|
|
|
|
|
).model_dump()
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 注册路由
|
|
|
|
|
|
app.include_router(health_router)
|
2026-04-04 18:26:29 +08:00
|
|
|
|
app.include_router(yichens_router)
|
2026-04-04 18:23:00 +08:00
|
|
|
|
app.include_router(collections_router)
|
|
|
|
|
|
app.include_router(prices_router)
|
|
|
|
|
|
|
|
|
|
|
|
# 统计接口
|
|
|
|
|
|
from api.middleware.response import ApiResponse
|
|
|
|
|
|
from database import db
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/v1/statistics", response_model=ApiResponse, tags=["statistics"])
|
|
|
|
|
|
async def get_statistics():
|
|
|
|
|
|
"""获取系统统计信息"""
|
|
|
|
|
|
with db.get_cursor() as cursor:
|
|
|
|
|
|
# 藏品总数
|
|
|
|
|
|
cursor.execute("SELECT COUNT(*) as cnt FROM collections")
|
|
|
|
|
|
total_collections = cursor.fetchone()["cnt"]
|
|
|
|
|
|
|
|
|
|
|
|
# 价格记录总数
|
|
|
|
|
|
cursor.execute("SELECT COUNT(*) as cnt FROM price_history")
|
|
|
|
|
|
total_price_records = cursor.fetchone()["cnt"]
|
|
|
|
|
|
|
|
|
|
|
|
# 今日新增价格记录
|
|
|
|
|
|
cursor.execute("""
|
|
|
|
|
|
SELECT COUNT(*) as cnt FROM price_history
|
|
|
|
|
|
WHERE DATE(crawled_at) = CURDATE()
|
|
|
|
|
|
""")
|
|
|
|
|
|
today_price_records = cursor.fetchone()["cnt"]
|
|
|
|
|
|
|
|
|
|
|
|
# 最近爬虫运行时间
|
|
|
|
|
|
cursor.execute("""
|
|
|
|
|
|
SELECT MAX(finished_at) as last_run
|
|
|
|
|
|
FROM crawl_logs WHERE status = 'success'
|
|
|
|
|
|
""")
|
|
|
|
|
|
last_crawl = cursor.fetchone()["last_run"]
|
|
|
|
|
|
|
|
|
|
|
|
# 分类统计
|
|
|
|
|
|
cursor.execute("""
|
|
|
|
|
|
SELECT category, COUNT(*) as count
|
|
|
|
|
|
FROM collections
|
|
|
|
|
|
WHERE category IS NOT NULL
|
|
|
|
|
|
GROUP BY category
|
|
|
|
|
|
""")
|
|
|
|
|
|
category_stats = {row["category"]: row["count"] for row in cursor.fetchall()}
|
|
|
|
|
|
|
|
|
|
|
|
return ApiResponse.success({
|
|
|
|
|
|
"total_collections": total_collections,
|
|
|
|
|
|
"total_price_records": total_price_records,
|
|
|
|
|
|
"today_price_records": today_price_records,
|
|
|
|
|
|
"last_crawl_time": last_crawl,
|
|
|
|
|
|
"category_stats": category_stats
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
# 爬虫调度接口
|
2026-04-04 20:02:13 +08:00
|
|
|
|
from crawlers.yichens_spider import YichensSpider
|
2026-04-04 18:23:00 +08:00
|
|
|
|
|
|
|
|
|
|
@app.post("/api/v1/crawl-jobs/trigger", response_model=ApiResponse, tags=["crawl"])
|
|
|
|
|
|
async def trigger_crawl(source: str = "yichens"):
|
|
|
|
|
|
"""触发爬虫任务"""
|
|
|
|
|
|
if source == "yichens":
|
2026-04-04 20:02:13 +08:00
|
|
|
|
spider = YichensSpider()
|
2026-04-04 18:23:00 +08:00
|
|
|
|
items = spider.run()
|
|
|
|
|
|
return ApiResponse.success({
|
|
|
|
|
|
"source": source,
|
|
|
|
|
|
"items_collected": len(items),
|
|
|
|
|
|
"status": "completed"
|
|
|
|
|
|
}, "Crawl job triggered successfully")
|
|
|
|
|
|
else:
|
|
|
|
|
|
raise HTTPException(status_code=400, detail=f"Unknown source: {source}")
|
|
|
|
|
|
|
|
|
|
|
|
@app.get("/api/v1/crawl-jobs", response_model=ApiResponse, tags=["crawl"])
|
|
|
|
|
|
async def get_crawl_jobs(limit: int = 20):
|
|
|
|
|
|
"""获取爬虫运行历史"""
|
|
|
|
|
|
with db.get_cursor() as cursor:
|
|
|
|
|
|
cursor.execute("""
|
|
|
|
|
|
SELECT * FROM crawl_logs
|
|
|
|
|
|
ORDER BY finished_at DESC LIMIT %s
|
|
|
|
|
|
""", (limit,))
|
|
|
|
|
|
items = cursor.fetchall()
|
|
|
|
|
|
return ApiResponse.success(items)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
import uvicorn
|
|
|
|
|
|
uvicorn.run(app, host="0.0.0.0", port=8080)
|