所有功能已完成,运行OK
This commit is contained in:
630
backend/main.py
Normal file
630
backend/main.py
Normal file
@@ -0,0 +1,630 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
古诗词阅读网站后端 API
|
||||
|
||||
基于 FastAPI 的 RESTful API,提供:
|
||||
- 诗词数据管理(CRUD)
|
||||
- 多类别组合筛选
|
||||
- 阅读标记管理
|
||||
- 统计分析
|
||||
- 批量导入
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Dict, Any
|
||||
from contextlib import contextmanager
|
||||
from fastapi import FastAPI, HTTPException, UploadFile, File, Query
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse, JSONResponse
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
import uvicorn
|
||||
|
||||
app = FastAPI(title="古诗词阅读 API", version="2.0.0")
|
||||
|
||||
# 启用 CORS
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 配置
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
PROJECT_ROOT = os.path.dirname(BASE_DIR)
|
||||
DB_PATH = os.path.join(BASE_DIR, 'poems.db')
|
||||
FRONTEND_DIR = os.path.join(PROJECT_ROOT, 'frontend')
|
||||
|
||||
# 分类标签体系定义
|
||||
CATEGORY_SYSTEM = {
|
||||
"season": {
|
||||
"name": "季节",
|
||||
"tags": ["春", "夏", "秋", "冬", "四季", "无明确季节"]
|
||||
},
|
||||
"solar_terms": {
|
||||
"name": "节气",
|
||||
"tags": ["立春", "雨水", "惊蛰", "春分", "清明", "谷雨",
|
||||
"立夏", "小满", "芒种", "夏至", "小暑", "大暑",
|
||||
"立秋", "处暑", "白露", "秋分", "寒露", "霜降",
|
||||
"立冬", "小雪", "大雪", "冬至", "小寒", "大寒"]
|
||||
},
|
||||
"time_of_day": {
|
||||
"name": "时辰",
|
||||
"tags": ["清晨", "上午", "正午", "下午", "黄昏", "夜晚", "深夜", "黎明", "不明确"]
|
||||
},
|
||||
"genre": {
|
||||
"name": "题材类型",
|
||||
"tags": ["山水田园", "边塞征战", "咏史怀古", "咏物言志", "送别怀人", "思乡怀远",
|
||||
"爱情闺怨", "友情赠答", "羁旅漂泊", "隐逸闲适", "讽喻时事", "节日习俗",
|
||||
"宴饮酬唱", "读书治学", "农耕劳作", "宗教禅理", "其他"]
|
||||
},
|
||||
"emotion_tone": {
|
||||
"name": "情感基调",
|
||||
"tags": ["喜悦欢快", "悲伤哀愁", "愤怒激愤", "忧郁伤感", "孤独寂寞",
|
||||
"宁静淡泊", "豪迈激昂", "思念眷恋", "惆怅失落", "平和超脱", "复杂混合"]
|
||||
},
|
||||
"emotions": {
|
||||
"name": "具体情感",
|
||||
"tags": ["喜", "怒", "哀", "乐", "忧", "思", "悲", "恐", "惊", "愁",
|
||||
"恨", "爱", "恋", "盼", "悔", "愧", "傲", "谦", "静", "躁"]
|
||||
},
|
||||
"nature_scenery": {
|
||||
"name": "自然景物",
|
||||
"tags": ["山", "水", "云", "雨", "雪", "风", "雷", "电", "日", "月", "星", "霜", "露", "霞"]
|
||||
},
|
||||
"plants": {
|
||||
"name": "植物",
|
||||
"tags": ["松", "竹", "梅", "兰", "菊", "荷", "柳", "桃", "李", "杏", "梨", "枫", "梧桐", "芭蕉", "其他"]
|
||||
},
|
||||
"animals": {
|
||||
"name": "动物",
|
||||
"tags": ["鸟", "雁", "燕", "鹊", "蝉", "蛙", "鱼", "龙", "凤", "马", "牛", "羊", "犬", "其他"]
|
||||
},
|
||||
"buildings": {
|
||||
"name": "建筑",
|
||||
"tags": ["楼", "阁", "亭", "台", "轩", "榭", "桥", "寺", "塔", "城", "关", "宫", "殿", "院", "其他"]
|
||||
},
|
||||
"philosophy": {
|
||||
"name": "哲理思想",
|
||||
"tags": ["儒家思想", "道家思想", "佛家禅理", "人生感悟", "历史兴叹", "自然之道", "无明显哲理"]
|
||||
},
|
||||
"life_stage": {
|
||||
"name": "人生阶段",
|
||||
"tags": ["少年", "青年", "中年", "老年", "不明确"]
|
||||
},
|
||||
"social_role": {
|
||||
"name": "社会身份",
|
||||
"tags": ["士人", "官员", "隐士", "游子", "征人", "商贾", "农夫", "僧道", "闺中", "其他"]
|
||||
},
|
||||
"technique": {
|
||||
"name": "写作手法",
|
||||
"tags": ["比兴", "赋", "对仗", "用典", "借景抒情", "托物言志", "虚实结合", "动静结合", "其他"]
|
||||
},
|
||||
"rhetoric": {
|
||||
"name": "修辞手法",
|
||||
"tags": ["比喻", "拟人", "夸张", "对偶", "排比", "反复", "设问", "反问", "其他"]
|
||||
},
|
||||
"colors": {
|
||||
"name": "色彩意象",
|
||||
"tags": ["青", "绿", "红", "白", "黄", "紫", "碧", "翠", "苍", "金"]
|
||||
},
|
||||
"sounds": {
|
||||
"name": "声音意象",
|
||||
"tags": ["钟声", "鼓声", "笛声", "琴声", "风声", "雨声", "鸟鸣", "蝉鸣", "其他"]
|
||||
},
|
||||
"location": {
|
||||
"name": "地理方位",
|
||||
"tags": ["江南", "塞北", "中原", "巴蜀", "关中", "岭南", "吴越", "荆楚", "其他"]
|
||||
},
|
||||
"festival": {
|
||||
"name": "节日习俗",
|
||||
"tags": ["春节", "元宵", "清明", "端午", "七夕", "中秋", "重阳", "除夕", "无"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db_connection():
|
||||
"""数据库连接上下文管理器"""
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
try:
|
||||
yield conn
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def init_database():
|
||||
"""初始化数据库表结构"""
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 诗词表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS poems (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT NOT NULL,
|
||||
author TEXT NOT NULL,
|
||||
paragraphs TEXT,
|
||||
signature TEXT UNIQUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# 分类标签表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS classifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
poem_id TEXT NOT NULL,
|
||||
category TEXT NOT NULL,
|
||||
tags TEXT,
|
||||
FOREIGN KEY (poem_id) REFERENCES poems(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
|
||||
# 阅读记录表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS reading_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
poem_id TEXT UNIQUE NOT NULL,
|
||||
is_read BOOLEAN DEFAULT FALSE,
|
||||
read_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (poem_id) REFERENCES poems(id) ON DELETE CASCADE
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建索引
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_classifications_poem ON classifications(poem_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_classifications_category ON classifications(category)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_reading_records_poem ON reading_records(poem_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_poems_signature ON poems(signature)')
|
||||
|
||||
conn.commit()
|
||||
|
||||
|
||||
def poem_to_dict(row: sqlite3.Row, classifications: Optional[List[sqlite3.Row]] = None, reading_record: Optional[sqlite3.Row] = None) -> Dict:
|
||||
"""将数据库行转换为诗词字典"""
|
||||
result = {
|
||||
'id': row['id'],
|
||||
'title': row['title'],
|
||||
'author': row['author'],
|
||||
'paragraphs': json.loads(row['paragraphs']) if row['paragraphs'] else [],
|
||||
'created_at': row['created_at'],
|
||||
'updated_at': row['updated_at']
|
||||
}
|
||||
|
||||
if classifications:
|
||||
result['classifications'] = {}
|
||||
for clf in classifications:
|
||||
result['classifications'][clf['category']] = json.loads(clf['tags'])
|
||||
|
||||
if reading_record:
|
||||
result['is_read'] = bool(reading_record['is_read'])
|
||||
result['read_at'] = reading_record['read_at']
|
||||
else:
|
||||
result['is_read'] = False
|
||||
result['read_at'] = None
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# ===== API 端点 =====
|
||||
|
||||
# 挂载静态文件目录
|
||||
app.mount("/static", StaticFiles(directory=FRONTEND_DIR), name="static")
|
||||
|
||||
@app.get("/")
|
||||
async def serve_frontend():
|
||||
"""提供前端页面"""
|
||||
return FileResponse(os.path.join(FRONTEND_DIR, 'index.html'))
|
||||
|
||||
|
||||
@app.get("/style.css")
|
||||
async def serve_css():
|
||||
"""提供 CSS 文件"""
|
||||
return FileResponse(os.path.join(FRONTEND_DIR, 'style.css'))
|
||||
|
||||
|
||||
@app.get("/script.js")
|
||||
async def serve_js():
|
||||
"""提供 JS 文件"""
|
||||
return FileResponse(os.path.join(FRONTEND_DIR, 'script.js'))
|
||||
|
||||
|
||||
@app.get("/api/categories")
|
||||
async def get_categories():
|
||||
"""获取分类标签体系"""
|
||||
return CATEGORY_SYSTEM
|
||||
|
||||
|
||||
@app.get("/api/stats")
|
||||
async def get_statistics():
|
||||
"""获取统计数据"""
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 总数
|
||||
cursor.execute('SELECT COUNT(*) FROM poems')
|
||||
total = cursor.fetchone()[0]
|
||||
|
||||
# 已读数量
|
||||
cursor.execute('SELECT COUNT(*) FROM reading_records WHERE is_read = TRUE')
|
||||
read_count = cursor.fetchone()[0]
|
||||
|
||||
# 各分类统计
|
||||
cursor.execute('''
|
||||
SELECT category, COUNT(*) as count
|
||||
FROM classifications
|
||||
GROUP BY category
|
||||
''')
|
||||
category_stats = {row['category']: row['count'] for row in cursor.fetchall()}
|
||||
|
||||
# 热门标签
|
||||
cursor.execute('''
|
||||
SELECT category, tags FROM classifications
|
||||
WHERE category = 'genre' OR category = 'emotion_tone'
|
||||
''')
|
||||
popular_tags = {}
|
||||
for row in cursor.fetchall():
|
||||
tags = json.loads(row['tags'])
|
||||
for tag in tags:
|
||||
if tag not in popular_tags:
|
||||
popular_tags[tag] = 0
|
||||
popular_tags[tag] += 1
|
||||
|
||||
top_tags = sorted(popular_tags.items(), key=lambda x: x[1], reverse=True)[:20]
|
||||
|
||||
return {
|
||||
'total_poems': total,
|
||||
'read_count': read_count,
|
||||
'unread_count': total - read_count,
|
||||
'category_stats': category_stats,
|
||||
'top_tags': top_tags,
|
||||
'reading_progress': round((read_count / total * 100) if total > 0 else 0, 1)
|
||||
}
|
||||
|
||||
|
||||
@app.post("/api/poems/import")
|
||||
async def import_poems(file: UploadFile = File(...)):
|
||||
"""批量导入诗词(支持 JSON/JSONL)"""
|
||||
if not file.filename or not file.filename.endswith(('.json', '.jsonl')):
|
||||
raise HTTPException(status_code=400, detail="仅支持 JSON 和 JSONL 格式")
|
||||
|
||||
content = await file.read()
|
||||
content_str = content.decode('utf-8').strip()
|
||||
|
||||
if not content_str:
|
||||
raise HTTPException(status_code=400, detail="文件内容为空")
|
||||
|
||||
# 解析文件
|
||||
raw_items = []
|
||||
try:
|
||||
data = json.loads(content_str)
|
||||
if isinstance(data, list):
|
||||
raw_items = data
|
||||
elif isinstance(data, dict):
|
||||
raw_items = [data]
|
||||
except json.JSONDecodeError:
|
||||
for line in content_str.splitlines():
|
||||
line = line.strip()
|
||||
if line:
|
||||
try:
|
||||
obj = json.loads(line)
|
||||
if isinstance(obj, dict):
|
||||
raw_items.append(obj)
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
# 导入数据库
|
||||
imported_count = 0
|
||||
skipped_count = 0
|
||||
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
for item in raw_items:
|
||||
try:
|
||||
# 验证基本格式
|
||||
if not all(k in item for k in ['title', 'author', 'paragraphs']):
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
poem_id = item.get('id', f"poem_{datetime.now().timestamp()}")
|
||||
signature = item.get('signature', '')
|
||||
paragraphs = json.dumps(item['paragraphs'], ensure_ascii=False)
|
||||
|
||||
# 检查是否已存在
|
||||
if signature:
|
||||
cursor.execute('SELECT id FROM poems WHERE signature = ?', (signature,))
|
||||
if cursor.fetchone():
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
# 插入诗词
|
||||
cursor.execute('''
|
||||
INSERT OR REPLACE INTO poems (id, title, author, paragraphs, signature)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
''', (poem_id, item['title'], item['author'], paragraphs, signature or ''))
|
||||
|
||||
# 插入分类
|
||||
if 'llm_classification' in item or 'classifications' in item:
|
||||
classifications = item.get('llm_classification', item.get('classifications', {}))
|
||||
|
||||
# 先删除旧分类
|
||||
cursor.execute('DELETE FROM classifications WHERE poem_id = ?', (poem_id,))
|
||||
|
||||
# 插入新分类
|
||||
for category, tags in classifications.items():
|
||||
if isinstance(tags, list):
|
||||
tags_str = json.dumps(tags, ensure_ascii=False)
|
||||
else:
|
||||
tags_str = json.dumps([tags], ensure_ascii=False)
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO classifications (poem_id, category, tags)
|
||||
VALUES (?, ?, ?)
|
||||
''', (poem_id, category, tags_str))
|
||||
|
||||
# 初始化阅读记录
|
||||
cursor.execute('''
|
||||
INSERT OR IGNORE INTO reading_records (poem_id, is_read)
|
||||
VALUES (?, FALSE)
|
||||
''', (poem_id,))
|
||||
|
||||
imported_count += 1
|
||||
|
||||
except Exception as e:
|
||||
print(f"导入失败:{e}")
|
||||
skipped_count += 1
|
||||
continue
|
||||
|
||||
conn.commit()
|
||||
|
||||
return {
|
||||
'message': f'成功导入 {imported_count} 首诗词',
|
||||
'imported': imported_count,
|
||||
'skipped': skipped_count
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/poems")
|
||||
async def get_poems(
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
categories: Optional[str] = Query(None, description="多类别筛选,格式:category1:tag1,category2:tag2"),
|
||||
search: Optional[str] = Query(None, description="搜索关键词"),
|
||||
is_read: Optional[bool] = Query(None, description="阅读状态筛选")
|
||||
):
|
||||
"""获取诗词列表(支持分页、多类别筛选、搜索)"""
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 构建查询条件
|
||||
conditions = []
|
||||
params = []
|
||||
|
||||
# 阅读状态筛选
|
||||
if is_read is not None:
|
||||
conditions.append('rr.is_read = ?')
|
||||
params.append(1 if is_read else 0)
|
||||
|
||||
# 搜索
|
||||
if search:
|
||||
conditions.append('(p.title LIKE ? OR p.author LIKE ?)')
|
||||
params.extend([f'%{search}%', f'%{search}%'])
|
||||
|
||||
# 多类别筛选
|
||||
if categories:
|
||||
category_conditions = []
|
||||
for cat_filter in categories.split(','):
|
||||
if ':' in cat_filter:
|
||||
category, tag = cat_filter.split(':', 1)
|
||||
category_conditions.append('''
|
||||
EXISTS (
|
||||
SELECT 1 FROM classifications c
|
||||
WHERE c.poem_id = p.id
|
||||
AND c.category = ?
|
||||
AND c.tags LIKE ?
|
||||
)
|
||||
''')
|
||||
params.extend([category, f'%{tag}%'])
|
||||
|
||||
if category_conditions:
|
||||
conditions.append(' AND '.join(category_conditions))
|
||||
|
||||
where_clause = ' AND '.join(conditions) if conditions else '1=1'
|
||||
|
||||
# 查询总数
|
||||
count_sql = f'''
|
||||
SELECT COUNT(DISTINCT p.id) FROM poems p
|
||||
LEFT JOIN reading_records rr ON p.id = rr.poem_id
|
||||
WHERE {where_clause}
|
||||
'''
|
||||
cursor.execute(count_sql, params)
|
||||
total = cursor.fetchone()[0]
|
||||
|
||||
# 查询数据
|
||||
sql = f'''
|
||||
SELECT p.*, rr.is_read, rr.read_at
|
||||
FROM poems p
|
||||
LEFT JOIN reading_records rr ON p.id = rr.poem_id
|
||||
WHERE {where_clause}
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
'''
|
||||
params.extend([page_size, (page - 1) * page_size])
|
||||
cursor.execute(sql, params)
|
||||
poems = cursor.fetchall()
|
||||
|
||||
# 获取每首诗的分类
|
||||
poem_ids = [row['id'] for row in poems]
|
||||
classifications = {}
|
||||
if poem_ids:
|
||||
cursor.execute('''
|
||||
SELECT poem_id, category, tags FROM classifications
|
||||
WHERE poem_id IN ({})
|
||||
'''.format(','.join('?' * len(poem_ids))), poem_ids)
|
||||
|
||||
for clf in cursor.fetchall():
|
||||
if clf['poem_id'] not in classifications:
|
||||
classifications[clf['poem_id']] = []
|
||||
classifications[clf['poem_id']].append(clf)
|
||||
|
||||
# 转换结果
|
||||
result = []
|
||||
for row in poems:
|
||||
poem_dict = {
|
||||
'id': row['id'],
|
||||
'title': row['title'],
|
||||
'author': row['author'],
|
||||
'paragraphs': json.loads(row['paragraphs']) if row['paragraphs'] else [],
|
||||
'created_at': row['created_at'],
|
||||
'updated_at': row['updated_at'],
|
||||
'is_read': bool(row['is_read']),
|
||||
'read_at': row['read_at'],
|
||||
'classifications': {}
|
||||
}
|
||||
|
||||
# 添加分类
|
||||
if row['id'] in classifications:
|
||||
for clf in classifications[row['id']]:
|
||||
poem_dict['classifications'][clf['category']] = json.loads(clf['tags'])
|
||||
|
||||
result.append(poem_dict)
|
||||
|
||||
return {
|
||||
'total': total,
|
||||
'page': page,
|
||||
'page_size': page_size,
|
||||
'total_pages': (total + page_size - 1) // page_size,
|
||||
'poems': result
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/poems/random")
|
||||
async def get_random_poem():
|
||||
"""获取随机一首诗词"""
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute('SELECT * FROM poems ORDER BY RANDOM() LIMIT 1')
|
||||
poem = cursor.fetchone()
|
||||
|
||||
if not poem:
|
||||
raise HTTPException(status_code=404, detail="没有可用的诗词")
|
||||
|
||||
cursor.execute('SELECT * FROM classifications WHERE poem_id = ?', (poem['id'],))
|
||||
classifications = cursor.fetchall()
|
||||
|
||||
cursor.execute('SELECT * FROM reading_records WHERE poem_id = ?', (poem['id'],))
|
||||
reading_record = cursor.fetchone()
|
||||
|
||||
return poem_to_dict(poem, classifications, reading_record)
|
||||
|
||||
|
||||
@app.get("/api/poems/{poem_id}")
|
||||
async def get_poem(poem_id: str):
|
||||
"""获取单首诗词详情"""
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取诗词
|
||||
cursor.execute('SELECT * FROM poems WHERE id = ?', (poem_id,))
|
||||
poem = cursor.fetchone()
|
||||
|
||||
if not poem:
|
||||
raise HTTPException(status_code=404, detail="诗词不存在")
|
||||
|
||||
# 获取分类
|
||||
cursor.execute('SELECT * FROM classifications WHERE poem_id = ?', (poem_id,))
|
||||
classifications = cursor.fetchall()
|
||||
|
||||
# 获取阅读记录
|
||||
cursor.execute('SELECT * FROM reading_records WHERE poem_id = ?', (poem_id,))
|
||||
reading_record = cursor.fetchone()
|
||||
|
||||
return poem_to_dict(poem, classifications, reading_record)
|
||||
|
||||
|
||||
@app.put("/api/poems/{poem_id}/read")
|
||||
async def toggle_read_status(poem_id: str, is_read: bool = Query(...)):
|
||||
"""切换阅读状态"""
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 检查诗词是否存在
|
||||
cursor.execute('SELECT id FROM poems WHERE id = ?', (poem_id,))
|
||||
if not cursor.fetchone():
|
||||
raise HTTPException(status_code=404, detail="诗词不存在")
|
||||
|
||||
# 更新或插入阅读记录
|
||||
read_at = datetime.now().isoformat() if is_read else None
|
||||
cursor.execute('''
|
||||
INSERT INTO reading_records (poem_id, is_read, read_at)
|
||||
VALUES (?, ?, ?)
|
||||
ON CONFLICT(poem_id) DO UPDATE SET
|
||||
is_read = excluded.is_read,
|
||||
read_at = excluded.read_at
|
||||
''', (poem_id, 1 if is_read else 0, read_at))
|
||||
|
||||
conn.commit()
|
||||
|
||||
return {'success': True, 'is_read': is_read, 'read_at': read_at}
|
||||
|
||||
|
||||
@app.get("/api/tags/cloud")
|
||||
async def get_tag_cloud():
|
||||
"""获取标签云数据"""
|
||||
with get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
SELECT category, tags FROM classifications
|
||||
''')
|
||||
|
||||
tag_counts = {}
|
||||
for row in cursor.fetchall():
|
||||
tags = json.loads(row['tags'])
|
||||
for tag in tags:
|
||||
key = f"{row['category']}:{tag}"
|
||||
if key not in tag_counts:
|
||||
tag_counts[key] = {
|
||||
'category': row['category'],
|
||||
'tag': tag,
|
||||
'count': 0
|
||||
}
|
||||
tag_counts[key]['count'] += 1
|
||||
|
||||
return {
|
||||
'tags': list(tag_counts.values()),
|
||||
'categories': {k: v['name'] for k, v in CATEGORY_SYSTEM.items()}
|
||||
}
|
||||
|
||||
|
||||
# 启动时初始化数据库
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
init_database()
|
||||
print(f"\n数据库路径:{DB_PATH}")
|
||||
print(f"前端目录:{FRONTEND_DIR}")
|
||||
print("\nAPI 端点:")
|
||||
print(" GET / - 前端页面")
|
||||
print(" GET /api/categories - 分类体系")
|
||||
print(" GET /api/stats - 统计数据")
|
||||
print(" POST /api/poems/import - 批量导入")
|
||||
print(" GET /api/poems - 诗词列表")
|
||||
print(" GET /api/poems/{id} - 诗词详情")
|
||||
print(" PUT /api/poems/{id}/read - 切换阅读状态")
|
||||
print(" GET /api/poems/random - 随机诗词")
|
||||
print(" GET /api/tags/cloud - 标签云")
|
||||
print("\n按 Ctrl+C 停止服务\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(app, host="0.0.0.0", port=8000)
|
||||
Reference in New Issue
Block a user