feat: add book detail, notes detail, recommendation page and MySQL schema

新增功能:
- 书籍详情弹窗:点击书架书籍展示详细信息(书名、作者、出版社、ISBN、字数、评分、简介、阅读进度),数据实时从微信读书 API 获取并入库
- 笔记详情弹窗:点击有笔记的书展示划线和想法/点评,按章节分组展示,数据实时获取并入库
- 推荐页面:个性化推荐(为你推荐)+ 关键词搜索书籍,数据从微信读书 API 获取并入库
- 修复 Flask 中文 URL 参数编码问题

后端新增 API:
- GET /api/book/detail?book_id=xxx — 书籍详情
- GET /api/book/notes?book_id=xxx — 笔记详情(划线+想法)
- GET /api/recommend?maxIdx=0 — 个性化推荐
- GET /api/search?keyword=xxx — 搜索书籍

数据库新增表:
- book_details, book_progress, book_highlights, book_chapters
- book_reviews, recommended_books, search_results

MySQL 版本建表语句 schema_mysql.sql:
- 18 张表的完整 DDL,含索引、字符集 utf8mb4、字段注释
- 适用于后续从 SQLite 迁移至 MySQL
This commit is contained in:
admin committed 2026-07-17 10:07:05 +08:00
1 parent 1e9c537b86
commit ebfa3a1852
6 files changed
+1660 -4

No files matched your search

+475
View File
@@ -8,6 +8,7 @@ import json
import os
import sqlite3
import time
import urllib.request
from datetime import datetime
from flask import Flask, jsonify, render_template, request
@@ -16,6 +17,80 @@ app = Flask(__name__, static_folder="static", template_folder="templates")
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weread.db")
WEREAD_API_URL = "https://i.weread.qq.com/api/agent/gateway"
WEREAD_API_KEY = os.environ.get("WEREAD_API_KEY", "")
WEREAD_SKILL_VERSION = "1.0.3"
def weread_api_call(api_name, **kwargs):
"""调用微信读书 API(按需获取数据时使用)"""
body = {"api_name": api_name, "skill_version": WEREAD_SKILL_VERSION}
body.update(kwargs)
req = urllib.request.Request(
WEREAD_API_URL,
data=json.dumps(body).encode("utf-8"),
headers={
"Authorization": f"Bearer {WEREAD_API_KEY}",
"Content-Type": "application/json",
},
method="POST",
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode("utf-8"))
def ensure_tables(conn):
"""确保按需数据表存在"""
c = conn.cursor()
c.executescript("""
CREATE TABLE IF NOT EXISTS book_details (
book_id TEXT PRIMARY KEY,
title TEXT, author TEXT, translator TEXT, cover TEXT,
intro TEXT, category TEXT, publisher TEXT, publish_time TEXT,
isbn TEXT, word_count INTEGER, new_rating REAL,
new_rating_count INTEGER, fetched_at INTEGER
);
CREATE TABLE IF NOT EXISTS book_progress (
book_id TEXT PRIMARY KEY,
progress INTEGER, chapter_uid INTEGER, chapter_offset INTEGER,
update_time INTEGER, record_reading_time INTEGER,
finish_time INTEGER, is_start_reading INTEGER, fetched_at INTEGER
);
CREATE TABLE IF NOT EXISTS book_highlights (
id INTEGER PRIMARY KEY AUTOINCREMENT,
book_id TEXT, bookmark_id TEXT, chapter_uid INTEGER,
mark_text TEXT, create_time INTEGER, type INTEGER,
range_text TEXT, color_style INTEGER, fetched_at INTEGER
);
CREATE TABLE IF NOT EXISTS book_chapters (
id INTEGER PRIMARY KEY AUTOINCREMENT,
book_id TEXT, chapter_uid INTEGER, chapter_idx INTEGER,
title TEXT, UNIQUE(book_id, chapter_uid)
);
CREATE TABLE IF NOT EXISTS book_reviews (
id INTEGER PRIMARY KEY AUTOINCREMENT,
book_id TEXT, review_id TEXT, content TEXT,
create_time INTEGER, star INTEGER, chapter_name TEXT,
is_finish INTEGER, fetched_at INTEGER
);
CREATE TABLE IF NOT EXISTS recommended_books (
id INTEGER PRIMARY KEY AUTOINCREMENT,
book_id TEXT, title TEXT, author TEXT, cover TEXT,
intro TEXT, category TEXT, reason TEXT,
reading_count INTEGER, new_rating REAL,
new_rating_count INTEGER, rating_title TEXT, fetched_at INTEGER
);
CREATE TABLE IF NOT EXISTS search_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
keyword TEXT, book_id TEXT, title TEXT, author TEXT,
cover TEXT, intro TEXT, publisher TEXT, category TEXT,
reading_count INTEGER, new_rating REAL,
new_rating_count INTEGER, rating_title TEXT,
search_idx INTEGER, fetched_at INTEGER
);
""")
conn.commit()
def get_db():
conn = sqlite3.connect(DB_PATH)
@@ -371,5 +446,405 @@ def api_archives():
return jsonify(archives)
# ===== 按需获取:书籍详情 =====
@app.route("/api/book/detail")
def api_book_detail():
"""书籍详情:实时调微信读书 API 获取,入库后返回"""
book_id = request.args.get("book_id", "")
if not book_id:
return jsonify({"error": "book_id is required"}), 400
conn = get_db()
ensure_tables(conn)
c = conn.cursor()
now = int(time.time())
# 1. 获取书籍信息
try:
info = weread_api_call("/book/info", bookId=book_id)
c.execute(
"""INSERT OR REPLACE INTO book_details
(book_id, title, author, translator, cover, intro, category,
publisher, publish_time, isbn, word_count, new_rating,
new_rating_count, fetched_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
book_id,
info.get("title", ""),
info.get("author", ""),
info.get("translator", ""),
info.get("cover", ""),
info.get("intro", ""),
info.get("category", ""),
info.get("publisher", ""),
info.get("publishTime", ""),
info.get("isbn", ""),
info.get("wordCount", 0),
info.get("newRating", 0),
info.get("newRatingCount", 0),
now,
),
)
conn.commit()
except Exception as e:
# API 调用失败时尝试从数据库读取缓存
c.execute("SELECT * FROM book_details WHERE book_id=?", (book_id,))
cached = c.fetchone()
if not cached:
return jsonify({"error": f"获取书籍信息失败: {str(e)}"}), 500
# 2. 获取阅读进度
progress_data = {}
try:
prog = weread_api_call("/book/getprogress", bookId=book_id)
book_prog = prog.get("book", {})
c.execute(
"""INSERT OR REPLACE INTO book_progress
(book_id, progress, chapter_uid, chapter_offset, update_time,
record_reading_time, finish_time, is_start_reading, fetched_at)
VALUES (?,?,?,?,?,?,?,?,?)""",
(
book_id,
book_prog.get("progress", 0),
book_prog.get("chapterUid", 0),
book_prog.get("chapterOffset", 0),
book_prog.get("updateTime", 0),
book_prog.get("recordReadingTime", 0),
book_prog.get("finishTime", 0),
book_prog.get("isStartReading", 0),
now,
),
)
conn.commit()
progress_data = {
"progress": book_prog.get("progress", 0),
"record_reading_time": book_prog.get("recordReadingTime", 0),
"record_reading_time_str": format_duration(book_prog.get("recordReadingTime", 0)),
"update_time": ts_to_date(book_prog.get("updateTime", 0)),
"finish_time": ts_to_date(book_prog.get("finishTime", 0)),
"is_start_reading": book_prog.get("isStartReading", 0),
}
except Exception:
c.execute("SELECT * FROM book_progress WHERE book_id=?", (book_id,))
cached = c.fetchone()
if cached:
progress_data = {
"progress": cached["progress"],
"record_reading_time": cached["record_reading_time"],
"record_reading_time_str": format_duration(cached["record_reading_time"]),
"update_time": ts_to_date(cached["update_time"]),
"finish_time": ts_to_date(cached["finish_time"]),
"is_start_reading": cached["is_start_reading"],
}
# 3. 组装返回
c.execute("SELECT * FROM book_details WHERE book_id=?", (book_id,))
detail = c.fetchone()
conn.close()
if not detail:
return jsonify({"error": "数据获取失败"}), 500
return jsonify({
"book_id": book_id,
"title": detail["title"],
"author": detail["author"],
"translator": detail["translator"],
"cover": detail["cover"],
"intro": detail["intro"],
"category": detail["category"],
"publisher": detail["publisher"],
"publish_time": detail["publish_time"],
"isbn": detail["isbn"],
"word_count": detail["word_count"],
"new_rating": detail["new_rating"],
"new_rating_count": detail["new_rating_count"],
"progress": progress_data,
"deep_link": f"weread://reading?bId={book_id}",
})
# ===== 按需获取:笔记详情 =====
@app.route("/api/book/notes")
def api_book_notes():
"""笔记详情:实时调微信读书 API 获取划线+想法,入库后返回"""
book_id = request.args.get("book_id", "")
if not book_id:
return jsonify({"error": "book_id is required"}), 400
conn = get_db()
ensure_tables(conn)
c = conn.cursor()
now = int(time.time())
# 1. 获取划线
highlights = []
chapters = {}
try:
data = weread_api_call("/book/bookmarklist", bookId=book_id)
# 保存章节信息
for ch in data.get("chapters", []):
c.execute(
"""INSERT OR REPLACE INTO book_chapters
(book_id, chapter_uid, chapter_idx, title)
VALUES (?,?,?,?)""",
(book_id, ch.get("chapterUid", 0), ch.get("chapterIdx", 0), ch.get("title", "")),
)
chapters[ch.get("chapterUid", 0)] = ch.get("title", "")
# 保存划线
c.execute("DELETE FROM book_highlights WHERE book_id=?", (book_id,))
for bm in data.get("updated", []):
c.execute(
"""INSERT INTO book_highlights
(book_id, bookmark_id, chapter_uid, mark_text, create_time,
type, range_text, color_style, fetched_at)
VALUES (?,?,?,?,?,?,?,?,?)""",
(
book_id,
bm.get("bookmarkId", ""),
bm.get("chapterUid", 0),
bm.get("markText", ""),
bm.get("createTime", 0),
bm.get("type", 0),
bm.get("range", ""),
bm.get("colorStyle", 0),
now,
),
)
highlights.append({
"bookmark_id": bm.get("bookmarkId", ""),
"chapter_uid": bm.get("chapterUid", 0),
"chapter_title": chapters.get(bm.get("chapterUid", 0), ""),
"mark_text": bm.get("markText", ""),
"create_time": ts_to_date(bm.get("createTime", 0)),
"range": bm.get("range", ""),
})
conn.commit()
except Exception as e:
# 从数据库读取缓存
c.execute("SELECT * FROM book_highlights WHERE book_id=? ORDER BY create_time", (book_id,))
for row in c.fetchall():
c2 = conn.cursor()
c2.execute("SELECT title FROM book_chapters WHERE book_id=? AND chapter_uid=?", (book_id, row["chapter_uid"]))
ch_row = c2.fetchone()
highlights.append({
"bookmark_id": row["bookmark_id"],
"chapter_uid": row["chapter_uid"],
"chapter_title": ch_row["title"] if ch_row else "",
"mark_text": row["mark_text"],
"create_time": ts_to_date(row["create_time"]),
"range": row["range_text"],
})
# 2. 获取想法/点评
reviews = []
try:
rev_data = weread_api_call("/review/list/mine", bookid=book_id, count=100)
c.execute("DELETE FROM book_reviews WHERE book_id=?", (book_id,))
for item in rev_data.get("reviews", []):
rev = item.get("review", {})
c.execute(
"""INSERT INTO book_reviews
(book_id, review_id, content, create_time, star,
chapter_name, is_finish, fetched_at)
VALUES (?,?,?,?,?,?,?,?)""",
(
book_id,
rev.get("reviewId", ""),
rev.get("content", ""),
rev.get("createTime", 0),
rev.get("star", 0),
rev.get("chapterName", ""),
rev.get("isFinish", 0),
now,
),
)
reviews.append({
"review_id": rev.get("reviewId", ""),
"content": rev.get("content", ""),
"create_time": ts_to_date(rev.get("createTime", 0)),
"star": rev.get("star", 0),
"chapter_name": rev.get("chapterName", ""),
"is_finish": rev.get("isFinish", 0),
})
conn.commit()
except Exception:
c.execute("SELECT * FROM book_reviews WHERE book_id=? ORDER BY create_time DESC", (book_id,))
for row in c.fetchall():
reviews.append({
"review_id": row["review_id"],
"content": row["content"],
"create_time": ts_to_date(row["create_time"]),
"star": row["star"],
"chapter_name": row["chapter_name"],
"is_finish": row["is_finish"],
})
conn.close()
# 按章节分组划线
grouped_highlights = {}
for h in highlights:
ch_title = h["chapter_title"] or f"章节{h['chapter_uid']}"
if ch_title not in grouped_highlights:
grouped_highlights[ch_title] = []
grouped_highlights[ch_title].append(h)
return jsonify({
"book_id": book_id,
"highlights": highlights,
"highlight_count": len(highlights),
"grouped_highlights": grouped_highlights,
"reviews": reviews,
"review_count": len(reviews),
})
# ===== 推荐书籍 =====
@app.route("/api/recommend")
def api_recommend():
"""个性化推荐:实时调微信读书 API,入库后返回"""
max_idx = int(request.args.get("maxIdx", 0))
conn = get_db()
ensure_tables(conn)
c = conn.cursor()
now = int(time.time())
try:
data = weread_api_call("/book/recommend", count=12, maxIdx=max_idx)
books = data.get("books", [])
for book in books:
rating_detail = book.get("newRatingDetail", {})
c.execute(
"""INSERT OR REPLACE INTO recommended_books
(book_id, title, author, cover, intro, category, reason,
reading_count, new_rating, new_rating_count, rating_title, fetched_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)""",
(
book.get("bookId", ""),
book.get("title", ""),
book.get("author", ""),
book.get("cover", ""),
book.get("intro", ""),
book.get("category", ""),
book.get("reason", ""),
book.get("readingCount", 0),
book.get("newRating", 0),
book.get("newRatingCount", 0),
rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
now,
),
)
conn.commit()
except Exception as e:
return jsonify({"error": f"获取推荐失败: {str(e)}"}), 500
conn.close()
result = []
for book in books:
rating_detail = book.get("newRatingDetail", {})
result.append({
"book_id": book.get("bookId", ""),
"title": book.get("title", ""),
"author": book.get("author", ""),
"cover": book.get("cover", ""),
"intro": book.get("intro", ""),
"category": book.get("category", ""),
"reason": book.get("reason", ""),
"reading_count": book.get("readingCount", 0),
"new_rating": book.get("newRating", 0),
"new_rating_count": book.get("newRatingCount", 0),
"rating_title": rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
})
return jsonify({
"books": result,
"has_more": data.get("hasMore", 0),
"last_search_idx": books[-1].get("searchIdx", 0) if books else 0,
})
# ===== 搜索书籍 =====
@app.route("/api/search")
def api_search():
"""搜索书籍:实时调微信读书 API,入库后返回"""
keyword = request.args.get("keyword", "").strip()
# 修复 Flask URL 参数中文编码问题
try:
keyword = keyword.encode("latin-1").decode("utf-8")
except (UnicodeDecodeError, UnicodeEncodeError):
pass
if not keyword:
return jsonify({"error": "keyword is required"}), 400
max_idx = int(request.args.get("maxIdx", 0))
conn = get_db()
ensure_tables(conn)
c = conn.cursor()
now = int(time.time())
try:
data = weread_api_call("/store/search", keyword=keyword, scope=10, maxIdx=max_idx)
results = data.get("results", [])
books = []
for group in results:
for item in group.get("books", []):
bi = item.get("bookInfo", {})
rating_detail = item.get("newRatingDetail", {})
c.execute(
"""INSERT OR REPLACE INTO search_results
(keyword, book_id, title, author, cover, intro, publisher,
category, reading_count, new_rating, new_rating_count,
rating_title, search_idx, fetched_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
keyword,
bi.get("bookId", ""),
bi.get("title", ""),
bi.get("author", ""),
bi.get("cover", ""),
bi.get("intro", ""),
bi.get("publisher", ""),
bi.get("category", ""),
item.get("readingCount", 0),
item.get("newRating", 0),
item.get("newRatingCount", 0),
rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
item.get("searchIdx", 0),
now,
),
)
books.append({
"book_id": bi.get("bookId", ""),
"title": bi.get("title", ""),
"author": bi.get("author", ""),
"cover": bi.get("cover", ""),
"intro": bi.get("intro", ""),
"publisher": bi.get("publisher", ""),
"category": bi.get("category", ""),
"reading_count": item.get("readingCount", 0),
"new_rating": item.get("newRating", 0),
"new_rating_count": item.get("newRatingCount", 0),
"rating_title": rating_detail.get("title", "") if isinstance(rating_detail, dict) else "",
})
conn.commit()
except Exception as e:
return jsonify({"error": f"搜索失败: {str(e)}"}), 500
conn.close()
return jsonify({
"keyword": keyword,
"books": books,
"has_more": data.get("hasMore", 0),
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8080, debug=False)