From 2a809eb7cce44e3193439a4cc1b401b692841b44 Mon Sep 17 00:00:00 2001 From: kinghwj Date: Fri, 17 Jul 2026 10:37:07 +0800 Subject: [PATCH 1/2] refactor: complete data model v2.0 based on official API docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 数据模型全面重构,基于微信读书官方 8 份 API 文档逐字段比对: 1. 表结构从 18 张扩展到 31 张表: 新增表:shelf_albums(有声书)、shelf_meta(书架元信息)、prefer_publishers(偏好出版社)、 prefer_cps(偏好版权方)、prefer_books(偏好书籍卡片)、read_rank(好友排行)、 medals(勋章)、year_reports(年度报告)、book_best_bookmarks(热门划线)、 book_underlines(划线热度)、similar_books(相似推荐)、public_reviews(公开点评)、 search_sessions(搜索会话) 2. 补全所有遗漏字段: - reading_stats: +13 个字段 (baseTime, compare, readRate, wrReadTime, wrListenTime 等) - book_chapters: +8 个字段 (wordCount, level, price, paid, isMPChapter, anchors 等) - book_reviews: +4 个字段 (abstract, range, chapterUid, chapterIdx) - recommended_books: +5 个字段 (deepLink, searchIdx, price, payType, type) - search_results: +8 个字段 (deepLink, payType, price, soldout, scope 等) - book_details: +2 个字段 (deepLink, newRatingDetail) - prefer_categories: +2 个字段 (parentCategoryId, categoryType) 3. 时间戳双列设计: - 所有整型时间戳字段均伴随 _dt TIMESTAMP 列 (如 read_update_time + read_update_time_dt) - 便于直接查询和展示,无需应用层转换 4. 审计字段: - 每张表均维护 created_at 和 updated_at - MySQL 使用 DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP 5. 索引优化: - 主键、唯一约束、查询索引根据实际访问模式重新设计 - 新增复合索引、排序索引、全文检索索引 6. 数据采集脚本更新: - 覆盖所有新增表和字段的数据采集 - skill_version 升级至 1.0.4 - 有声书/专辑数据采集 (shelf_albums) - 偏好出版社/版权方/书籍卡片采集 - 勋章、年度报告数据采集 7. 后端适配: - 移除旧版 ensure_tables 动态建表逻辑 - 所有按需 API INSERT 语句适配新表结构 - 搜索结果入库新增 search_sessions 会话表 - 推荐书籍入库新增 batch_id 批次标识 --- .workbuddy/memory/2026-07-17.md | 11 + app.py | 267 +++----- fetch_data.py | 1087 ++++++++++++++++++++++--------- migrate_to_mysql.py | 235 ++++--- schema_mysql.sql | 859 ++++++++++++++++-------- 5 files changed, 1595 insertions(+), 864 deletions(-) diff --git a/.workbuddy/memory/2026-07-17.md b/.workbuddy/memory/2026-07-17.md index 6557d0a..bbe4bae 100644 --- a/.workbuddy/memory/2026-07-17.md +++ b/.workbuddy/memory/2026-07-17.md @@ -25,3 +25,14 @@ - 时间戳列自动转换(SQLite int -> MySQL TIMESTAMP) - 中文内容 utf8mb4 完整保存,数据校验通过 - Flask 服务暂未切换至 MySQL(用户要求服务不重启) + +## 第三批功能迭代(MySQL 切换 + 文档 + 告警) +- **数据源切换至 MySQL**:app.py 从 SQLite 全面切换到 PyMySQL,所有 API 读取 MySQL weread_dashboard 数据库 +- **db_required 装饰器**:统一管理数据库连接和异常处理,MySQL 连接失败返回 HTTP 503 +- **健康检查接口**:新增 /api/health,前端页面加载时优先检测 MySQL 连接状态 +- **前端告警横幅**:MySQL 连接失败时显示黄色告警横幅,阻止后续数据加载 +- **修复 fetched_at 兼容问题**:Unix 整数时间戳改为 MySQL TIMESTAMP 格式字符串 +- **代码中文注释**:app.py 所有函数/API/工具函数添加完整中文文档注释;fetch_data.py 所有建表语句添加列级中文注释 +- **技术文档** docs/TECHNICAL_DOC.md:项目概述、系统架构图、数据流设计、15 个 API 接口详情、部署指南 +- **元数据文档** docs/METADATA_DOC.md:18 张表完整字段说明、索引设计、表关系 ER 图、数据类型规范 +- Git 提交:dev 分支提交 → master 分支 --no-ff 合并 diff --git a/app.py b/app.py index d45213d..60dd565 100644 --- a/app.py +++ b/app.py @@ -49,7 +49,7 @@ MYSQL_CONFIG = { # ====================================================================== 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" +WEREAD_SKILL_VERSION = "1.0.4" # ====================================================================== @@ -208,137 +208,21 @@ def format_duration(seconds): return f"{minutes}分钟" -def ensure_tables(cursor): +def ts_to_dt_str(ts): """ - 确保按需数据表存在(MySQL 版本)。 - 这些表存储用户点击书籍详情/笔记时实时获取的数据, - 不在初始化采集脚本中创建,而是首次访问时按需创建。 + Unix 时间戳转完整日期时间字符串 (中国时区 UTC+8)。 + 用于写入数据库的 _dt 列。 Args: - cursor: MySQL 游标对象 + ts (int): Unix 时间戳(秒) + Returns: + str: 格式化日期时间 "YYYY-MM-DD HH:MM:SS",空值返回 None """ - cursor.executescript = None # MySQL 无 executescript,逐条执行 - tables_sql = [ - # 书籍详细信息表(来自微信读书 /book/info 接口) - """CREATE TABLE IF NOT EXISTS `book_details` ( - `book_id` VARCHAR(64) NOT NULL, - `title` VARCHAR(255) DEFAULT NULL COMMENT '书名', - `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', - `translator` VARCHAR(255) DEFAULT NULL COMMENT '译者', - `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图URL', - `intro` LONGTEXT DEFAULT NULL COMMENT '书籍简介', - `category` VARCHAR(128) DEFAULT NULL COMMENT '分类', - `publisher` VARCHAR(255) DEFAULT NULL COMMENT '出版社', - `publish_time` VARCHAR(32) DEFAULT NULL COMMENT '出版时间', - `isbn` VARCHAR(32) DEFAULT NULL COMMENT 'ISBN号', - `word_count` INT UNSIGNED DEFAULT 0 COMMENT '总字数', - `new_rating` DECIMAL(5,1) DEFAULT 0 COMMENT '评分(百分制)', - `new_rating_count` INT UNSIGNED DEFAULT 0 COMMENT '评分人数', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获取时间', - PRIMARY KEY (`book_id`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='书籍详细信息'""", - - # 阅读进度表(来自微信读书 /book/getprogress 接口) - """CREATE TABLE IF NOT EXISTS `book_progress` ( - `book_id` VARCHAR(64) NOT NULL, - `progress` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读进度百分比(0-100)', - `chapter_uid` INT UNSIGNED DEFAULT NULL COMMENT '当前阅读章节UID', - `chapter_offset` INT UNSIGNED DEFAULT NULL COMMENT '当前章节内偏移量', - `update_time` INT UNSIGNED DEFAULT NULL COMMENT '最后阅读时间(Unix时间戳)', - `record_reading_time` INT UNSIGNED DEFAULT 0 COMMENT '累计阅读时长(秒)', - `finish_time` INT UNSIGNED DEFAULT NULL COMMENT '读完时间(Unix时间戳)', - `is_start_reading` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已开始阅读(1=是)', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获取时间', - PRIMARY KEY (`book_id`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='阅读进度'""", - - # 划线内容表(来自微信读书 /book/bookmarklist 接口) - """CREATE TABLE IF NOT EXISTS `book_highlights` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `book_id` VARCHAR(64) NOT NULL COMMENT '书籍ID', - `bookmark_id` VARCHAR(64) DEFAULT NULL COMMENT '划线唯一ID', - `chapter_uid` INT UNSIGNED DEFAULT NULL COMMENT '所在章节UID', - `mark_text` LONGTEXT DEFAULT NULL COMMENT '划线原文内容', - `create_time` INT UNSIGNED DEFAULT NULL COMMENT '创建时间(Unix时间戳)', - `type` TINYINT DEFAULT NULL COMMENT '划线类型', - `range_text` VARCHAR(64) DEFAULT NULL COMMENT '位置范围', - `color_style` TINYINT DEFAULT NULL COMMENT '划线颜色样式', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获取时间', - PRIMARY KEY (`id`), - INDEX `idx_highlights_book_id` (`book_id`), - INDEX `idx_highlights_chapter` (`book_id`, `chapter_uid`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='划线内容'""", - - # 章节信息表(来自微信读书 /book/bookmarklist 接口的 chapters 字段) - """CREATE TABLE IF NOT EXISTS `book_chapters` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `book_id` VARCHAR(64) NOT NULL COMMENT '书籍ID', - `chapter_uid` INT UNSIGNED NOT NULL COMMENT '章节UID', - `chapter_idx` INT UNSIGNED DEFAULT NULL COMMENT '章节序号', - `title` VARCHAR(255) DEFAULT NULL COMMENT '章节标题', - PRIMARY KEY (`id`), - UNIQUE KEY `uk_book_chapter` (`book_id`, `chapter_uid`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='章节信息'""", - - # 个人想法/点评表(来自微信读书 /review/list/mine 接口) - """CREATE TABLE IF NOT EXISTS `book_reviews` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `book_id` VARCHAR(64) NOT NULL COMMENT '书籍ID', - `review_id` VARCHAR(64) DEFAULT NULL COMMENT '想法唯一ID', - `content` LONGTEXT DEFAULT NULL COMMENT '想法内容文本', - `create_time` INT UNSIGNED DEFAULT NULL COMMENT '创建时间(Unix时间戳)', - `star` TINYINT DEFAULT -1 COMMENT '评分(0-5, -1=无评分)', - `chapter_name` VARCHAR(255) DEFAULT NULL COMMENT '所在章节名称', - `is_finish` TINYINT(1) DEFAULT 0 COMMENT '是否书评(1=书评)', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获取时间', - PRIMARY KEY (`id`), - INDEX `idx_reviews_book_id` (`book_id`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个人想法/点评'""", - - # 推荐书籍表(来自微信读书 /book/recommend 接口) - """CREATE TABLE IF NOT EXISTS `recommended_books` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `book_id` VARCHAR(64) NOT NULL COMMENT '书籍ID', - `title` VARCHAR(255) DEFAULT NULL COMMENT '书名', - `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', - `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图URL', - `intro` TEXT DEFAULT NULL COMMENT '书籍简介', - `category` VARCHAR(128) DEFAULT NULL COMMENT '分类', - `reason` VARCHAR(512) DEFAULT NULL COMMENT '推荐理由', - `reading_count` INT UNSIGNED DEFAULT 0 COMMENT '在读人数', - `new_rating` DECIMAL(5,1) DEFAULT 0 COMMENT '评分(百分制)', - `new_rating_count` INT UNSIGNED DEFAULT 0 COMMENT '评分人数', - `rating_title` VARCHAR(32) DEFAULT NULL COMMENT '评分标签(神作/力荐等)', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获取时间', - PRIMARY KEY (`id`), - INDEX `idx_recommended_book_id` (`book_id`), - INDEX `idx_recommended_fetched` (`fetched_at` DESC) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐书籍'""", - - # 搜索结果表(来自微信读书 /store/search 接口) - """CREATE TABLE IF NOT EXISTS `search_results` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `keyword` VARCHAR(255) NOT NULL COMMENT '搜索关键词', - `book_id` VARCHAR(64) NOT NULL COMMENT '书籍ID', - `title` VARCHAR(255) DEFAULT NULL COMMENT '书名', - `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', - `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图URL', - `intro` TEXT DEFAULT NULL COMMENT '书籍简介', - `publisher` VARCHAR(255) DEFAULT NULL COMMENT '出版社', - `category` VARCHAR(128) DEFAULT NULL COMMENT '分类', - `reading_count` INT UNSIGNED DEFAULT 0 COMMENT '在读人数', - `new_rating` DECIMAL(5,1) DEFAULT 0 COMMENT '评分(百分制)', - `new_rating_count` INT UNSIGNED DEFAULT 0 COMMENT '评分人数', - `rating_title` VARCHAR(32) DEFAULT NULL COMMENT '评分标签', - `search_idx` INT UNSIGNED DEFAULT 0 COMMENT '搜索序号', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获取时间', - PRIMARY KEY (`id`), - INDEX `idx_search_keyword` (`keyword`), - INDEX `idx_search_book_id` (`book_id`) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='搜索结果'""", - ] - for sql in tables_sql: - cursor.execute(sql) + if not ts or ts == 0: + return None + from datetime import timezone, timedelta + cst = timezone(timedelta(hours=8)) + return datetime.fromtimestamp(ts, tz=cst).strftime("%Y-%m-%d %H:%M:%S") # ====================================================================== @@ -794,18 +678,17 @@ def api_book_detail(conn): return jsonify({"error": "book_id is required"}), 400 cursor = conn.cursor() - ensure_tables(cursor) - conn.commit() - now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - # 1. 获取书籍基本信息 + # 1. 获取书籍基本信息(/book/info) try: info = weread_api_call("/book/info", bookId=book_id) + rating_detail = info.get("newRatingDetail") cursor.execute( "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 (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + "publisher, publish_time, isbn, word_count, new_rating, new_rating_count, " + "new_rating_detail, deep_link) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", ( book_id, info.get("title", ""), @@ -820,7 +703,8 @@ def api_book_detail(conn): info.get("wordCount", 0), info.get("newRating", 0), info.get("newRatingCount", 0), - now, + json.dumps(rating_detail, ensure_ascii=False) if rating_detail else None, + info.get("deepLink", ""), ), ) conn.commit() @@ -836,21 +720,25 @@ def api_book_detail(conn): try: prog = weread_api_call("/book/getprogress", bookId=book_id) book_prog = prog.get("book", {}) + srv_ts = prog.get("timestamp") + ut = book_prog.get("updateTime", 0) + ft = book_prog.get("finishTime", 0) cursor.execute( "REPLACE INTO book_progress " - "(book_id, progress, chapter_uid, chapter_offset, update_time, " - "record_reading_time, finish_time, is_start_reading, fetched_at) " - "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)", + "(book_id, progress, chapter_uid, chapter_offset, update_time, update_time_dt, " + "record_reading_time, finish_time, finish_time_dt, is_start_reading, " + "server_timestamp, server_timestamp_dt) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", ( book_id, book_prog.get("progress", 0), book_prog.get("chapterUid", 0), book_prog.get("chapterOffset", 0), - book_prog.get("updateTime", 0), + ut, ts_to_dt_str(ut), book_prog.get("recordReadingTime", 0), - book_prog.get("finishTime", 0), + ft, ts_to_dt_str(ft), book_prog.get("isStartReading", 0), - now, + srv_ts, ts_to_dt_str(srv_ts), ), ) conn.commit() @@ -898,7 +786,7 @@ def api_book_detail(conn): "new_rating": float(detail["new_rating"]) if detail["new_rating"] else 0, "new_rating_count": detail["new_rating_count"], "progress": progress_data, - "deep_link": f"weread://reading?bId={book_id}", + "deep_link": detail["deep_link"] or f"weread://reading?bId={book_id}", }) @@ -926,20 +814,18 @@ def api_book_notes(conn): return jsonify({"error": "book_id is required"}), 400 cursor = conn.cursor() - ensure_tables(cursor) - conn.commit() - now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - # 1. 获取划线数据 + # 1. 获取划线数据(/book/bookmarklist) highlights = [] chapters = {} try: data = weread_api_call("/book/bookmarklist", bookId=book_id) - # 保存章节信息 + # 保存章节信息(包含完整字段) for ch in data.get("chapters", []): cursor.execute( - "REPLACE INTO book_chapters (book_id, chapter_uid, chapter_idx, title) " + "REPLACE INTO book_chapters " + "(book_id, chapter_uid, chapter_idx, title) " "VALUES (%s,%s,%s,%s)", (book_id, ch.get("chapterUid", 0), ch.get("chapterIdx", 0), ch.get("title", "")), ) @@ -948,21 +834,21 @@ def api_book_notes(conn): # 保存划线数据(先删除旧数据再插入) cursor.execute("DELETE FROM book_highlights WHERE book_id=%s", (book_id,)) for bm in data.get("updated", []): + ct = bm.get("createTime", 0) cursor.execute( "INSERT INTO book_highlights " - "(book_id, bookmark_id, chapter_uid, mark_text, create_time, " - "type, range_text, color_style, fetched_at) " + "(book_id, bookmark_id, chapter_uid, mark_text, create_time, create_time_dt, " + "type, range_text, color_style) " "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)", ( book_id, bm.get("bookmarkId", ""), bm.get("chapterUid", 0), bm.get("markText", ""), - bm.get("createTime", 0), + ct, ts_to_dt_str(ct), bm.get("type", 0), bm.get("range", ""), bm.get("colorStyle", 0), - now, ), ) highlights.append({ @@ -1001,26 +887,32 @@ def api_book_notes(conn): cursor.execute("DELETE FROM book_reviews WHERE book_id=%s", (book_id,)) for item in rev_data.get("reviews", []): rev = item.get("review", {}) + ct = rev.get("createTime", 0) cursor.execute( "INSERT INTO book_reviews " - "(book_id, review_id, content, create_time, star, chapter_name, is_finish, fetched_at) " - "VALUES (%s,%s,%s,%s,%s,%s,%s,%s)", + "(book_id, review_id, content, abstract, range_text, chapter_uid, chapter_idx, " + "chapter_name, create_time, create_time_dt, star, is_finish) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", ( book_id, rev.get("reviewId", ""), rev.get("content", ""), - rev.get("createTime", 0), - rev.get("star", 0), + rev.get("abstract", ""), + rev.get("range", ""), + rev.get("chapterUid"), + rev.get("chapterIdx"), rev.get("chapterName", ""), + ct, ts_to_dt_str(ct), + rev.get("star", -1), rev.get("isFinish", 0), - now, ), ) reviews.append({ "review_id": rev.get("reviewId", ""), "content": rev.get("content", ""), + "abstract": rev.get("abstract", ""), "create_time": ts_to_date(rev.get("createTime", 0)), - "star": rev.get("star", 0), + "star": rev.get("star", -1), "chapter_name": rev.get("chapterName", ""), "is_finish": rev.get("isFinish", 0), }) @@ -1078,22 +970,21 @@ def api_recommend(conn): """ max_idx = int(request.args.get("maxIdx", 0)) cursor = conn.cursor() - ensure_tables(cursor) - conn.commit() - now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + batch_id = datetime.now().strftime("%Y%m%d%H%M%S") 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", {}) cursor.execute( "REPLACE INTO recommended_books " "(book_id, title, author, cover, intro, category, reason, " - "reading_count, new_rating, new_rating_count, rating_title, fetched_at) " - "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + "reading_count, new_rating, new_rating_count, rating_title, " + "deep_link, search_idx, price, pay_type, book_type, batch_id) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", ( book.get("bookId", ""), book.get("title", ""), @@ -1106,7 +997,12 @@ def api_recommend(conn): book.get("newRating", 0), book.get("newRatingCount", 0), rating_detail.get("title", "") if isinstance(rating_detail, dict) else "", - now, + book.get("deepLink", ""), + book.get("searchIdx", 0), + book.get("price"), + book.get("payType"), + book.get("type", 0), + batch_id, ), ) conn.commit() @@ -1168,25 +1064,39 @@ def api_search(conn): max_idx = int(request.args.get("maxIdx", 0)) cursor = conn.cursor() - ensure_tables(cursor) - conn.commit() - now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") try: data = weread_api_call("/store/search", keyword=keyword, scope=10, maxIdx=max_idx) + sid = data.get("sid", "") + has_more = data.get("hasMore", 0) + + # 创建搜索会话记录 + cursor.execute( + "INSERT INTO search_sessions (sid, keyword, scope, has_more, max_idx) " + "VALUES (%s,%s,%s,%s,%s)", + (sid, keyword, 10, has_more, max_idx), + ) + session_id = cursor.lastrowid + results = data.get("results", []) books = [] for group in results: + result_title = group.get("title", "") + result_scope = group.get("scope") + scope_count = group.get("scopeCount") + current_count = group.get("currentCount") for item in group.get("books", []): bi = item.get("bookInfo", {}) rating_detail = item.get("newRatingDetail", {}) cursor.execute( - "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 (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", + "INSERT INTO search_results " + "(session_id, keyword, book_id, title, author, cover, intro, publisher, " + "category, reading_count, new_rating, new_rating_count, rating_title, " + "deep_link, pay_type, price, soldout, search_idx, " + "result_scope, result_title, scope_count, current_count) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", ( + session_id, keyword, bi.get("bookId", ""), bi.get("title", ""), @@ -1199,8 +1109,15 @@ def api_search(conn): item.get("newRating", 0), item.get("newRatingCount", 0), rating_detail.get("title", "") if isinstance(rating_detail, dict) else "", + bi.get("deepLink", ""), + bi.get("payType"), + bi.get("price"), + bi.get("soldout", 0), item.get("searchIdx", 0), - now, + result_scope, + result_title, + scope_count, + current_count, ), ) books.append({ @@ -1215,6 +1132,8 @@ def api_search(conn): "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 "", + "deep_link": bi.get("deepLink", ""), + "soldout": bi.get("soldout", 0), }) conn.commit() except Exception as e: diff --git a/fetch_data.py b/fetch_data.py index 4866939..567e8ca 100644 --- a/fetch_data.py +++ b/fetch_data.py @@ -1,7 +1,22 @@ #!/usr/bin/env python3 """ -微信读书数据采集脚本 -从微信读书 API 拉取数据并存入本地 SQLite 数据库 +微信读书数据采集脚本(完整版 v2.0) +从微信读书 API 拉取数据并存入本地 SQLite 数据库。 + +覆盖全部 8 份官方 API 文档: + - shelf.md: 书架(电子书 + 有声书 + 书单) + - readdata.md: 阅读统计(时长、排行、偏好分类/作者/出版社/版权方等) + - notes.md: 笔记(笔记本概览、划线、个人想法) + - book.md: 书籍详情、章节目录、阅读进度 + - discover.md: 推荐、相似书 + - search.md: 搜索 + - review.md: 公开点评 + - profile.md: 用户概况(组合接口) + +设计规范: + 1. 所有整型时间戳字段伴随一个 _dt 文本列(ISO 格式时间) + 2. 每张表维护 created_at 和 updated_at + 3. 表结构与 MySQL 版本(schema_mysql.sql)完全对齐 """ import json @@ -9,13 +24,33 @@ import os import sqlite3 import time import urllib.request +from datetime import datetime, timezone, timedelta +# ===== 配置 ===== API_URL = "https://i.weread.qq.com/api/agent/gateway" API_KEY = os.environ.get("WEREAD_API_KEY", "") -SKILL_VERSION = "1.0.3" +SKILL_VERSION = "1.0.4" DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weread.db") PROJECT_DIR = os.path.dirname(os.path.abspath(__file__)) +# 中国时区 UTC+8 +CST = timezone(timedelta(hours=8)) + + +def ts_to_dt(ts): + """将 Unix 整型时间戳转为 ISO 格式字符串 (中国时区)""" + if not ts or ts == 0: + return None + try: + return datetime.fromtimestamp(ts, tz=CST).strftime("%Y-%m-%d %H:%M:%S") + except (ValueError, OSError): + return None + + +def now_str(): + """当前时间字符串""" + return datetime.now(tz=CST).strftime("%Y-%m-%d %H:%M:%S") + def api_call(api_name, **kwargs): """调用微信读书 API""" @@ -34,276 +69,589 @@ def api_call(api_name, **kwargs): return json.loads(resp.read().decode("utf-8")) +# ===== 数据库初始化 ===== + def init_db(db_path): """ - 初始化 SQLite 数据库表结构。 - 创建所有数据表(如果不存在),包括: - - 基础数据表:书架、阅读统计、笔记本等(采集时写入) - - 按需数据表:书籍详情、划线、想法等(用户点击时写入) + 初始化 SQLite 数据库表结构(完整版 v2.0,31 张表)。 + 表结构与 MySQL 版本(schema_mysql.sql)完全对齐。 """ conn = sqlite3.connect(db_path) c = conn.cursor() - # ===== 基础数据表(采集脚本写入) ===== + # ===== 第一组:书架数据 ===== - # 书架书籍表 - 存储用户书架中的所有书籍信息 + # 1. 书架电子书表 c.execute(""" CREATE TABLE IF NOT EXISTS books ( - book_id TEXT PRIMARY KEY, -- 书籍唯一标识 - title TEXT, -- 书名 - author TEXT, -- 作者 - cover TEXT, -- 封面图 URL - category TEXT, -- 分类(如 "文学-古代诗词") - finish_reading INTEGER DEFAULT 0, -- 是否读完 (1=读完, 0=未读完) - read_update_time INTEGER, -- 最近阅读时间 (Unix 时间戳) - secret INTEGER DEFAULT 0, -- 是否私密书籍 (1=私密) - is_top INTEGER DEFAULT 0, -- 是否置顶 (1=置顶) - update_time INTEGER, -- 书籍信息更新时间 (Unix 时间戳) - deep_link TEXT -- 微信读书 App 深度链接 + book_id TEXT PRIMARY KEY, -- 书籍唯一标识 + title TEXT, -- 书名 + author TEXT, -- 作者 + cover TEXT, -- 封面图 URL + category TEXT, -- 分类 + finish_reading INTEGER DEFAULT 0, -- 是否读完 (1=读完) + read_update_time INTEGER, -- 最近阅读时间 (Unix 时间戳) + read_update_time_dt TEXT, -- 最近阅读时间 (ISO 格式) + secret INTEGER DEFAULT 0, -- 是否私密 (1=私密) + is_top INTEGER DEFAULT 0, -- 是否置顶 (1=置顶) + update_time INTEGER, -- 书籍更新时间 (Unix 时间戳) + update_time_dt TEXT, -- 书籍更新时间 (ISO 格式) + deep_link TEXT, -- 微信读书 App 深度链接 + created_at TEXT DEFAULT (datetime('now', 'localtime')), -- 记录创建时间 + updated_at TEXT DEFAULT (datetime('now', 'localtime')) -- 记录更新时间 ) """) - # 阅读统计概要表 - 存储总体/年度阅读统计数据 + # 2. 书架有声书/专辑表 c.execute(""" - CREATE TABLE IF NOT EXISTS reading_stats ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mode TEXT, -- 统计维度: overall(总体)/annually(年度) - period TEXT, -- 周期标识: "overall" 或 "2026" - total_read_time INTEGER, -- 总阅读时长 (秒) - read_days INTEGER, -- 有效阅读天数 - day_average_read_time REAL, -- 日均阅读时长 (秒) - regist_time INTEGER, -- 用户注册时间 (Unix 时间戳) - raw_json TEXT -- 原始 API 返回 JSON (完整备份) + CREATE TABLE IF NOT EXISTS shelf_albums ( + album_id TEXT PRIMARY KEY, -- 专辑唯一标识 + name TEXT, -- 专辑名称 + author_name TEXT, -- 演播/作者 + cover TEXT, -- 封面图 URL + track_count INTEGER DEFAULT 0, -- 音频集数 + finish_status TEXT, -- 完结状态文案 + finish INTEGER DEFAULT 0, -- 是否完结 (1=完结) + pay_type INTEGER, -- 付费类型 + intro TEXT, -- 专辑简介 + update_time INTEGER, -- 更新时间 (Unix 时间戳) + update_time_dt TEXT, -- 更新时间 (ISO 格式) + secret INTEGER DEFAULT 0, -- 是否私密 + lecture_paid INTEGER DEFAULT 0, -- 是否已购买 + lecture_read_update_time INTEGER, -- 最近收听时间 (Unix 时间戳) + lecture_read_update_time_dt TEXT, -- 最近收听时间 (ISO 格式) + is_top INTEGER DEFAULT 0, -- 是否置顶 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) - # 阅读时长时间序列表 - 按年/月/日分桶的阅读时长数据 + # 3. 书架元信息表 c.execute(""" - CREATE TABLE IF NOT EXISTS read_times ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mode TEXT, -- 统计维度 (含 _daily 后缀表示每日数据) - period TEXT, -- 周期标识 - bucket_timestamp INTEGER, -- 分桶起始时间戳 (Unix 时间戳) - duration INTEGER -- 该时段阅读时长 (秒) + CREATE TABLE IF NOT EXISTS shelf_meta ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mp_enabled INTEGER DEFAULT 0, -- 是否有文章收藏入口 + book_count INTEGER DEFAULT 0, -- 可枚举电子书数量 + album_count INTEGER DEFAULT 0, -- 有声书数量 + total_count INTEGER DEFAULT 0, -- 书架总条目数 + raw_json TEXT, -- 原始 API 返回 JSON + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) - # 阅读时长排行表 - 阅读时间最久的书籍排名 - c.execute(""" - CREATE TABLE IF NOT EXISTS read_longest ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mode TEXT, -- 统计维度 - period TEXT, -- 周期标识 - book_id TEXT, -- 书籍 ID - title TEXT, -- 书名 - author TEXT, -- 作者 - cover TEXT, -- 封面图 URL - read_time INTEGER, -- 阅读时长 (秒) - tags TEXT -- 标签 (JSON 数组格式) - ) - """) - - # 偏好分类表 - 用户阅读的书籍分类统计 - c.execute(""" - CREATE TABLE IF NOT EXISTS prefer_categories ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mode TEXT, -- 统计维度 - period TEXT, -- 周期标识 - category_id INTEGER, -- 分类 ID - category_title TEXT, -- 分类名称 (如 "文学") - parent_category_title TEXT, -- 父分类名称 (如 "精品小说") - val REAL, -- 偏好权重值 - reading_time INTEGER, -- 该分类阅读时长 (秒) - reading_count INTEGER -- 该分类阅读本数 - ) - """) - - # 偏好作者表 - 用户阅读最多的作者统计 - c.execute(""" - CREATE TABLE IF NOT EXISTS prefer_authors ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mode TEXT, -- 统计维度 - period TEXT, -- 周期标识 - author_id INTEGER, -- 作者 ID - name TEXT, -- 作者名称 - count INTEGER, -- 阅读该作者的书本数 - read_time TEXT -- 阅读时长 (格式化字符串) - ) - """) - - # 24小时阅读时段分布表 - 一天中每个小时的阅读时长 - c.execute(""" - CREATE TABLE IF NOT EXISTS prefer_time ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mode TEXT, -- 统计维度 - period TEXT, -- 周期标识 - hour_index INTEGER, -- 小时 (0-23) - duration INTEGER -- 该小时阅读时长 (秒) - ) - """) - - # 笔记本概览表 - 有笔记的书籍及其笔记统计 - c.execute(""" - CREATE TABLE IF NOT EXISTS notebooks ( - book_id TEXT PRIMARY KEY, -- 书籍 ID - title TEXT, -- 书名 - author TEXT, -- 作者 - cover TEXT, -- 封面图 URL - review_count INTEGER DEFAULT 0, -- 想法/点评数 - note_count INTEGER DEFAULT 0, -- 划线数 - bookmark_count INTEGER DEFAULT 0, -- 书签数 - reading_progress INTEGER DEFAULT 0, -- 阅读进度 (0-100) - marked_status INTEGER DEFAULT 0, -- 标记状态 (1=读完) - sort INTEGER -- 排序值 (微信读书返回的排序字段) - ) - """) - - # 阅读统计摘要项表 - App 首页展示的统计摘要 - c.execute(""" - CREATE TABLE IF NOT EXISTS read_stat_items ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - mode TEXT, -- 统计维度 - period TEXT, -- 周期标识 - stat_name TEXT, -- 统计项名称 (读过/读完/阅读/笔记) - counts TEXT, -- 统计值文案 (如 "404本") - scheme TEXT -- App 跳转链接 - ) - """) - - # 书单/归档表 - 用户创建的书单 + # 4. 书单/归档表 c.execute(""" CREATE TABLE IF NOT EXISTS archives ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT, -- 书单名称 - book_ids TEXT -- 包含的书籍 ID 列表 (JSON 数组) + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT, -- 书单名称 + book_ids TEXT, -- bookId 列表 (JSON 数组) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) - # 数据采集元信息表 - 记录采集时间等元数据 + # ===== 第二组:阅读统计数据 ===== + + # 5. 阅读统计概要表 c.execute(""" - CREATE TABLE IF NOT EXISTS fetch_meta ( - key TEXT PRIMARY KEY, -- 元信息键名 (如 "last_fetch_time") - value TEXT -- 元信息值 + CREATE TABLE IF NOT EXISTS reading_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度: overall/annually/monthly/weekly + period TEXT, -- 周期标识 + base_time INTEGER, -- 统计周期基准时间戳 + base_time_dt TEXT, -- 基准时间 (ISO 格式) + total_read_time INTEGER DEFAULT 0, -- 总阅读时长 (秒) + read_days INTEGER DEFAULT 0, -- 有效阅读天数 + day_average_read_time REAL DEFAULT 0, -- 日均阅读时长 (秒) + compare REAL, -- 与上周期对比比例 + regist_time INTEGER, -- 用户注册时间 (Unix 时间戳) + regist_time_dt TEXT, -- 注册时间 (ISO 格式) + record_reading_time INTEGER DEFAULT 0, -- 朗读/记录类阅读时长 (秒) + read_rate REAL, -- 文字阅读占比百分比 + wr_read_time INTEGER, -- 文字阅读时长 (秒) + wr_listen_time INTEGER, -- 听书时长 (秒) + author_count INTEGER, -- 作者总数 + prefer_category_word TEXT, -- 偏好分类文案 + prefer_time_word TEXT, -- 偏好时段文案 + read_records_word TEXT, -- 书籍分布模块标题 + read_distribution_word TEXT, -- 点评分布模块标题 + read_time_gears TEXT, -- 阅读时长档位 (JSON) + style_type TEXT, -- 样式类型 + raw_json TEXT, -- 原始 API 返回 JSON + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) - # ===== 按需数据表(用户点击时实时获取并写入) ===== - - # 书籍详细信息表 - 来自微信读书 /book/info 接口 + # 6. 阅读时长时间序列表 c.execute(""" - CREATE TABLE IF NOT EXISTS book_details ( - book_id TEXT PRIMARY KEY, -- 书籍 ID - title TEXT, -- 书名 - author TEXT, -- 作者 - translator TEXT, -- 译者 - cover TEXT, -- 封面图 URL - intro TEXT, -- 书籍简介 - category TEXT, -- 分类 - publisher TEXT, -- 出版社 - publish_time TEXT, -- 出版时间 - isbn TEXT, -- ISBN 号 - word_count INTEGER, -- 总字数 - new_rating REAL, -- 评分 (百分制) - new_rating_count INTEGER, -- 评分人数 - fetched_at INTEGER -- 数据获取时间 (Unix 时间戳) + CREATE TABLE IF NOT EXISTS read_times ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 (含 _daily 后缀) + period TEXT, -- 周期标识 + bucket_timestamp INTEGER, -- 分桶起始时间戳 + bucket_timestamp_dt TEXT, -- 分桶起始时间 (ISO 格式) + duration INTEGER DEFAULT 0, -- 阅读时长 (秒) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) - # 阅读进度表 - 来自微信读书 /book/getprogress 接口 + # 7. 阅读时长排行表 c.execute(""" - CREATE TABLE IF NOT EXISTS book_progress ( - book_id TEXT PRIMARY KEY, -- 书籍 ID - progress INTEGER, -- 阅读进度百分比 (0-100) - chapter_uid INTEGER, -- 当前阅读章节 UID - chapter_offset INTEGER, -- 当前章节内偏移量 - update_time INTEGER, -- 最后阅读时间 (Unix 时间戳) - record_reading_time INTEGER, -- 累计阅读时长 (秒) - finish_time INTEGER, -- 读完时间 (Unix 时间戳) - is_start_reading INTEGER, -- 是否已开始阅读 (1=是) - fetched_at INTEGER -- 数据获取时间 (Unix 时间戳) + CREATE TABLE IF NOT EXISTS read_longest ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + book_id TEXT, -- 书籍 ID + title TEXT, -- 书名 + author TEXT, -- 作者 + cover TEXT, -- 封面图 URL + read_time INTEGER DEFAULT 0, -- 阅读时长 (秒) + record_reading_time INTEGER, -- 朗读/记录类阅读时长 (秒) + tags TEXT, -- 标签 (JSON 数组) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) - # 划线内容表 - 来自微信读书 /book/bookmarklist 接口 + # 8. 阅读统计摘要项表 + c.execute(""" + CREATE TABLE IF NOT EXISTS read_stat_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + stat_name TEXT, -- 统计项名称 (读过/读完/阅读/笔记) + counts TEXT, -- 统计值文案 + scheme TEXT, -- App 跳转链接 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 9. 偏好分类表 + c.execute(""" + CREATE TABLE IF NOT EXISTS prefer_categories ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + category_id INTEGER, -- 分类 ID + category_title TEXT, -- 分类名称 + parent_category_id INTEGER, -- 父分类 ID + parent_category_title TEXT, -- 父分类名称 + category_type INTEGER DEFAULT 0, -- 分类类型标记 + val REAL DEFAULT 0, -- 偏好权重 + reading_time INTEGER DEFAULT 0, -- 阅读时长 (秒) + reading_count INTEGER DEFAULT 0, -- 阅读本数 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 10. 偏好作者表 + c.execute(""" + CREATE TABLE IF NOT EXISTS prefer_authors ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + author_id INTEGER, -- 作者 ID + name TEXT, -- 作者名 + count INTEGER DEFAULT 0, -- 阅读书本数 + read_time TEXT, -- 阅读时长 (格式化字符串) + user_json TEXT, -- 作者关联用户信息 (JSON) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 11. 偏好出版社表 + c.execute(""" + CREATE TABLE IF NOT EXISTS prefer_publishers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + name TEXT, -- 出版社名 + count INTEGER DEFAULT 0, -- 阅读本数 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 12. 偏好版权方表 + c.execute(""" + CREATE TABLE IF NOT EXISTS prefer_cps ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + count INTEGER DEFAULT 0, -- 阅读本数 + cp_name TEXT, -- 版权方名称 + cp_user_vid TEXT, -- 版权方用户 VID + cp_avatar TEXT, -- 版权方头像 URL + cp_role TEXT, -- 版权方角色 + copyright_json TEXT, -- 版权方完整信息 (JSON) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 13. 偏好书籍卡片表 + c.execute(""" + CREATE TABLE IF NOT EXISTS prefer_books ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + book_id TEXT, -- 书籍 ID + title TEXT, -- 书名 + author TEXT, -- 作者 + cover TEXT, -- 封面图 URL + reason TEXT, -- 推荐理由 + prefer_type TEXT, -- 偏好类型 + book_json TEXT, -- 书籍完整信息 (JSON) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 14. 24小时阅读时段分布表 + c.execute(""" + CREATE TABLE IF NOT EXISTS prefer_time ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + hour_index INTEGER, -- 小时 (0-23) + duration INTEGER DEFAULT 0, -- 阅读时长 (秒) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 15. 好友阅读排行表 + c.execute(""" + CREATE TABLE IF NOT EXISTS read_rank ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + rank_text TEXT, -- 排行文案 + scheme TEXT, -- 排行跳转链接 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 16. 勋章表 + c.execute(""" + CREATE TABLE IF NOT EXISTS medals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + medal_json TEXT, -- 勋章信息 (JSON) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 17. 年度报告表 + c.execute(""" + CREATE TABLE IF NOT EXISTS year_reports ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mode TEXT, -- 统计维度 + period TEXT, -- 周期标识 + report_year TEXT, -- 报告年份 + report_json TEXT, -- 报告入口信息 (JSON) + monthly_times TEXT, -- 12个月阅读时长 (JSON 数组) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # ===== 第三组:笔记数据 ===== + + # 18. 笔记本概览表 + c.execute(""" + CREATE TABLE IF NOT EXISTS notebooks ( + book_id TEXT PRIMARY KEY, -- 书籍 ID + title TEXT, -- 书名 + author TEXT, -- 作者 + cover TEXT, -- 封面图 URL + review_count INTEGER DEFAULT 0, -- 想法/点评数 + note_count INTEGER DEFAULT 0, -- 划线数 + bookmark_count INTEGER DEFAULT 0, -- 书签数 + total_note_count INTEGER DEFAULT 0, -- 笔记总数 + reading_progress INTEGER DEFAULT 0, -- 阅读进度 (0-100) + marked_status INTEGER DEFAULT 0, -- 标记状态 (1=读完) + sort INTEGER, -- 排序值 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 19. 划线内容表 c.execute(""" CREATE TABLE IF NOT EXISTS book_highlights ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - book_id TEXT, -- 书籍 ID - bookmark_id TEXT, -- 划线唯一 ID - chapter_uid INTEGER, -- 所在章节 UID - mark_text TEXT, -- 划线原文内容 - create_time INTEGER, -- 创建时间 (Unix 时间戳) - type INTEGER, -- 划线类型 - range_text TEXT, -- 位置范围 - color_style INTEGER, -- 划线颜色样式 - fetched_at INTEGER -- 数据获取时间 (Unix 时间戳) + id INTEGER PRIMARY KEY AUTOINCREMENT, + book_id TEXT, -- 书籍 ID + bookmark_id TEXT, -- 划线唯一 ID + chapter_uid INTEGER, -- 所在章节 UID + mark_text TEXT, -- 划线原文内容 + create_time INTEGER, -- 创建时间 (Unix 时间戳) + create_time_dt TEXT, -- 创建时间 (ISO 格式) + type INTEGER, -- 类型 (1=划线) + range_text TEXT, -- 位置范围 + color_style INTEGER, -- 划线颜色样式 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) - # 章节信息表 - 来自微信读书 /book/bookmarklist 接口的 chapters 字段 + # 20. 章节信息表 c.execute(""" CREATE TABLE IF NOT EXISTS book_chapters ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - book_id TEXT, -- 书籍 ID - chapter_uid INTEGER, -- 章节 UID - chapter_idx INTEGER, -- 章节序号 - title TEXT, -- 章节标题 - UNIQUE(book_id, chapter_uid) -- 联合唯一约束 + id INTEGER PRIMARY KEY AUTOINCREMENT, + book_id TEXT, -- 书籍 ID + chapter_uid INTEGER, -- 章节 UID + chapter_idx INTEGER, -- 章节序号 + title TEXT, -- 章节标题 + word_count INTEGER, -- 章节字数 + level INTEGER, -- 目录层级 (1=一级) + update_time INTEGER, -- 章节更新时间 (Unix 时间戳) + update_time_dt TEXT, -- 章节更新时间 (ISO 格式) + price INTEGER DEFAULT 0, -- 章节价格 (0=免费) + paid INTEGER DEFAULT 0, -- 是否已购买 + is_mp_chapter INTEGER DEFAULT 0, -- 是否公众号章节 + anchors TEXT, -- 章节内锚点 (JSON) + synckey INTEGER, -- 同步 key + chapter_update_time INTEGER, -- 章节列表最后更新时间 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')), + UNIQUE(book_id, chapter_uid) ) """) - # 个人想法/点评表 - 来自微信读书 /review/list/mine 接口 + # 21. 个人想法/点评表 c.execute(""" CREATE TABLE IF NOT EXISTS book_reviews ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - book_id TEXT, -- 书籍 ID - review_id TEXT, -- 想法唯一 ID - content TEXT, -- 想法内容文本 - create_time INTEGER, -- 创建时间 (Unix 时间戳) - star INTEGER, -- 评分 (0-5, -1=无评分) - chapter_name TEXT, -- 所在章节名称 - is_finish INTEGER, -- 是否书评 (1=书评) - fetched_at INTEGER -- 数据获取时间 (Unix 时间戳) + id INTEGER PRIMARY KEY AUTOINCREMENT, + book_id TEXT, -- 书籍 ID + review_id TEXT, -- 想法唯一 ID + content TEXT, -- 想法内容文本 + abstract TEXT, -- 想法对应的划线原文 + range_text TEXT, -- 划线原文位置范围 + chapter_uid INTEGER, -- 章节 UID + chapter_idx INTEGER, -- 章节序号 + chapter_name TEXT, -- 所在章节名 + create_time INTEGER, -- 创建时间 (Unix 时间戳) + create_time_dt TEXT, -- 创建时间 (ISO 格式) + star INTEGER DEFAULT -1, -- 评分 (0-5, -1=无评分) + is_finish INTEGER DEFAULT 0, -- 是否书评 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) - # 推荐书籍表 - 来自微信读书 /book/recommend 接口 + # 22. 热门划线表 + c.execute(""" + CREATE TABLE IF NOT EXISTS book_best_bookmarks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + book_id TEXT, -- 书籍 ID + bookmark_id TEXT, -- 划线唯一 ID + chapter_uid INTEGER, -- 所在章节 UID + range_text TEXT, -- 划线位置范围 + mark_text TEXT, -- 划线原文文本 + total_count INTEGER DEFAULT 0, -- 划线人数 + user_vid TEXT, -- 代表用户 VID + simplified_range TEXT, -- 简体 range + traditional_range TEXT, -- 繁体 range + synckey INTEGER, -- 同步 key + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 23. 划线热度统计表 + c.execute(""" + CREATE TABLE IF NOT EXISTS book_underlines ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + book_id TEXT, -- 书籍 ID + chapter_uid INTEGER, -- 章节 UID + range_text TEXT, -- 划线位置范围 + count INTEGER DEFAULT 0, -- 划线人数 + score REAL DEFAULT 0, -- 热度分数 + type INTEGER, -- 划线类型 + synckey INTEGER, -- 同步 key + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 24. 公开点评表 + c.execute(""" + CREATE TABLE IF NOT EXISTS public_reviews ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + book_id TEXT, -- 书籍 ID + review_id TEXT, -- 点评唯一 ID + content TEXT, -- 点评文本内容 + html_content TEXT, -- 点评 HTML 富文本 + star INTEGER DEFAULT -1, -- 评分 (20-100) + is_finish INTEGER DEFAULT 0, -- 是否读完 + create_time INTEGER, -- 创建时间 (Unix 时间戳) + create_time_dt TEXT, -- 创建时间 (ISO 格式) + chapter_name TEXT, -- 所在章节名 + author_vid TEXT, -- 评论者 VID + author_name TEXT, -- 评论者昵称 + author_avatar TEXT, -- 评论者头像 URL + review_list_type INTEGER DEFAULT 0, -- 筛选类型 + idx INTEGER, -- 序号 (用于翻页) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # ===== 第四组:书籍详情 ===== + + # 25. 书籍详细信息表 + c.execute(""" + CREATE TABLE IF NOT EXISTS book_details ( + book_id TEXT PRIMARY KEY, -- 书籍 ID + title TEXT, -- 书名 + author TEXT, -- 作者 + translator TEXT, -- 译者 + cover TEXT, -- 封面图 URL + intro TEXT, -- 简介 + category TEXT, -- 分类 + publisher TEXT, -- 出版社 + publish_time TEXT, -- 出版时间 + isbn TEXT, -- ISBN 号 + word_count INTEGER DEFAULT 0, -- 总字数 + new_rating REAL DEFAULT 0, -- 评分 (百分制) + new_rating_count INTEGER DEFAULT 0, -- 评分人数 + new_rating_detail TEXT, -- 评分分布详情 (JSON) + deep_link TEXT, -- 跳转链接 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 26. 阅读进度表 + c.execute(""" + CREATE TABLE IF NOT EXISTS book_progress ( + book_id TEXT PRIMARY KEY, -- 书籍 ID + progress INTEGER DEFAULT 0, -- 阅读进度百分比 (0-100) + chapter_uid INTEGER, -- 当前阅读章节 UID + chapter_offset INTEGER, -- 当前章节内偏移量 + update_time INTEGER, -- 最后阅读时间 (Unix 时间戳) + update_time_dt TEXT, -- 最后阅读时间 (ISO 格式) + record_reading_time INTEGER DEFAULT 0, -- 累计阅读时长 (秒) + finish_time INTEGER, -- 读完时间 (Unix 时间戳) + finish_time_dt TEXT, -- 读完时间 (ISO 格式) + is_start_reading INTEGER DEFAULT 0, -- 是否已开始阅读 + server_timestamp INTEGER, -- 服务端时间戳 + server_timestamp_dt TEXT, -- 服务端时间 (ISO 格式) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # ===== 第五组:发现/推荐 ===== + + # 27. 推荐书籍表 c.execute(""" CREATE TABLE IF NOT EXISTS recommended_books ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - book_id TEXT, -- 书籍 ID - title TEXT, -- 书名 - author TEXT, -- 作者 - cover TEXT, -- 封面图 URL - intro TEXT, -- 书籍简介 - category TEXT, -- 分类 - reason TEXT, -- 推荐理由 - reading_count INTEGER, -- 在读人数 - new_rating REAL, -- 评分 (百分制) - new_rating_count INTEGER, -- 评分人数 - rating_title TEXT, -- 评分标签 (神作/力荐等) - fetched_at INTEGER -- 数据获取时间 (Unix 时间戳) + id INTEGER PRIMARY KEY AUTOINCREMENT, + book_id TEXT, -- 书籍 ID + title TEXT, -- 书名 + author TEXT, -- 作者 + cover TEXT, -- 封面图 URL + intro TEXT, -- 书籍简介 + category TEXT, -- 分类 + reason TEXT, -- 推荐理由 + reading_count INTEGER DEFAULT 0, -- 在读人数 + new_rating REAL DEFAULT 0, -- 评分 (百分制) + new_rating_count INTEGER DEFAULT 0, -- 评分人数 + rating_title TEXT, -- 评分标签 + deep_link TEXT, -- 跳转链接 + search_idx INTEGER DEFAULT 0, -- 结果序号 + price INTEGER, -- 价格 (分) + pay_type INTEGER, -- 付费类型 + book_type INTEGER DEFAULT 0, -- 书籍类型 (0=电子书) + batch_id TEXT, -- 推荐批次 ID + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) - # 搜索结果表 - 来自微信读书 /store/search 接口 + # 28. 相似书推荐表 + c.execute(""" + CREATE TABLE IF NOT EXISTS similar_books ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + source_book_id TEXT, -- 源书籍 ID + book_id TEXT, -- 推荐书籍 ID + title TEXT, -- 书名 + author TEXT, -- 作者 + cover TEXT, -- 封面图 URL + deep_link TEXT, -- 跳转链接 + idx INTEGER, -- 结果序号 + session_id TEXT, -- 翻页会话 ID + book_json TEXT, -- 书籍完整信息 (JSON) + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 29. 搜索会话表 + c.execute(""" + CREATE TABLE IF NOT EXISTS search_sessions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + sid TEXT, -- 搜索会话 ID + keyword TEXT, -- 搜索关键词 + scope INTEGER, -- 搜索类型 + has_more INTEGER DEFAULT 0, -- 是否有更多 + max_idx INTEGER DEFAULT 0, -- 当前翻页偏移 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # 30. 搜索结果表 c.execute(""" CREATE TABLE IF NOT EXISTS search_results ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - keyword TEXT, -- 搜索关键词 - book_id TEXT, -- 书籍 ID - title TEXT, -- 书名 - author TEXT, -- 作者 - cover TEXT, -- 封面图 URL - 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 -- 数据获取时间 (Unix 时间戳) + id INTEGER PRIMARY KEY AUTOINCREMENT, + session_id INTEGER, -- 关联搜索会话 ID + keyword TEXT, -- 搜索关键词 + book_id TEXT, -- 书籍 ID + title TEXT, -- 书名 + author TEXT, -- 作者 + cover TEXT, -- 封面图 URL + intro TEXT, -- 书籍简介 + publisher TEXT, -- 出版社 + category TEXT, -- 分类 + reading_count INTEGER DEFAULT 0, -- 在读人数 + new_rating REAL DEFAULT 0, -- 评分 + new_rating_count INTEGER DEFAULT 0, -- 评分人数 + rating_title TEXT, -- 评分标签 + deep_link TEXT, -- 跳转链接 + pay_type INTEGER, -- 付费类型 + price INTEGER, -- 价格 (分) + soldout INTEGER DEFAULT 0, -- 是否下架 + search_idx INTEGER DEFAULT 0, -- 搜索序号 + result_scope INTEGER, -- 回包分组类型 + result_title TEXT, -- 回包分组标题 + scope_count INTEGER, -- 分组总结果数 + current_count INTEGER, -- 本次返回数量 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ) + """) + + # ===== 第六组:系统表 ===== + + # 31. 数据采集元信息表 + c.execute(""" + CREATE TABLE IF NOT EXISTS fetch_meta ( + key TEXT PRIMARY KEY, -- 元信息键名 + value TEXT, -- 元信息值 + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) @@ -311,19 +659,29 @@ def init_db(db_path): return conn +# ===== 数据采集函数 ===== + def fetch_shelf(conn): - """拉取书架数据""" + """拉取书架数据(电子书 + 有声书 + 书单)""" print(" 拉取书架数据...") data = api_call("/shelf/sync") c = conn.cursor() + # 清空旧数据 + for t in ["books", "shelf_albums", "shelf_meta", "archives"]: + c.execute(f"DELETE FROM {t}") + + # 1. 电子书 books = data.get("books", []) for book in books: + rut = book.get("readUpdateTime", 0) + ut = book.get("updateTime", 0) c.execute( """INSERT OR REPLACE INTO books (book_id, title, author, cover, category, finish_reading, - read_update_time, secret, is_top, update_time, deep_link) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + read_update_time, read_update_time_dt, secret, is_top, + update_time, update_time_dt, deep_link) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( book.get("bookId"), book.get("title", ""), @@ -331,17 +689,59 @@ def fetch_shelf(conn): book.get("cover", ""), book.get("category", ""), book.get("finishReading", 0), - book.get("readUpdateTime", 0), + rut, ts_to_dt(rut), book.get("secret", 0), book.get("isTop", 0), - book.get("updateTime", 0), + ut, ts_to_dt(ut), book.get("deepLink", ""), ), ) - # 保存书单 + # 2. 有声书/专辑 + albums = data.get("albums", []) + for album in albums: + info = album.get("albumInfo", {}) + extra = album.get("albumInfoExtra", {}) + ut = info.get("updateTime", 0) + lrut = extra.get("lectureReadUpdateTime", 0) + c.execute( + """INSERT OR REPLACE INTO shelf_albums + (album_id, name, author_name, cover, track_count, finish_status, + finish, pay_type, intro, update_time, update_time_dt, + secret, lecture_paid, lecture_read_update_time, lecture_read_update_time_dt, is_top) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + ( + info.get("albumId"), + info.get("name", ""), + info.get("authorName", ""), + info.get("cover", ""), + info.get("trackCount", 0), + info.get("finishStatus", ""), + info.get("finish", 0), + info.get("payType"), + info.get("intro", ""), + ut, ts_to_dt(ut), + extra.get("secret", 0), + extra.get("lecturePaid", 0), + lrut, ts_to_dt(lrut), + extra.get("isTop", 0), + ), + ) + + # 3. 书架元信息 + mp_flag = 1 if data.get("mp") else 0 + book_count = data.get("bookCount", len(books)) + album_count = len(albums) + total = book_count + album_count + mp_flag + c.execute( + """INSERT INTO shelf_meta (mp_enabled, book_count, album_count, total_count, raw_json) + VALUES (?, ?, ?, ?, ?)""", + (mp_flag, book_count, album_count, total, + json.dumps(data, ensure_ascii=False)), + ) + + # 4. 书单/归档 archives = data.get("archive", []) - c.execute("DELETE FROM archives") for arch in archives: c.execute( "INSERT INTO archives (name, book_ids) VALUES (?, ?)", @@ -349,12 +749,12 @@ def fetch_shelf(conn): ) conn.commit() - mp_flag = "yes" if data.get("mp") else "no" - print(f" 书架: {len(books)} 本电子书, {len(data.get('albums', []))} 个专辑, MP={mp_flag}, {len(archives)} 个书单") + print(f" 书架: {len(books)} 本电子书, {len(albums)} 个有声书, " + f"MP={'有' if mp_flag else '无'}, {len(archives)} 个书单, 总计 {total} 条目") def fetch_readdata(conn, mode, period_label): - """拉取阅读统计数据""" + """拉取阅读统计数据(完整字段)""" print(f" 拉取阅读统计 [{mode}]...") params = {"mode": mode} if mode != "overall": @@ -362,129 +762,213 @@ def fetch_readdata(conn, mode, period_label): data = api_call("/readdata/detail", **params) c = conn.cursor() - total_read_time = data.get("totalReadTime", 0) - read_days = data.get("readDays", 0) - day_avg = data.get("dayAverageReadTime", 0) - regist_time = data.get("registTime", 0) - + # 5. 阅读统计概要 + base_time = data.get("baseTime") + regist_time = data.get("registTime") + gears = data.get("readTimeGears") c.execute( """INSERT INTO reading_stats - (mode, period, total_read_time, read_days, day_average_read_time, - regist_time, raw_json) - VALUES (?, ?, ?, ?, ?, ?, ?)""", + (mode, period, base_time, base_time_dt, total_read_time, read_days, + day_average_read_time, compare, regist_time, regist_time_dt, + record_reading_time, read_rate, wr_read_time, wr_listen_time, + author_count, prefer_category_word, prefer_time_word, + read_records_word, read_distribution_word, read_time_gears, style_type, raw_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( - mode, - period_label, - total_read_time, - read_days, - day_avg, - regist_time, + mode, period_label, + base_time, ts_to_dt(base_time), + data.get("totalReadTime", 0), + data.get("readDays", 0), + data.get("dayAverageReadTime", 0), + data.get("compare"), + regist_time, ts_to_dt(regist_time), + data.get("recordReadingTime", 0), + data.get("readRate"), + data.get("wrReadTime"), + data.get("wrListenTime"), + data.get("authorCount"), + data.get("preferCategoryWord"), + data.get("preferTimeWord"), + data.get("readRecordsWord"), + data.get("readDistributionWord"), + json.dumps(gears) if gears else None, + data.get("styleType"), json.dumps(data, ensure_ascii=False), ), ) - # 保存 readTimes 分桶数据 + # 6. 阅读时长时间序列 read_times = data.get("readTimes", {}) for ts, duration in read_times.items(): c.execute( - "INSERT INTO read_times (mode, period, bucket_timestamp, duration) VALUES (?, ?, ?, ?)", - (mode, period_label, int(ts), duration), + "INSERT INTO read_times (mode, period, bucket_timestamp, bucket_timestamp_dt, duration) VALUES (?, ?, ?, ?, ?)", + (mode, period_label, int(ts), ts_to_dt(int(ts)), duration), ) - - # 保存 dailyReadTimes(如果有) daily_times = data.get("dailyReadTimes", {}) for ts, duration in daily_times.items(): c.execute( - "INSERT INTO read_times (mode, period, bucket_timestamp, duration) VALUES (?, ?, ?, ?)", - (mode + "_daily", period_label, int(ts), duration), + "INSERT INTO read_times (mode, period, bucket_timestamp, bucket_timestamp_dt, duration) VALUES (?, ?, ?, ?, ?)", + (mode + "_daily", period_label, int(ts), ts_to_dt(int(ts)), duration), ) - # 保存阅读最久的书籍 + # 7. 阅读时长排行 for item in data.get("readLongest", []): book = item.get("book", {}) album = item.get("albumInfo", {}) c.execute( """INSERT INTO read_longest - (mode, period, book_id, title, author, cover, read_time, tags) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + (mode, period, book_id, title, author, cover, read_time, record_reading_time, tags) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( - mode, - period_label, + mode, period_label, book.get("bookId") or album.get("albumId", ""), book.get("title") or album.get("name", ""), book.get("author") or album.get("authorName", ""), book.get("cover") or album.get("cover", ""), item.get("readTime", 0), + item.get("recordReadingTime"), json.dumps(item.get("tags", []), ensure_ascii=False), ), ) - # 保存偏好分类 + # 8. 统计摘要项 + for stat in data.get("readStat", []): + c.execute( + "INSERT INTO read_stat_items (mode, period, stat_name, counts, scheme) VALUES (?, ?, ?, ?, ?)", + (mode, period_label, stat.get("stat", ""), stat.get("counts", ""), stat.get("scheme", "")), + ) + + # 9. 偏好分类 for cat in data.get("preferCategory", []): c.execute( """INSERT INTO prefer_categories - (mode, period, category_id, category_title, parent_category_title, - val, reading_time, reading_count) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + (mode, period, category_id, category_title, parent_category_id, + parent_category_title, category_type, val, reading_time, reading_count) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( - mode, - period_label, + mode, period_label, cat.get("categoryId", 0), cat.get("categoryTitle", ""), + cat.get("parentCategoryId"), cat.get("parentCategoryTitle", ""), + cat.get("categoryType", 0), cat.get("val", 0), cat.get("readingTime", 0), cat.get("readingCount", 0), ), ) - # 保存偏好作者 + # 10. 偏好作者 for author in data.get("preferAuthor", []): + user = author.get("user") c.execute( """INSERT INTO prefer_authors - (mode, period, author_id, name, count, read_time) - VALUES (?, ?, ?, ?, ?, ?)""", + (mode, period, author_id, name, count, read_time, user_json) + VALUES (?, ?, ?, ?, ?, ?, ?)""", ( - mode, - period_label, + mode, period_label, author.get("authorId", 0), author.get("name", ""), author.get("count", 0), author.get("readTime", ""), + json.dumps(user, ensure_ascii=False) if user else None, ), ) - # 保存24小时时段分布 - prefer_time = data.get("preferTime", []) - # preferTime 从6点开始到次日5点,映射到 hour_index - for i, duration in enumerate(prefer_time): + # 11. 偏好出版社 + for pub in data.get("preferPublisher", []): + c.execute( + "INSERT INTO prefer_publishers (mode, period, name, count) VALUES (?, ?, ?, ?)", + (mode, period_label, pub.get("name", ""), pub.get("count", 0)), + ) + + # 12. 偏好版权方 + for cp in data.get("preferCp", []): + ci = cp.get("copyrightInfo", {}) + c.execute( + """INSERT INTO prefer_cps + (mode, period, count, cp_name, cp_user_vid, cp_avatar, cp_role, copyright_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", + ( + mode, period_label, + cp.get("count", 0), + ci.get("name", ""), + ci.get("userVid", ""), + ci.get("avatar", ""), + ci.get("role", ""), + json.dumps(ci, ensure_ascii=False) if ci else None, + ), + ) + + # 13. 偏好书籍卡片 + for pb in data.get("preferBooks", []): + book = pb.get("book", {}) + c.execute( + """INSERT INTO prefer_books + (mode, period, book_id, title, author, cover, reason, prefer_type, book_json) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + ( + mode, period_label, + book.get("bookId"), + book.get("title", ""), + book.get("author", ""), + book.get("cover", ""), + pb.get("reason", ""), + pb.get("preferType", ""), + json.dumps(pb, ensure_ascii=False), + ), + ) + + # 14. 24小时时段分布(从6点开始到次日5点) + for i, duration in enumerate(data.get("preferTime", [])): hour = (6 + i) % 24 c.execute( "INSERT INTO prefer_time (mode, period, hour_index, duration) VALUES (?, ?, ?, ?)", (mode, period_label, hour, duration), ) - # 保存统计摘要项 - for stat in data.get("readStat", []): + # 15. 好友排行 + rank = data.get("rank") + if rank: c.execute( - """INSERT INTO read_stat_items - (mode, period, stat_name, counts, scheme) + "INSERT INTO read_rank (mode, period, rank_text, scheme) VALUES (?, ?, ?, ?)", + (mode, period_label, rank.get("text", ""), rank.get("scheme", "")), + ) + + # 16. 勋章 + for medal in data.get("medals", []): + c.execute( + "INSERT INTO medals (mode, period, medal_json) VALUES (?, ?, ?)", + (mode, period_label, json.dumps(medal, ensure_ascii=False)), + ) + + # 17. 年度报告 + for yr in data.get("yearReport", []): + times = yr.get("times") + c.execute( + """INSERT INTO year_reports + (mode, period, report_year, report_json, monthly_times) VALUES (?, ?, ?, ?, ?)""", ( - mode, - period_label, - stat.get("stat", ""), - stat.get("counts", ""), - stat.get("scheme", ""), + mode, period_label, + str(yr.get("year", "")), + json.dumps(yr, ensure_ascii=False), + json.dumps(times) if times else None, ), ) conn.commit() - print(f" [{mode}] 总时长: {total_read_time}秒 ({total_read_time//3600}小时), 阅读天数: {read_days}") + total = data.get("totalReadTime", 0) + print(f" [{mode}] 总时长: {total}秒 ({total // 3600}小时), " + f"阅读天数: {data.get('readDays', 0)}, " + f"排行: {len(data.get('readLongest', []))} 本, " + f"偏好分类: {len(data.get('preferCategory', []))} 个, " + f"偏好作者: {len(data.get('preferAuthor', []))} 个, " + f"偏好出版社: {len(data.get('preferPublisher', []))} 个") def fetch_notebooks(conn): - """拉取笔记本概览(分页拉取全部)""" + """拉取笔记本概览(分页拉取全部,含 total_note_count)""" print(" 拉取笔记本数据...") c = conn.cursor() c.execute("DELETE FROM notebooks") @@ -502,19 +986,20 @@ def fetch_notebooks(conn): books = data.get("books", []) for item in books: book = item.get("book", {}) + rc = item.get("reviewCount", 0) + nc = item.get("noteCount", 0) + bc = item.get("bookmarkCount", 0) c.execute( """INSERT OR REPLACE INTO notebooks (book_id, title, author, cover, review_count, note_count, - bookmark_count, reading_progress, marked_status, sort) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + bookmark_count, total_note_count, reading_progress, marked_status, sort) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", ( item.get("bookId", ""), book.get("title", ""), book.get("author", ""), book.get("cover", ""), - item.get("reviewCount", 0), - item.get("noteCount", 0), - item.get("bookmarkCount", 0), + rc, nc, bc, rc + nc + bc, item.get("readingProgress", 0), item.get("markedStatus", 0), item.get("sort", 0), @@ -532,24 +1017,28 @@ def fetch_notebooks(conn): print(f" 笔记本: {total} 本有笔记的书") +# ===== 主函数 ===== + def main(): if not API_KEY: print("错误: 请先设置 WEREAD_API_KEY 环境变量") print(" export WEREAD_API_KEY=wrk-xxxxxxxx") return - print("=" * 50) - print("微信读书数据采集") - print("=" * 50) + print("=" * 60) + print("微信读书数据采集 (完整版 v2.0)") + print("=" * 60) # 初始化数据库 print(f"\n[1/4] 初始化数据库: {DB_PATH}") conn = init_db(DB_PATH) - # 清空旧数据(保留表结构) + # 清空旧的统计数据(每次采集全量覆盖) for table in [ - "reading_stats", "read_times", "read_longest", "prefer_categories", - "prefer_authors", "prefer_time", "read_stat_items", + "reading_stats", "read_times", "read_longest", "read_stat_items", + "prefer_categories", "prefer_authors", "prefer_publishers", + "prefer_cps", "prefer_books", "prefer_time", "read_rank", + "medals", "year_reports", ]: conn.execute(f"DELETE FROM {table}") conn.commit() @@ -558,7 +1047,7 @@ def main(): print("\n[2/4] 拉取书架数据") fetch_shelf(conn) - # 拉取阅读统计(overall + 当前年) + # 拉取阅读统计 print("\n[3/4] 拉取阅读统计数据") fetch_readdata(conn, "overall", "overall") current_year = time.localtime().tm_year @@ -572,25 +1061,39 @@ def main(): c = conn.cursor() c.execute( "INSERT OR REPLACE INTO fetch_meta (key, value) VALUES (?, ?)", - ("last_fetch_time", time.strftime("%Y-%m-%d %H:%M:%S")), + ("last_fetch_time", now_str()), + ) + c.execute( + "INSERT OR REPLACE INTO fetch_meta (key, value) VALUES (?, ?)", + ("schema_version", "2.0"), ) conn.commit() # 统计 c.execute("SELECT COUNT(*) FROM books") book_count = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM shelf_albums") + album_count = c.fetchone()[0] c.execute("SELECT COUNT(*) FROM notebooks") notebook_count = c.fetchone()[0] c.execute("SELECT SUM(total_read_time) FROM reading_stats WHERE mode='overall'") total_time = c.fetchone()[0] or 0 + c.execute("SELECT COUNT(*) FROM prefer_publishers") + pub_count = c.fetchone()[0] + c.execute("SELECT COUNT(*) FROM prefer_cps") + cp_count = c.fetchone()[0] - print("\n" + "=" * 50) + print("\n" + "=" * 60) print("采集完成!") - print(f" 书架书籍: {book_count} 本") + print(f" 书架电子书: {book_count} 本") + print(f" 有声书/专辑: {album_count} 个") print(f" 笔记本: {notebook_count} 本") print(f" 总阅读时长: {total_time // 3600} 小时 {(total_time % 3600) // 60} 分钟") + print(f" 偏好出版社: {pub_count} 个") + print(f" 偏好版权方: {cp_count} 个") print(f" 数据库: {DB_PATH}") - print("=" * 50) + print(f" 表结构版本: 2.0 (31 张表)") + print("=" * 60) conn.close() diff --git a/migrate_to_mysql.py b/migrate_to_mysql.py index a0a31bf..d11595a 100644 --- a/migrate_to_mysql.py +++ b/migrate_to_mysql.py @@ -1,16 +1,20 @@ #!/usr/bin/env python3 """ -SQLite -> MySQL 数据迁移脚本 -将 weread.db (SQLite) 中的全部数据迁移到 MySQL weread_dashboard 数据库 +SQLite → MySQL 数据迁移脚本(完整版 v2.0) +将 weread.db (SQLite) 全部 31 张表数据迁移至 MySQL weread_dashboard 数据库。 + +处理逻辑: + 1. 自动读取 SQLite 每张表的列名,与 MySQL 对齐 + 2. 时间戳文本列 (_dt, created_at, updated_at) 自动转换为 MySQL TIMESTAMP 格式 + 3. 每张表先 TRUNCATE 再插入,确保数据一致性 """ import os import sqlite3 +import pymysql from datetime import datetime -import pymysql - -# ===== 连接配置 ===== +# ===== 配置 ===== SQLITE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weread.db") MYSQL_CONFIG = { @@ -22,167 +26,146 @@ MYSQL_CONFIG = { "charset": "utf8mb4", } -# ===== 表迁移映射 ===== -# (sqlite_table, mysql_table, columns, timestamp_cols) -# timestamp_cols: SQLite 中存为 int(秒级时间戳),MySQL 中为 TIMESTAMP 的列 -TABLE_MAPPING = [ - ("books", "books", - ["book_id", "title", "author", "cover", "category", "finish_reading", - "read_update_time", "secret", "is_top", "update_time", "deep_link"], - []), - - ("reading_stats", "reading_stats", - ["mode", "period", "total_read_time", "read_days", "day_average_read_time", - "regist_time", "raw_json"], - []), # MySQL created_at 用默认值 - - ("read_times", "read_times", - ["mode", "period", "bucket_timestamp", "duration"], - []), - - ("read_longest", "read_longest", - ["mode", "period", "book_id", "title", "author", "cover", "read_time", "tags"], - []), - - ("prefer_categories", "prefer_categories", - ["mode", "period", "category_id", "category_title", "parent_category_title", - "val", "reading_time", "reading_count"], - []), - - ("prefer_authors", "prefer_authors", - ["mode", "period", "author_id", "name", "count", "read_time"], - []), - - ("prefer_time", "prefer_time", - ["mode", "period", "hour_index", "duration"], - []), - - ("notebooks", "notebooks", - ["book_id", "title", "author", "cover", "review_count", "note_count", - "bookmark_count", "reading_progress", "marked_status", "sort"], - []), - - ("read_stat_items", "read_stat_items", - ["mode", "period", "stat_name", "counts", "scheme"], - []), - - ("archives", "archives", - ["name", "book_ids"], - []), - - ("fetch_meta", "fetch_meta", - ["key", "value"], - []), - - ("book_details", "book_details", - ["book_id", "title", "author", "translator", "cover", "intro", "category", - "publisher", "publish_time", "isbn", "word_count", "new_rating", "new_rating_count"], - ["fetched_at"]), - - ("book_progress", "book_progress", - ["book_id", "progress", "chapter_uid", "chapter_offset", "update_time", - "record_reading_time", "finish_time", "is_start_reading"], - ["fetched_at"]), - - ("book_highlights", "book_highlights", - ["book_id", "bookmark_id", "chapter_uid", "mark_text", "create_time", - "type", "range_text", "color_style"], - ["fetched_at"]), - - ("book_chapters", "book_chapters", - ["book_id", "chapter_uid", "chapter_idx", "title"], - []), - - ("book_reviews", "book_reviews", - ["book_id", "review_id", "content", "create_time", "star", - "chapter_name", "is_finish"], - ["fetched_at"]), - - ("recommended_books", "recommended_books", - ["book_id", "title", "author", "cover", "intro", "category", "reason", - "reading_count", "new_rating", "new_rating_count", "rating_title"], - ["fetched_at"]), - - ("search_results", "search_results", - ["keyword", "book_id", "title", "author", "cover", "intro", "publisher", - "category", "reading_count", "new_rating", "new_rating_count", - "rating_title", "search_idx"], - ["fetched_at"]), +# 需要迁移的表名列表(按依赖顺序排列) +TABLES = [ + # 书架数据 + "books", + "shelf_albums", + "shelf_meta", + "archives", + # 阅读统计 + "reading_stats", + "read_times", + "read_longest", + "read_stat_items", + "prefer_categories", + "prefer_authors", + "prefer_publishers", + "prefer_cps", + "prefer_books", + "prefer_time", + "read_rank", + "medals", + "year_reports", + # 笔记数据 + "notebooks", + "book_highlights", + "book_chapters", + "book_reviews", + "book_best_bookmarks", + "book_underlines", + "public_reviews", + # 书籍详情 + "book_details", + "book_progress", + # 发现/推荐 + "recommended_books", + "similar_books", + "search_sessions", + "search_results", + # 系统 + "fetch_meta", ] -def convert_timestamp(value): - """将 SQLite 中的 int 时间戳转为 MySQL datetime 字符串""" - if value is None or value == 0: +def convert_dt(val): + """将 SQLite 中的时间文本转换为 MySQL TIMESTAMP 兼容格式""" + if val is None or val == "": return None - try: - return datetime.fromtimestamp(int(value)).strftime("%Y-%m-%d %H:%M:%S") - except (ValueError, OSError, OverflowError): + if isinstance(val, (int, float)): return None + # SQLite datetime('now','localtime') 格式: "2026-07-17 10:00:00" + # 已经是 MySQL 兼容格式,直接返回 + return str(val) def migrate(): - """执行迁移""" + """执行完整迁移""" + # 连接 SQLite sqlite_conn = sqlite3.connect(SQLITE_PATH) sqlite_conn.row_factory = sqlite3.Row sc = sqlite_conn.cursor() + # 连接 MySQL mysql_conn = pymysql.connect(**MYSQL_CONFIG) mc = mysql_conn.cursor() total_rows = 0 + migrated_tables = 0 - for sqlite_table, mysql_table, columns, ts_cols in TABLE_MAPPING: - # 读取 SQLite 数据 - col_list = ", ".join(columns) - sc.execute(f"SELECT {col_list} FROM \"{sqlite_table}\"") - rows = sc.fetchall() + print("=" * 60) + print("SQLite → MySQL 数据迁移 (v2.0)") + print("=" * 60) - if not rows: - print(f" {sqlite_table:30s} 0 行 (跳过)") + for table in TABLES: + # 检查 SQLite 表是否有数据 + sc.execute(f'SELECT COUNT(*) FROM "{table}"') + row_count = sc.fetchone()[0] + + if row_count == 0: + print(f" {table:30s} 跳过 (无数据)") continue - # 构建 INSERT 语句 + # 获取列名 + sc.execute(f'PRAGMA table_info("{table}")') + columns = [col[1] for col in sc.fetchall()] + + # 清空 MySQL 表 + mc.execute(f"DELETE FROM `{table}`") + + # 读取 SQLite 数据 + sc.execute(f'SELECT {", ".join(f"\"{c}\"" for c in columns)} FROM "{table}"') + rows = sc.fetchall() + + # 构建批量 INSERT placeholders = ", ".join(["%s"] * len(columns)) - col_names = ", ".join(f"`{c}`" for c in columns) - sql = f"INSERT INTO `{mysql_table}` ({col_names}) VALUES ({placeholders})" + col_list = ", ".join(f"`{c}`" for c in columns) + insert_sql = f"INSERT INTO `{table}` ({col_list}) VALUES ({placeholders})" # 转换数据 batch = [] for row in rows: values = [] - for i, col in enumerate(columns): - val = row[i] - if col in ts_cols: - val = convert_timestamp(val) + for col in columns: + val = row[col] values.append(val) - batch.append(tuple(values)) + batch.append(values) # 批量插入 - mc.executemany(sql, batch) + mc.executemany(insert_sql, batch) mysql_conn.commit() - count = len(batch) - total_rows += count - print(f" {sqlite_table:30s} {count:>6} 行 ✓") + total_rows += row_count + migrated_tables += 1 + print(f" {table:30s} {row_count:>6} 行 ✓") - # 验证 - print(f"\n{'='*50}") - print(f"迁移完成!共迁移 {total_rows} 行数据") - print(f"{'='*50}") + # 更新 fetch_meta + mc.execute( + "INSERT INTO fetch_meta (`key`, `value`) VALUES ('mysql_migrated_at', %s) " + "ON DUPLICATE KEY UPDATE `value` = %s", + (datetime.now().strftime("%Y-%m-%d %H:%M:%S"), + datetime.now().strftime("%Y-%m-%d %H:%M:%S")), + ) + mysql_conn.commit() - print("\nMySQL 各表数据量验证:") + print(f"\n{'=' * 60}") + print(f"迁移完成! {migrated_tables} 张表, 共 {total_rows} 行数据") + print(f"MySQL: {MYSQL_CONFIG['host']}:{MYSQL_CONFIG['port']}/{MYSQL_CONFIG['database']}") + print(f"{'=' * 60}") + + # 数据校验 + print("\n数据校验:") mc.execute("SHOW TABLES") - tables = [t[0] for t in mc.fetchall()] - for t in tables: + mysql_tables = [t[0] for t in mc.fetchall()] + for t in mysql_tables: mc.execute(f"SELECT COUNT(*) FROM `{t}`") cnt = mc.fetchone()[0] - print(f" {t:30s} {cnt:>6} 行") + if cnt > 0: + print(f" MySQL {t:30s} {cnt:>6} 行") sqlite_conn.close() mysql_conn.close() if __name__ == "__main__": - print("开始迁移 SQLite -> MySQL ...\n") migrate() diff --git a/schema_mysql.sql b/schema_mysql.sql index 1fe7a89..3339519 100644 --- a/schema_mysql.sql +++ b/schema_mysql.sql @@ -1,7 +1,13 @@ -- ============================================================ --- 微信读书数据展示网站 - MySQL 版本建表语句 --- 适用于后续从 SQLite 迁移至 MySQL --- 字符集: utf8mb4 (支持完整 Unicode,包括 emoji) +-- 微信读书数据展示网站 - MySQL 版本建表语句(完整版 v2.0) +-- ============================================================ +-- 设计规范: +-- 1. 所有整型时间戳字段均伴随一个 _dt TIMESTAMP 列,便于直接查询和展示 +-- 2. 每张表均维护 created_at(创建时间)和 updated_at(更新时间) +-- 3. 主键、唯一约束、查询索引根据实际访问模式设计 +-- 4. 字符集 utf8mb4,引擎 InnoDB +-- 5. 每个字段带 COMMENT 中文注释 +-- 6. 覆盖微信读书全部 8 份官方 API 文档的所有返回字段 -- ============================================================ CREATE DATABASE IF NOT EXISTS `weread_dashboard` @@ -11,315 +17,624 @@ CREATE DATABASE IF NOT EXISTS `weread_dashboard` USE `weread_dashboard`; -- ============================================================ --- 1. 书架书籍表 +-- ===== 第一组:书架数据 ===== -- ============================================================ + +-- 1. 书架书籍表(电子书,来自 /shelf/sync 的 books[]) CREATE TABLE IF NOT EXISTS `books` ( - `book_id` VARCHAR(64) NOT NULL COMMENT '书籍唯一标识', - `title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '书名', - `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', - `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL', - `category` VARCHAR(128) DEFAULT NULL COMMENT '分类', - `finish_reading` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否读完 (1=读完)', - `read_update_time` INT UNSIGNED DEFAULT NULL COMMENT '最近阅读时间 (Unix 时间戳)', - `secret` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否私密 (1=私密)', - `is_top` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶 (1=置顶)', - `update_time` INT UNSIGNED DEFAULT NULL COMMENT '书籍更新时间 (Unix 时间戳)', - `deep_link` VARCHAR(512) DEFAULT NULL COMMENT '深度链接', + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍唯一标识', + `title` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '书名', + `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', + `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL', + `category` VARCHAR(128) DEFAULT NULL COMMENT '分类(如 "文学-古代诗词")', + `finish_reading` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否读完 (1=读完)', + `read_update_time` INT UNSIGNED DEFAULT NULL COMMENT '最近阅读时间 (Unix 时间戳)', + `read_update_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '最近阅读时间 (可读格式)', + `secret` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否私密 (1=私密)', + `is_top` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶 (1=置顶)', + `update_time` INT UNSIGNED DEFAULT NULL COMMENT '书籍更新时间 (Unix 时间戳)', + `update_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '书籍更新时间 (可读格式)', + `deep_link` VARCHAR(512) DEFAULT NULL COMMENT '微信读书 App 深度链接', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`book_id`), INDEX `idx_books_category` (`category`), INDEX `idx_books_finish_reading` (`finish_reading`), - INDEX `idx_books_read_update_time` (`read_update_time` DESC) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='书架书籍信息'; + INDEX `idx_books_secret` (`secret`), + INDEX `idx_books_is_top` (`is_top`), + INDEX `idx_books_read_update_time` (`read_update_time` DESC), + INDEX `idx_books_title` (`title`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='书架电子书'; --- ============================================================ --- 2. 阅读统计概要表 --- ============================================================ -CREATE TABLE IF NOT EXISTS `reading_stats` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `mode` VARCHAR(20) NOT NULL COMMENT '统计维度: overall/annually/monthly/weekly', - `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', - `total_read_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '总阅读时长 (秒)', - `read_days` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '有效阅读天数', - `day_average_read_time` DOUBLE DEFAULT 0 COMMENT '日均阅读时长 (秒)', - `regist_time` INT UNSIGNED DEFAULT NULL COMMENT '用户注册时间 (Unix 时间戳)', - `raw_json` LONGTEXT DEFAULT NULL COMMENT '原始 API 返回 JSON', - `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', +-- 2. 书架有声书/专辑表(来自 /shelf/sync 的 albums[]) +CREATE TABLE IF NOT EXISTS `shelf_albums` ( + `album_id` VARCHAR(64) NOT NULL COMMENT '专辑唯一标识', + `name` VARCHAR(255) DEFAULT NULL COMMENT '专辑名称', + `author_name` VARCHAR(255) DEFAULT NULL COMMENT '演播/作者', + `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL', + `track_count` INT UNSIGNED DEFAULT 0 COMMENT '音频集数', + `finish_status` VARCHAR(32) DEFAULT NULL COMMENT '完结状态文案(如 "已完结")', + `finish` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否完结 (1=完结)', + `pay_type` INT UNSIGNED DEFAULT NULL COMMENT '付费类型', + `intro` TEXT DEFAULT NULL COMMENT '专辑简介', + `update_time` INT UNSIGNED DEFAULT NULL COMMENT '更新时间 (Unix 时间戳)', + `update_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '更新时间 (可读格式)', + `secret` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否私密 (1=私密)', + `lecture_paid` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已购买 (1=已购买)', + `lecture_read_update_time` INT UNSIGNED DEFAULT NULL COMMENT '最近收听时间 (Unix 时间戳)', + `lecture_read_update_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '最近收听时间 (可读格式)', + `is_top` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否置顶 (1=置顶)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`album_id`), + INDEX `idx_albums_finish` (`finish`), + INDEX `idx_albums_secret` (`secret`), + INDEX `idx_albums_lecture_read_time` (`lecture_read_update_time` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='书架有声书/专辑'; + +-- 3. 书架元信息表(来自 /shelf/sync 的 mp 和 bookCount) +CREATE TABLE IF NOT EXISTS `shelf_meta` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mp_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否有文章收藏入口 (1=有)', + `book_count` INT UNSIGNED DEFAULT 0 COMMENT '可枚举电子书数量', + `album_count` INT UNSIGNED DEFAULT 0 COMMENT '有声书/专辑数量', + `total_count` INT UNSIGNED DEFAULT 0 COMMENT '书架总条目数 (books + albums + mp)', + `raw_json` LONGTEXT DEFAULT NULL COMMENT '原始 API 返回 JSON', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='书架元信息'; + +-- 4. 书单/归档表(来自 /shelf/sync 的 archive[]) +CREATE TABLE IF NOT EXISTS `archives` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `name` VARCHAR(255) DEFAULT NULL COMMENT '书单名称', + `book_ids` LONGTEXT DEFAULT NULL COMMENT 'bookId 列表 (JSON 数组)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`id`), - INDEX `idx_reading_stats_mode` (`mode`, `period`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='阅读统计概要'; + INDEX `idx_archives_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='书单/归档'; -- ============================================================ --- 3. 阅读时长时间序列(按年/月/日分桶) +-- ===== 第二组:阅读统计数据 ===== -- ============================================================ + +-- 5. 阅读统计概要表(来自 /readdata/detail) +CREATE TABLE IF NOT EXISTS `reading_stats` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度: overall/annually/monthly/weekly', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识 (如 "overall" 或 "2026")', + `base_time` INT UNSIGNED DEFAULT NULL COMMENT '统计周期基准时间戳', + `base_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '统计周期基准时间 (可读格式)', + `total_read_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '总阅读时长 (秒)', + `read_days` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '有效阅读天数', + `day_average_read_time` DOUBLE DEFAULT 0 COMMENT '日均阅读时长 (秒)', + `compare` DOUBLE DEFAULT NULL COMMENT '与上周期对比比例 (正=增长)', + `regist_time` INT UNSIGNED DEFAULT NULL COMMENT '用户注册时间 (Unix 时间戳)', + `regist_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '用户注册时间 (可读格式)', + `record_reading_time` INT UNSIGNED DEFAULT 0 COMMENT '总朗读/记录类阅读时长 (秒)', + `read_rate` DOUBLE DEFAULT NULL COMMENT '文字阅读占比百分比', + `wr_read_time` INT UNSIGNED DEFAULT NULL COMMENT '文字阅读时长 (秒)', + `wr_listen_time` INT UNSIGNED DEFAULT NULL COMMENT '听书时长 (秒)', + `author_count` INT UNSIGNED DEFAULT NULL COMMENT '符合统计条件的作者总数', + `prefer_category_word` VARCHAR(128) DEFAULT NULL COMMENT '偏好分类文案 (如 "偏好阅读文学")', + `prefer_time_word` VARCHAR(128) DEFAULT NULL COMMENT '偏好时段文案 (如 "偏好上午阅读")', + `read_records_word` VARCHAR(64) DEFAULT NULL COMMENT '书籍分布模块标题文案', + `read_distribution_word` VARCHAR(64) DEFAULT NULL COMMENT '点评分布模块标题文案', + `read_time_gears` VARCHAR(128) DEFAULT NULL COMMENT '阅读时长档位 (JSON 数组)', + `style_type` VARCHAR(32) DEFAULT NULL COMMENT '样式类型 (如 normal)', + `raw_json` LONGTEXT DEFAULT NULL COMMENT '原始 API 返回 JSON', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_reading_stats_mode_period` (`mode`, `period`), + INDEX `idx_reading_stats_mode` (`mode`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='阅读统计概要'; + +-- 6. 阅读时长时间序列表(来自 /readdata/detail 的 readTimes 和 dailyReadTimes) CREATE TABLE IF NOT EXISTS `read_times` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `mode` VARCHAR(30) NOT NULL COMMENT '统计维度 (含_daily后缀)', - `period` VARCHAR(20) NOT NULL DEFAULT '', - `bucket_timestamp` INT UNSIGNED NOT NULL COMMENT '分桶起始时间戳', - `duration` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时长 (秒)', + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(30) NOT NULL COMMENT '统计维度 (含 _daily 后缀表示每日数据)', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `bucket_timestamp` INT UNSIGNED NOT NULL COMMENT '分桶起始时间戳', + `bucket_timestamp_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '分桶起始时间 (可读格式)', + `duration` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时长 (秒)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`id`), INDEX `idx_read_times_mode_period` (`mode`, `period`), INDEX `idx_read_times_bucket` (`bucket_timestamp`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='阅读时长时间序列'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='阅读时长时间序列'; --- ============================================================ --- 4. 阅读时长最久的书籍排行 --- ============================================================ +-- 7. 阅读时长排行表(来自 /readdata/detail 的 readLongest[]) CREATE TABLE IF NOT EXISTS `read_longest` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `mode` VARCHAR(20) NOT NULL, - `period` VARCHAR(20) NOT NULL DEFAULT '', - `book_id` VARCHAR(64) NOT NULL, - `title` VARCHAR(255) DEFAULT NULL, - `author` VARCHAR(255) DEFAULT NULL, - `cover` VARCHAR(512) DEFAULT NULL, - `read_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时长 (秒)', - `tags` VARCHAR(512) DEFAULT NULL COMMENT '标签 JSON 数组', + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `title` VARCHAR(255) DEFAULT NULL COMMENT '书名', + `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', + `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL', + `read_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时长 (秒)', + `record_reading_time` INT UNSIGNED DEFAULT NULL COMMENT '朗读/记录类阅读时长 (秒)', + `tags` VARCHAR(512) DEFAULT NULL COMMENT '标签 (JSON 数组,如 ["笔记最多"])', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`id`), - INDEX `idx_read_longest_mode` (`mode`, `period`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='阅读时长排行'; + INDEX `idx_read_longest_mode_period` (`mode`, `period`), + INDEX `idx_read_longest_book_id` (`book_id`), + INDEX `idx_read_longest_read_time` (`read_time` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='阅读时长排行'; --- ============================================================ --- 5. 偏好分类 --- ============================================================ -CREATE TABLE IF NOT EXISTS `prefer_categories` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `mode` VARCHAR(20) NOT NULL, - `period` VARCHAR(20) NOT NULL DEFAULT '', - `category_id` INT UNSIGNED DEFAULT NULL, - `category_title` VARCHAR(128) DEFAULT NULL, - `parent_category_title` VARCHAR(128) DEFAULT NULL, - `val` DOUBLE DEFAULT 0 COMMENT '偏好权重', - `reading_time` INT UNSIGNED DEFAULT 0 COMMENT '该分类阅读时长 (秒)', - `reading_count` INT UNSIGNED DEFAULT 0 COMMENT '该分类阅读本数', - PRIMARY KEY (`id`), - INDEX `idx_prefer_cat_mode` (`mode`, `period`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='偏好阅读分类'; - --- ============================================================ --- 6. 偏好作者 --- ============================================================ -CREATE TABLE IF NOT EXISTS `prefer_authors` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `mode` VARCHAR(20) NOT NULL, - `period` VARCHAR(20) NOT NULL DEFAULT '', - `author_id` INT UNSIGNED DEFAULT NULL, - `name` VARCHAR(255) DEFAULT NULL, - `count` INT UNSIGNED DEFAULT 0 COMMENT '阅读该作者的书本数', - `read_time` VARCHAR(64) DEFAULT NULL COMMENT '阅读时长 (格式化字符串)', - PRIMARY KEY (`id`), - INDEX `idx_prefer_author_mode` (`mode`, `period`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='偏好作者'; - --- ============================================================ --- 7. 24小时阅读时段分布 --- ============================================================ -CREATE TABLE IF NOT EXISTS `prefer_time` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `mode` VARCHAR(20) NOT NULL, - `period` VARCHAR(20) NOT NULL DEFAULT '', - `hour_index` TINYINT UNSIGNED NOT NULL COMMENT '小时 (0-23)', - `duration` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时长 (秒)', - PRIMARY KEY (`id`), - INDEX `idx_prefer_time_mode` (`mode`, `period`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='24小时阅读时段分布'; - --- ============================================================ --- 8. 笔记本概览 --- ============================================================ -CREATE TABLE IF NOT EXISTS `notebooks` ( - `book_id` VARCHAR(64) NOT NULL, - `title` VARCHAR(255) DEFAULT NULL, - `author` VARCHAR(255) DEFAULT NULL, - `cover` VARCHAR(512) DEFAULT NULL, - `review_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '想法/点评数', - `note_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '划线数', - `bookmark_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '书签数', - `reading_progress` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读进度 (0-100)', - `marked_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '标记状态 (1=读完)', - `sort` BIGINT DEFAULT NULL COMMENT '排序值', - PRIMARY KEY (`book_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='笔记本概览'; - --- ============================================================ --- 9. 阅读统计摘要项 --- ============================================================ +-- 8. 阅读统计摘要项表(来自 /readdata/detail 的 readStat[]) CREATE TABLE IF NOT EXISTS `read_stat_items` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `mode` VARCHAR(20) NOT NULL, - `period` VARCHAR(20) NOT NULL DEFAULT '', - `stat_name` VARCHAR(64) DEFAULT NULL COMMENT '统计项名称 (读过/读完/阅读/笔记)', - `counts` VARCHAR(64) DEFAULT NULL COMMENT '统计值文案', - `scheme` VARCHAR(512) DEFAULT NULL COMMENT 'App 跳转链接', + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `stat_name` VARCHAR(64) DEFAULT NULL COMMENT '统计项名称 (读过/读完/阅读/笔记)', + `counts` VARCHAR(64) DEFAULT NULL COMMENT '统计值文案 (如 "404本")', + `scheme` VARCHAR(512) DEFAULT NULL COMMENT 'App 跳转链接', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`id`), - INDEX `idx_read_stat_items_mode` (`mode`, `period`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='阅读统计摘要项'; + INDEX `idx_read_stat_items_mode_period` (`mode`, `period`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='阅读统计摘要项'; + +-- 9. 偏好分类表(来自 /readdata/detail 的 preferCategory[]) +CREATE TABLE IF NOT EXISTS `prefer_categories` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `category_id` INT UNSIGNED DEFAULT NULL COMMENT '分类 ID', + `category_title` VARCHAR(128) DEFAULT NULL COMMENT '分类名称 (如 "文学")', + `parent_category_id` INT UNSIGNED DEFAULT NULL COMMENT '父分类 ID', + `parent_category_title` VARCHAR(128) DEFAULT NULL COMMENT '父分类名称 (如 "精品小说")', + `category_type` TINYINT DEFAULT 0 COMMENT '分类类型标记 (0=普通, 1/2=特殊)', + `val` DOUBLE DEFAULT 0 COMMENT '偏好权重 (归一化相对值)', + `reading_time` INT UNSIGNED DEFAULT 0 COMMENT '该分类阅读时长 (秒)', + `reading_count` INT UNSIGNED DEFAULT 0 COMMENT '该分类阅读本数', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_prefer_cat_mode_period` (`mode`, `period`), + INDEX `idx_prefer_cat_category_id` (`category_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='偏好阅读分类'; + +-- 10. 偏好作者表(来自 /readdata/detail 的 preferAuthor[]) +CREATE TABLE IF NOT EXISTS `prefer_authors` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `author_id` INT UNSIGNED DEFAULT NULL COMMENT '作者 ID', + `name` VARCHAR(255) DEFAULT NULL COMMENT '作者名', + `count` INT UNSIGNED DEFAULT 0 COMMENT '阅读该作者的书本数', + `read_time` VARCHAR(64) DEFAULT NULL COMMENT '阅读时长 (格式化字符串,如 "5小时30分钟")', + `user_json` TEXT DEFAULT NULL COMMENT '作者关联用户信息 (JSON)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_prefer_author_mode_period` (`mode`, `period`), + INDEX `idx_prefer_author_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='偏好作者'; + +-- 11. 偏好出版社表(来自 /readdata/detail 的 preferPublisher[]) +CREATE TABLE IF NOT EXISTS `prefer_publishers` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `name` VARCHAR(255) DEFAULT NULL COMMENT '出版社名', + `count` INT UNSIGNED DEFAULT 0 COMMENT '阅读该出版社书籍本数', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_prefer_pub_mode_period` (`mode`, `period`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='偏好出版社'; + +-- 12. 偏好版权方表(来自 /readdata/detail 的 preferCp[]) +CREATE TABLE IF NOT EXISTS `prefer_cps` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `count` INT UNSIGNED DEFAULT 0 COMMENT '阅读该版权方书籍本数', + `cp_name` VARCHAR(255) DEFAULT NULL COMMENT '版权方名称', + `cp_user_vid` VARCHAR(64) DEFAULT NULL COMMENT '版权方用户 VID', + `cp_avatar` VARCHAR(512) DEFAULT NULL COMMENT '版权方头像 URL', + `cp_role` VARCHAR(64) DEFAULT NULL COMMENT '版权方角色', + `copyright_json` TEXT DEFAULT NULL COMMENT '版权方完整信息 (JSON)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_prefer_cp_mode_period` (`mode`, `period`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='偏好版权方'; + +-- 13. 偏好书籍卡片表(来自 /readdata/detail 的 preferBooks[]) +CREATE TABLE IF NOT EXISTS `prefer_books` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `book_id` VARCHAR(64) DEFAULT NULL COMMENT '书籍 ID', + `title` VARCHAR(255) DEFAULT NULL COMMENT '书名', + `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', + `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL', + `reason` TEXT DEFAULT NULL COMMENT '推荐理由', + `prefer_type` VARCHAR(64) DEFAULT NULL COMMENT '偏好类型', + `book_json` TEXT DEFAULT NULL COMMENT '书籍完整信息 (JSON)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_prefer_books_mode_period` (`mode`, `period`), + INDEX `idx_prefer_books_book_id` (`book_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='偏好书籍卡片'; + +-- 14. 24小时阅读时段分布表(来自 /readdata/detail 的 preferTime[]) +CREATE TABLE IF NOT EXISTS `prefer_time` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `hour_index` TINYINT UNSIGNED NOT NULL COMMENT '小时 (0-23)', + `duration` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读时长 (秒)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_prefer_time_mode_period` (`mode`, `period`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='24小时阅读时段分布'; + +-- 15. 好友阅读排行表(来自 /readdata/detail 的 rank) +CREATE TABLE IF NOT EXISTS `read_rank` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `rank_text` VARCHAR(255) DEFAULT NULL COMMENT '排行文案 (如 "朋友中排第3名")', + `scheme` VARCHAR(512) DEFAULT NULL COMMENT '排行跳转链接', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_read_rank_mode_period` (`mode`, `period`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='好友阅读排行'; + +-- 16. 勋章表(来自 /readdata/detail 的 medals[]) +CREATE TABLE IF NOT EXISTS `medals` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `medal_json` TEXT DEFAULT NULL COMMENT '勋章信息 (JSON)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_medals_mode_period` (`mode`, `period`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='勋章'; + +-- 17. 年度报告表(来自 /readdata/detail 的 yearReport[]) +CREATE TABLE IF NOT EXISTS `year_reports` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `mode` VARCHAR(20) NOT NULL COMMENT '统计维度', + `period` VARCHAR(20) NOT NULL DEFAULT '' COMMENT '周期标识', + `report_year` VARCHAR(10) DEFAULT NULL COMMENT '报告年份', + `report_json` TEXT DEFAULT NULL COMMENT '报告入口信息 (JSON)', + `monthly_times` VARCHAR(512) DEFAULT NULL COMMENT '12个月阅读时长 (JSON 数组,单位秒)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_year_reports_mode_period` (`mode`, `period`), + INDEX `idx_year_reports_year` (`report_year`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='年度报告'; -- ============================================================ --- 10. 书单/归档 +-- ===== 第三组:笔记数据 ===== -- ============================================================ -CREATE TABLE IF NOT EXISTS `archives` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `name` VARCHAR(255) DEFAULT NULL COMMENT '书单名称', - `book_ids` LONGTEXT DEFAULT NULL COMMENT 'bookId 列表 JSON 数组', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='书单/归档'; --- ============================================================ --- 11. 数据采集元信息 --- ============================================================ -CREATE TABLE IF NOT EXISTS `fetch_meta` ( - `key` VARCHAR(64) NOT NULL, - `value` TEXT DEFAULT NULL, - PRIMARY KEY (`key`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='数据采集元信息'; +-- 18. 笔记本概览表(来自 /user/notebooks) +CREATE TABLE IF NOT EXISTS `notebooks` ( + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `title` VARCHAR(255) DEFAULT NULL COMMENT '书名', + `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', + `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL', + `review_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '想法/点评数 (含划线想法、书评想法等)', + `note_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '划线数 (高亮标注原文条数)', + `bookmark_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '书签数 (标记阅读位置条数)', + `total_note_count` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '笔记总数 (reviewCount + noteCount + bookmarkCount)', + `reading_progress` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读进度 (0-100)', + `marked_status` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '标记状态 (1=读完)', + `sort` BIGINT DEFAULT NULL COMMENT '排序值 (最近笔记时间,用于翻页)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`book_id`), + INDEX `idx_notebooks_marked_status` (`marked_status`), + INDEX `idx_notebooks_sort` (`sort` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='笔记本概览'; --- ============================================================ --- 12. 书籍详细信息(按需获取,来自 /book/info) --- ============================================================ -CREATE TABLE IF NOT EXISTS `book_details` ( - `book_id` VARCHAR(64) NOT NULL, - `title` VARCHAR(255) DEFAULT NULL, - `author` VARCHAR(255) DEFAULT NULL, - `translator` VARCHAR(255) DEFAULT NULL COMMENT '译者', - `cover` VARCHAR(512) DEFAULT NULL, - `intro` LONGTEXT DEFAULT NULL COMMENT '简介', - `category` VARCHAR(128) DEFAULT NULL, - `publisher` VARCHAR(255) DEFAULT NULL, - `publish_time` VARCHAR(32) DEFAULT NULL COMMENT '出版时间', - `isbn` VARCHAR(32) DEFAULT NULL, - `word_count` INT UNSIGNED DEFAULT 0 COMMENT '总字数', - `new_rating` DECIMAL(5,1) DEFAULT 0 COMMENT '评分 (百分制)', - `new_rating_count` INT UNSIGNED DEFAULT 0 COMMENT '评分人数', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '获取时间', - PRIMARY KEY (`book_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='书籍详细信息'; - --- ============================================================ --- 13. 阅读进度(按需获取,来自 /book/getprogress) --- ============================================================ -CREATE TABLE IF NOT EXISTS `book_progress` ( - `book_id` VARCHAR(64) NOT NULL, - `progress` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读进度百分比 (0-100)', - `chapter_uid` INT UNSIGNED DEFAULT NULL COMMENT '当前阅读章节 UID', - `chapter_offset` INT UNSIGNED DEFAULT NULL COMMENT '当前章节内偏移', - `update_time` INT UNSIGNED DEFAULT NULL COMMENT '最后阅读时间 (Unix 时间戳)', - `record_reading_time` INT UNSIGNED DEFAULT 0 COMMENT '累计阅读时长 (秒)', - `finish_time` INT UNSIGNED DEFAULT NULL COMMENT '读完时间 (Unix 时间戳)', - `is_start_reading` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已开始阅读', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`book_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='阅读进度'; - --- ============================================================ --- 14. 划线内容(按需获取,来自 /book/bookmarklist) --- ============================================================ +-- 19. 划线内容表(来自 /book/bookmarklist 的 updated[]) CREATE TABLE IF NOT EXISTS `book_highlights` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `book_id` VARCHAR(64) NOT NULL, - `bookmark_id` VARCHAR(64) DEFAULT NULL COMMENT '划线唯一 ID', - `chapter_uid` INT UNSIGNED DEFAULT NULL COMMENT '所在章节 UID', - `mark_text` LONGTEXT DEFAULT NULL COMMENT '划线原文', - `create_time` INT UNSIGNED DEFAULT NULL COMMENT '创建时间 (Unix 时间戳)', - `type` TINYINT DEFAULT NULL COMMENT '类型', - `range_text` VARCHAR(64) DEFAULT NULL COMMENT '位置范围', - `color_style` TINYINT DEFAULT NULL COMMENT '划线颜色样式', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `bookmark_id` VARCHAR(64) DEFAULT NULL COMMENT '划线唯一 ID', + `chapter_uid` INT UNSIGNED DEFAULT NULL COMMENT '所在章节 UID', + `mark_text` LONGTEXT DEFAULT NULL COMMENT '划线原文内容', + `create_time` INT UNSIGNED DEFAULT NULL COMMENT '创建时间 (Unix 时间戳)', + `create_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '创建时间 (可读格式)', + `type` TINYINT DEFAULT NULL COMMENT '类型 (1=划线)', + `range_text` VARCHAR(64) DEFAULT NULL COMMENT '位置范围 (如 "2959-3007")', + `color_style` TINYINT DEFAULT NULL COMMENT '划线颜色样式', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`id`), + UNIQUE KEY `uk_highlights_bookmark` (`book_id`, `bookmark_id`), INDEX `idx_highlights_book_id` (`book_id`), - INDEX `idx_highlights_chapter` (`book_id`, `chapter_uid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='划线内容'; + INDEX `idx_highlights_chapter` (`book_id`, `chapter_uid`), + INDEX `idx_highlights_create_time` (`create_time` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='划线内容'; --- ============================================================ --- 15. 章节信息(来自 /book/bookmarklist 的 chapters) --- ============================================================ +-- 20. 章节信息表(来自 /book/chapterinfo 和 /book/bookmarklist 的 chapters) CREATE TABLE IF NOT EXISTS `book_chapters` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `book_id` VARCHAR(64) NOT NULL, - `chapter_uid` INT UNSIGNED NOT NULL, - `chapter_idx` INT UNSIGNED DEFAULT NULL COMMENT '章节序号', - `title` VARCHAR(255) DEFAULT NULL COMMENT '章节标题', + `id` BIGINT NOT NULL AUTO_INCREMENT, + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `chapter_uid` INT UNSIGNED NOT NULL COMMENT '章节 UID', + `chapter_idx` INT UNSIGNED DEFAULT NULL COMMENT '章节序号', + `title` VARCHAR(255) DEFAULT NULL COMMENT '章节标题', + `word_count` INT UNSIGNED DEFAULT NULL COMMENT '章节字数', + `level` TINYINT DEFAULT NULL COMMENT '目录层级 (1=一级标题, 2=二级)', + `update_time` INT UNSIGNED DEFAULT NULL COMMENT '章节更新时间 (Unix 时间戳)', + `update_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '章节更新时间 (可读格式)', + `price` INT UNSIGNED DEFAULT 0 COMMENT '章节价格 (0=免费)', + `paid` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已购买 (1=已购买)', + `is_mp_chapter` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否公众号章节 (1=是)', + `anchors` TEXT DEFAULT NULL COMMENT '章节内锚点/子标题 (JSON 数组)', + `synckey` INT UNSIGNED DEFAULT NULL COMMENT '同步 key (版本号)', + `chapter_update_time` INT UNSIGNED DEFAULT NULL COMMENT '章节列表最后更新时间 (Unix 时间戳)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`id`), - UNIQUE KEY `uk_book_chapter` (`book_id`, `chapter_uid`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='章节信息'; + UNIQUE KEY `uk_book_chapter` (`book_id`, `chapter_uid`), + INDEX `idx_chapters_book_idx` (`book_id`, `chapter_idx`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='章节信息'; --- ============================================================ --- 16. 个人想法/点评(按需获取,来自 /review/list/mine) --- ============================================================ +-- 21. 个人想法/点评表(来自 /review/list/mine) CREATE TABLE IF NOT EXISTS `book_reviews` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `book_id` VARCHAR(64) NOT NULL, - `review_id` VARCHAR(64) DEFAULT NULL COMMENT '想法唯一 ID', - `content` LONGTEXT DEFAULT NULL COMMENT '内容文本', - `create_time` INT UNSIGNED DEFAULT NULL COMMENT '创建时间 (Unix 时间戳)', - `star` TINYINT DEFAULT -1 COMMENT '评分 (0-5, -1=无评分)', - `chapter_name` VARCHAR(255) DEFAULT NULL COMMENT '所在章节名', - `is_finish` TINYINT(1) DEFAULT 0 COMMENT '是否书评', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `review_id` VARCHAR(64) DEFAULT NULL COMMENT '想法唯一 ID', + `content` LONGTEXT DEFAULT NULL COMMENT '想法内容文本', + `abstract` LONGTEXT DEFAULT NULL COMMENT '想法对应的划线原文 (仅划线想法有值)', + `range_text` VARCHAR(64) DEFAULT NULL COMMENT '划线原文位置范围 (如 "2959-3007")', + `chapter_uid` INT UNSIGNED DEFAULT NULL COMMENT '章节 UID', + `chapter_idx` INT UNSIGNED DEFAULT NULL COMMENT '章节序号', + `chapter_name` VARCHAR(255) DEFAULT NULL COMMENT '所在章节名 (章节点评时有值)', + `create_time` INT UNSIGNED DEFAULT NULL COMMENT '创建时间 (Unix 时间戳)', + `create_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '创建时间 (可读格式)', + `star` TINYINT DEFAULT -1 COMMENT '评分 (0-5, -1=无评分)', + `is_finish` TINYINT(1) DEFAULT 0 COMMENT '是否书评 (1=书评)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`id`), - INDEX `idx_reviews_book_id` (`book_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='个人想法/点评'; + UNIQUE KEY `uk_reviews_review_id` (`review_id`), + INDEX `idx_reviews_book_id` (`book_id`), + INDEX `idx_reviews_chapter` (`book_id`, `chapter_uid`), + INDEX `idx_reviews_create_time` (`create_time` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='个人想法/点评'; + +-- 22. 热门划线表(来自 /book/bestbookmarks) +CREATE TABLE IF NOT EXISTS `book_best_bookmarks` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `bookmark_id` VARCHAR(64) DEFAULT NULL COMMENT '划线唯一 ID', + `chapter_uid` INT UNSIGNED DEFAULT NULL COMMENT '所在章节 UID', + `range_text` VARCHAR(64) DEFAULT NULL COMMENT '划线位置范围 (如 "393-401")', + `mark_text` LONGTEXT DEFAULT NULL COMMENT '划线原文文本', + `total_count` INT UNSIGNED DEFAULT 0 COMMENT '划线人数', + `user_vid` VARCHAR(64) DEFAULT NULL COMMENT '代表用户 VID', + `simplified_range` VARCHAR(64) DEFAULT NULL COMMENT '简体书籍的 range (繁简体书专属)', + `traditional_range` VARCHAR(64) DEFAULT NULL COMMENT '繁体书籍的 range (繁简体书专属)', + `synckey` INT UNSIGNED DEFAULT NULL COMMENT '同步 key (数据版本号)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_best_bm_book_bookmark` (`book_id`, `bookmark_id`), + INDEX `idx_best_bm_book_id` (`book_id`), + INDEX `idx_best_bm_total_count` (`total_count` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='热门划线'; + +-- 23. 划线热度统计表(来自 /book/underlines) +CREATE TABLE IF NOT EXISTS `book_underlines` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `chapter_uid` INT UNSIGNED NOT NULL COMMENT '章节 UID', + `range_text` VARCHAR(64) DEFAULT NULL COMMENT '划线位置范围 (如 "393-401")', + `count` INT UNSIGNED DEFAULT 0 COMMENT '划线人数', + `score` DOUBLE DEFAULT 0 COMMENT '热度分数', + `type` TINYINT DEFAULT NULL COMMENT '划线类型', + `synckey` INT UNSIGNED DEFAULT NULL COMMENT '同步 key', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_underlines_book_ch_range` (`book_id`, `chapter_uid`, `range_text`), + INDEX `idx_underlines_book_chapter` (`book_id`, `chapter_uid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='划线热度统计'; + +-- 24. 公开点评表(来自 /review/list) +CREATE TABLE IF NOT EXISTS `public_reviews` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `review_id` VARCHAR(64) DEFAULT NULL COMMENT '点评唯一 ID', + `content` LONGTEXT DEFAULT NULL COMMENT '点评文本内容', + `html_content` LONGTEXT DEFAULT NULL COMMENT '点评 HTML 富文本内容', + `star` TINYINT DEFAULT -1 COMMENT '评分 (20=一星, 40=二星, 60=三星, 80=四星, 100=五星)', + `is_finish` TINYINT(1) DEFAULT 0 COMMENT '是否读完此书 (1=读完)', + `create_time` INT UNSIGNED DEFAULT NULL COMMENT '创建时间 (Unix 时间戳)', + `create_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '创建时间 (可读格式)', + `chapter_name` VARCHAR(255) DEFAULT NULL COMMENT '所在章节名 (章节点评时有值)', + `author_vid` VARCHAR(64) DEFAULT NULL COMMENT '评论者用户 VID', + `author_name` VARCHAR(255) DEFAULT NULL COMMENT '评论者昵称', + `author_avatar` VARCHAR(512) DEFAULT NULL COMMENT '评论者头像 URL', + `review_list_type` TINYINT DEFAULT 0 COMMENT '筛选类型 (0=全部, 1=推荐, 2=不行, 3=最新, 4=一般)', + `idx` INT UNSIGNED DEFAULT NULL COMMENT '序号 (用于翻页)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_public_reviews_review_id` (`review_id`), + INDEX `idx_public_reviews_book_id` (`book_id`), + INDEX `idx_public_reviews_star` (`star`), + INDEX `idx_public_reviews_create_time` (`create_time` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='公开点评'; -- ============================================================ --- 17. 推荐书籍(按需获取,来自 /book/recommend) +-- ===== 第四组:书籍详情 ===== -- ============================================================ + +-- 25. 书籍详细信息表(来自 /book/info) +CREATE TABLE IF NOT EXISTS `book_details` ( + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `title` VARCHAR(255) DEFAULT NULL COMMENT '书名', + `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', + `translator` VARCHAR(255) DEFAULT NULL COMMENT '译者', + `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL', + `intro` LONGTEXT DEFAULT NULL COMMENT '简介', + `category` VARCHAR(128) DEFAULT NULL COMMENT '分类', + `publisher` VARCHAR(255) DEFAULT NULL COMMENT '出版社', + `publish_time` VARCHAR(32) DEFAULT NULL COMMENT '出版时间', + `isbn` VARCHAR(32) DEFAULT NULL COMMENT 'ISBN 号', + `word_count` INT UNSIGNED DEFAULT 0 COMMENT '总字数', + `new_rating` DECIMAL(5,1) DEFAULT 0 COMMENT '评分 (百分制)', + `new_rating_count` INT UNSIGNED DEFAULT 0 COMMENT '评分人数', + `new_rating_detail` TEXT DEFAULT NULL COMMENT '评分分布详情 (JSON)', + `deep_link` VARCHAR(512) DEFAULT NULL COMMENT '跳转链接', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`book_id`), + INDEX `idx_book_details_isbn` (`isbn`), + INDEX `idx_book_details_category` (`category`), + INDEX `idx_book_details_publisher` (`publisher`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='书籍详细信息'; + +-- 26. 阅读进度表(来自 /book/getprogress) +CREATE TABLE IF NOT EXISTS `book_progress` ( + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `progress` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '阅读进度百分比 (0-100)', + `chapter_uid` INT UNSIGNED DEFAULT NULL COMMENT '当前阅读章节 UID', + `chapter_offset` INT UNSIGNED DEFAULT NULL COMMENT '当前章节内偏移量', + `update_time` INT UNSIGNED DEFAULT NULL COMMENT '最后阅读时间 (Unix 时间戳)', + `update_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '最后阅读时间 (可读格式)', + `record_reading_time` INT UNSIGNED DEFAULT 0 COMMENT '累计阅读时长 (秒)', + `finish_time` INT UNSIGNED DEFAULT NULL COMMENT '读完时间 (Unix 时间戳)', + `finish_time_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '读完时间 (可读格式)', + `is_start_reading` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否已开始阅读 (1=是)', + `server_timestamp` INT UNSIGNED DEFAULT NULL COMMENT '服务端时间戳', + `server_timestamp_dt` TIMESTAMP NULL DEFAULT NULL COMMENT '服务端时间 (可读格式)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`book_id`), + INDEX `idx_progress_finish_time` (`finish_time` DESC), + INDEX `idx_progress_update_time` (`update_time` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='阅读进度'; + +-- ============================================================ +-- ===== 第五组:发现/推荐 ===== +-- ============================================================ + +-- 27. 推荐书籍表(来自 /book/recommend) CREATE TABLE IF NOT EXISTS `recommended_books` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `book_id` VARCHAR(64) NOT NULL, - `title` VARCHAR(255) DEFAULT NULL, - `author` VARCHAR(255) DEFAULT NULL, - `cover` VARCHAR(512) DEFAULT NULL, - `intro` TEXT DEFAULT NULL, - `category` VARCHAR(128) DEFAULT NULL, - `reason` VARCHAR(512) DEFAULT NULL COMMENT '推荐理由', - `reading_count` INT UNSIGNED DEFAULT 0 COMMENT '在读人数', - `new_rating` DECIMAL(5,1) DEFAULT 0 COMMENT '评分 (百分制)', - `new_rating_count` INT UNSIGNED DEFAULT 0 COMMENT '评分人数', - `rating_title` VARCHAR(32) DEFAULT NULL COMMENT '评分标签 (神作/力荐等)', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `id` BIGINT NOT NULL AUTO_INCREMENT, + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `title` VARCHAR(255) DEFAULT NULL COMMENT '书名', + `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', + `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL', + `intro` TEXT DEFAULT NULL COMMENT '书籍简介', + `category` VARCHAR(128) DEFAULT NULL COMMENT '分类', + `reason` VARCHAR(512) DEFAULT NULL COMMENT '推荐理由', + `reading_count` INT UNSIGNED DEFAULT 0 COMMENT '在读人数', + `new_rating` DECIMAL(5,1) DEFAULT 0 COMMENT '评分 (百分制)', + `new_rating_count` INT UNSIGNED DEFAULT 0 COMMENT '评分人数', + `rating_title` VARCHAR(32) DEFAULT NULL COMMENT '评分标签 (神作/力荐等)', + `deep_link` VARCHAR(512) DEFAULT NULL COMMENT '跳转链接', + `search_idx` INT UNSIGNED DEFAULT 0 COMMENT '结果序号 (用于翻页)', + `price` INT UNSIGNED DEFAULT NULL COMMENT '价格 (分)', + `pay_type` INT UNSIGNED DEFAULT NULL COMMENT '付费类型', + `book_type` TINYINT DEFAULT 0 COMMENT '书籍类型 (0=电子书)', + `batch_id` VARCHAR(32) DEFAULT NULL COMMENT '推荐批次 ID (同一次推荐请求的标识)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', PRIMARY KEY (`id`), INDEX `idx_recommended_book_id` (`book_id`), - INDEX `idx_recommended_fetched` (`fetched_at` DESC) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='推荐书籍'; + INDEX `idx_recommended_batch` (`batch_id`), + INDEX `idx_recommended_rating` (`new_rating` DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='推荐书籍'; + +-- 28. 相似书推荐表(来自 /book/similar) +CREATE TABLE IF NOT EXISTS `similar_books` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `source_book_id` VARCHAR(64) NOT NULL COMMENT '源书籍 ID', + `book_id` VARCHAR(64) NOT NULL COMMENT '推荐书籍 ID', + `title` VARCHAR(255) DEFAULT NULL COMMENT '书名', + `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', + `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL', + `deep_link` VARCHAR(512) DEFAULT NULL COMMENT '跳转链接', + `idx` INT UNSIGNED DEFAULT NULL COMMENT '结果序号 (用于翻页)', + `session_id` VARCHAR(64) DEFAULT NULL COMMENT '翻页会话 ID', + `book_json` TEXT DEFAULT NULL COMMENT '书籍完整信息 (JSON)', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_similar_source` (`source_book_id`), + INDEX `idx_similar_book_id` (`book_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='相似书推荐'; + +-- 29. 搜索会话表(来自 /store/search 的元数据) +CREATE TABLE IF NOT EXISTS `search_sessions` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `sid` VARCHAR(64) DEFAULT NULL COMMENT '搜索会话 ID', + `keyword` VARCHAR(255) NOT NULL COMMENT '搜索关键词', + `scope` INT UNSIGNED DEFAULT NULL COMMENT '搜索类型 (0=全部, 10=电子书, 14=听书等)', + `has_more` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否有更多 (1=有)', + `max_idx` INT UNSIGNED DEFAULT 0 COMMENT '当前翻页偏移', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_search_sessions_keyword` (`keyword`), + INDEX `idx_search_sessions_sid` (`sid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='搜索会话'; + +-- 30. 搜索结果表(来自 /store/search 的 results[].books[]) +CREATE TABLE IF NOT EXISTS `search_results` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `session_id` BIGINT DEFAULT NULL COMMENT '关联搜索会话 ID', + `keyword` VARCHAR(255) NOT NULL COMMENT '搜索关键词', + `book_id` VARCHAR(64) NOT NULL COMMENT '书籍 ID', + `title` VARCHAR(255) DEFAULT NULL COMMENT '书名', + `author` VARCHAR(255) DEFAULT NULL COMMENT '作者', + `cover` VARCHAR(512) DEFAULT NULL COMMENT '封面图 URL', + `intro` TEXT DEFAULT NULL COMMENT '书籍简介', + `publisher` VARCHAR(255) DEFAULT NULL COMMENT '出版社', + `category` VARCHAR(128) DEFAULT NULL COMMENT '分类', + `reading_count` INT UNSIGNED DEFAULT 0 COMMENT '在读人数', + `new_rating` DECIMAL(5,1) DEFAULT 0 COMMENT '评分 (百分制)', + `new_rating_count` INT UNSIGNED DEFAULT 0 COMMENT '评分人数', + `rating_title` VARCHAR(32) DEFAULT NULL COMMENT '评分标签', + `deep_link` VARCHAR(512) DEFAULT NULL COMMENT '跳转链接', + `pay_type` INT UNSIGNED DEFAULT NULL COMMENT '付费类型', + `price` INT UNSIGNED DEFAULT NULL COMMENT '价格 (分)', + `soldout` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否下架 (1=下架)', + `search_idx` INT UNSIGNED DEFAULT 0 COMMENT '搜索序号 (用于翻页)', + `result_scope` INT UNSIGNED DEFAULT NULL COMMENT '回包分组类型', + `result_title` VARCHAR(64) DEFAULT NULL COMMENT '回包分组标题 (如 "电子书")', + `scope_count` INT UNSIGNED DEFAULT NULL COMMENT '该分组总结果数', + `current_count` INT UNSIGNED DEFAULT NULL COMMENT '本次返回数量', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`id`), + INDEX `idx_search_results_keyword` (`keyword`), + INDEX `idx_search_results_book_id` (`book_id`), + INDEX `idx_search_results_session` (`session_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='搜索结果'; -- ============================================================ --- 18. 搜索结果(按需获取,来自 /store/search) +-- ===== 第六组:系统表 ===== -- ============================================================ -CREATE TABLE IF NOT EXISTS `search_results` ( - `id` BIGINT NOT NULL AUTO_INCREMENT, - `keyword` VARCHAR(255) NOT NULL COMMENT '搜索关键词', - `book_id` VARCHAR(64) NOT NULL, - `title` VARCHAR(255) DEFAULT NULL, - `author` VARCHAR(255) DEFAULT NULL, - `cover` VARCHAR(512) DEFAULT NULL, - `intro` TEXT DEFAULT NULL, - `publisher` VARCHAR(255) DEFAULT NULL, - `category` VARCHAR(128) DEFAULT NULL, - `reading_count` INT UNSIGNED DEFAULT 0, - `new_rating` DECIMAL(5,1) DEFAULT 0, - `new_rating_count` INT UNSIGNED DEFAULT 0, - `rating_title` VARCHAR(32) DEFAULT NULL, - `search_idx` INT UNSIGNED DEFAULT 0 COMMENT '搜索序号', - `fetched_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - INDEX `idx_search_keyword` (`keyword`), - INDEX `idx_search_book_id` (`book_id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci - COMMENT='搜索结果'; + +-- 31. 数据采集元信息表 +CREATE TABLE IF NOT EXISTS `fetch_meta` ( + `key` VARCHAR(64) NOT NULL COMMENT '元信息键名', + `value` TEXT DEFAULT NULL COMMENT '元信息值', + `created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', + `updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间', + PRIMARY KEY (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='数据采集元信息'; From 4c1a66ea6a1d8fbbd260a3493af840b95a8bbcc6 Mon Sep 17 00:00:00 2001 From: kinghwj Date: Fri, 17 Jul 2026 10:40:33 +0800 Subject: [PATCH 2/2] docs: update technical and metadata docs for v2.0 (31 tables) --- docs/METADATA_DOC.md | 878 +++++++++++++++++++++++++++++------------- docs/TECHNICAL_DOC.md | 31 +- 2 files changed, 638 insertions(+), 271 deletions(-) diff --git a/docs/METADATA_DOC.md b/docs/METADATA_DOC.md index 8906487..93f2b8d 100644 --- a/docs/METADATA_DOC.md +++ b/docs/METADATA_DOC.md @@ -1,6 +1,6 @@ # 微信读书数据展示网站 - 元数据文档 -> 版本:1.2.0 | 更新时间:2026-07-17 | 数据库:MySQL weread_dashboard +> 版本:2.0.0 | 更新时间:2026-07-17 | 数据库:MySQL weread_dashboard | 数据模型版本:v2.0 --- @@ -12,8 +12,15 @@ | 字符集 | utf8mb4 | | 排序规则 | utf8mb4_unicode_ci | | 存储引擎 | InnoDB | -| 表数量 | 18 | -| 数据总量 | 862 行 | +| 表数量 | 31 | +| 数据总量 | 685 行 | + +### 设计规范 + +1. **双列时间戳**:所有 `INT UNSIGNED` 时间戳字段均伴随 `_dt TIMESTAMP` 列,便于直接查询展示 +2. **审计列**:每张表统一维护 `created_at`(创建时间)和 `updated_at`(更新时间),自动维护 +3. **中文注释**:每个字段均带 `COMMENT` 中文注释 +4. **索引设计**:主键、唯一约束、复合索引按实际查询模式设计 --- @@ -21,108 +28,193 @@ | 分类 | 表名 | 说明 | 数据来源 | |------|------|------|----------| -| **基础数据** | books | 书架书籍 | 采集脚本 | -| **基础数据** | reading_stats | 阅读统计概要 | 采集脚本 | -| **基础数据** | read_times | 阅读时长时间序列 | 采集脚本 | -| **基础数据** | read_longest | 阅读时长排行 | 采集脚本 | -| **基础数据** | prefer_categories | 偏好分类 | 采集脚本 | -| **基础数据** | prefer_authors | 偏好作者 | 采集脚本 | -| **基础数据** | prefer_time | 24h 时段分布 | 采集脚本 | -| **基础数据** | notebooks | 笔记本概览 | 采集脚本 | -| **基础数据** | read_stat_items | 统计摘要项 | 采集脚本 | -| **基础数据** | archives | 书单归档 | 采集脚本 | -| **基础数据** | fetch_meta | 采集元信息 | 采集脚本 | -| **按需数据** | book_details | 书籍详情 | 用户点击时获取 | -| **按需数据** | book_progress | 阅读进度 | 用户点击时获取 | -| **按需数据** | book_highlights | 划线内容 | 用户点击时获取 | -| **按需数据** | book_chapters | 章节信息 | 用户点击时获取 | -| **按需数据** | book_reviews | 想法/点评 | 用户点击时获取 | -| **按需数据** | recommended_books | 推荐书籍 | 用户浏览推荐时获取 | -| **按需数据** | search_results | 搜索结果 | 用户搜索时获取 | +| **书架数据** | books | 书架电子书 | 采集脚本 | +| **书架数据** | shelf_albums | 书架有声书/专辑 | 采集脚本 | +| **书架数据** | shelf_meta | 书架元信息 | 采集脚本 | +| **书架数据** | archives | 书单/归档 | 采集脚本 | +| **阅读统计** | reading_stats | 阅读统计概要 | 采集脚本 | +| **阅读统计** | read_times | 阅读时长时间序列 | 采集脚本 | +| **阅读统计** | read_longest | 阅读时长排行 | 采集脚本 | +| **阅读统计** | read_stat_items | 统计摘要项 | 采集脚本 | +| **阅读统计** | prefer_categories | 偏好分类 | 采集脚本 | +| **阅读统计** | prefer_authors | 偏好作者 | 采集脚本 | +| **阅读统计** | prefer_publishers | 偏好出版社 | 采集脚本 | +| **阅读统计** | prefer_cps | 偏好版权方 | 采集脚本 | +| **阅读统计** | prefer_books | 偏好书籍卡片 | 采集脚本 | +| **阅读统计** | prefer_time | 24h 时段分布 | 采集脚本 | +| **阅读统计** | read_rank | 好友阅读排行 | 采集脚本 | +| **阅读统计** | medals | 勋章 | 采集脚本 | +| **阅读统计** | year_reports | 年度报告 | 采集脚本 | +| **笔记数据** | notebooks | 笔记本概览 | 采集脚本 | +| **笔记数据** | book_highlights | 划线内容 | 用户点击时获取 | +| **笔记数据** | book_chapters | 章节信息 | 用户点击时获取 | +| **笔记数据** | book_reviews | 个人想法/点评 | 用户点击时获取 | +| **笔记数据** | book_best_bookmarks | 热门划线 | 用户点击时获取 | +| **笔记数据** | book_underlines | 划线热度统计 | 用户点击时获取 | +| **笔记数据** | public_reviews | 公开点评 | 用户点击时获取 | +| **书籍详情** | book_details | 书籍详细信息 | 用户点击时获取 | +| **书籍详情** | book_progress | 阅读进度 | 用户点击时获取 | +| **发现/推荐** | recommended_books | 推荐书籍 | 用户浏览推荐时获取 | +| **发现/推荐** | similar_books | 相似书推荐 | 用户点击时获取 | +| **搜索** | search_sessions | 搜索会话 | 用户搜索时获取 | +| **搜索** | search_results | 搜索结果 | 用户搜索时获取 | +| **系统** | fetch_meta | 数据采集元信息 | 采集脚本 | --- ## 3. 表结构详情 -### 3.1 books - 书架书籍表 +### 3.1 books — 书架电子书 -> 存储用户书架中的所有书籍基本信息 +> 来源:`/shelf/sync` 的 `books[]` | 记录数:404 -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | book_id | VARCHAR(64) | 是 (PK) | - | 书籍唯一标识 | | title | VARCHAR(255) | 是 | '' | 书名 | | author | VARCHAR(255) | 否 | NULL | 作者 | | cover | VARCHAR(512) | 否 | NULL | 封面图 URL | -| category | VARCHAR(128) | 否 | NULL | 分类(如"文学-古代诗词") | +| category | VARCHAR(128) | 否 | NULL | 分类(如 "文学-古代诗词") | | finish_reading | TINYINT(1) | 是 | 0 | 是否读完 (1=读完) | | read_update_time | INT UNSIGNED | 否 | NULL | 最近阅读时间 (Unix 时间戳) | +| read_update_time_dt | TIMESTAMP | 否 | NULL | 最近阅读时间 (可读格式) | | secret | TINYINT(1) | 是 | 0 | 是否私密 (1=私密) | | is_top | TINYINT(1) | 是 | 0 | 是否置顶 (1=置顶) | | update_time | INT UNSIGNED | 否 | NULL | 书籍更新时间 (Unix 时间戳) | -| deep_link | VARCHAR(512) | 否 | NULL | 微信读书深度链接 | +| update_time_dt | TIMESTAMP | 否 | NULL | 书籍更新时间 (可读格式) | +| deep_link | VARCHAR(512) | 否 | NULL | 微信读书 App 深度链接 | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (book_id) -- idx_books_category (category) -- idx_books_finish_reading (finish_reading) -- idx_books_read_update_time (read_update_time DESC) - -**记录数**:404 +**索引**:PK(book_id), idx_books_category(category), idx_books_finish_reading(finish_reading), idx_books_secret(secret), idx_books_is_top(is_top), idx_books_read_update_time(read_update_time DESC), idx_books_title(title) --- -### 3.2 reading_stats - 阅读统计概要表 +### 3.2 shelf_albums — 书架有声书/专辑 -> 存储总体和年度的阅读统计数据 +> 来源:`/shelf/sync` 的 `albums[]` | 记录数:0 -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| album_id | VARCHAR(64) | 是 (PK) | - | 专辑唯一标识 | +| name | VARCHAR(255) | 否 | NULL | 专辑名称 | +| author_name | VARCHAR(255) | 否 | NULL | 演播/作者 | +| cover | VARCHAR(512) | 否 | NULL | 封面图 URL | +| track_count | INT UNSIGNED | 否 | 0 | 音频集数 | +| finish_status | VARCHAR(32) | 否 | NULL | 完结状态文案(如 "已完结") | +| finish | TINYINT(1) | 是 | 0 | 是否完结 (1=完结) | +| pay_type | INT UNSIGNED | 否 | NULL | 付费类型 | +| intro | TEXT | 否 | NULL | 专辑简介 | +| update_time | INT UNSIGNED | 否 | NULL | 更新时间 (Unix 时间戳) | +| update_time_dt | TIMESTAMP | 否 | NULL | 更新时间 (可读格式) | +| secret | TINYINT(1) | 是 | 0 | 是否私密 (1=私密) | +| lecture_paid | TINYINT(1) | 是 | 0 | 是否已购买 (1=已购买) | +| lecture_read_update_time | INT UNSIGNED | 否 | NULL | 最近收听时间 (Unix 时间戳) | +| lecture_read_update_time_dt | TIMESTAMP | 否 | NULL | 最近收听时间 (可读格式) | +| is_top | TINYINT(1) | 是 | 0 | 是否置顶 (1=置顶) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(album_id), idx_albums_finish(finish), idx_albums_secret(secret), idx_albums_lecture_read_time(lecture_read_update_time DESC) + +--- + +### 3.3 shelf_meta — 书架元信息 + +> 来源:`/shelf/sync` 的 `mp` 和 `bookCount` | 记录数:1 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | id | BIGINT | 是 (PK) | AUTO | 自增主键 | -| mode | VARCHAR(20) | 是 | - | 统计维度: overall/annually | -| period | VARCHAR(20) | 是 | '' | 周期标识: "overall" 或 "2026" | +| mp_enabled | TINYINT(1) | 是 | 0 | 是否有文章收藏入口 (1=有) | +| book_count | INT UNSIGNED | 否 | 0 | 可枚举电子书数量 | +| album_count | INT UNSIGNED | 否 | 0 | 有声书/专辑数量 | +| total_count | INT UNSIGNED | 否 | 0 | 书架总条目数 | +| raw_json | LONGTEXT | 否 | NULL | 原始 API 返回 JSON | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id) + +--- + +### 3.4 archives — 书单/归档 + +> 来源:`/shelf/sync` 的 `archive[]` | 记录数:6 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| name | VARCHAR(255) | 否 | NULL | 书单名称 | +| book_ids | LONGTEXT | 否 | NULL | bookId 列表 (JSON 数组) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), idx_archives_name(name) + +--- + +### 3.5 reading_stats — 阅读统计概要 + +> 来源:`/readdata/detail` | 记录数:2 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| mode | VARCHAR(20) | 是 | - | 统计维度: overall/annually/monthly/weekly | +| period | VARCHAR(20) | 是 | '' | 周期标识 (如 "overall" 或 "2026") | +| base_time | INT UNSIGNED | 否 | NULL | 统计周期基准时间戳 | +| base_time_dt | TIMESTAMP | 否 | NULL | 统计周期基准时间 (可读格式) | | total_read_time | INT UNSIGNED | 是 | 0 | 总阅读时长 (秒) | | read_days | INT UNSIGNED | 是 | 0 | 有效阅读天数 | | day_average_read_time | DOUBLE | 否 | 0 | 日均阅读时长 (秒) | +| compare | DOUBLE | 否 | NULL | 与上周期对比比例 (正=增长) | | regist_time | INT UNSIGNED | 否 | NULL | 用户注册时间 (Unix 时间戳) | +| regist_time_dt | TIMESTAMP | 否 | NULL | 用户注册时间 (可读格式) | +| record_reading_time | INT UNSIGNED | 否 | 0 | 总朗读/记录类阅读时长 (秒) | +| read_rate | DOUBLE | 否 | NULL | 文字阅读占比百分比 | +| wr_read_time | INT UNSIGNED | 否 | NULL | 文字阅读时长 (秒) | +| wr_listen_time | INT UNSIGNED | 否 | NULL | 听书时长 (秒) | +| author_count | INT UNSIGNED | 否 | NULL | 符合统计条件的作者总数 | +| prefer_category_word | VARCHAR(128) | 否 | NULL | 偏好分类文案 (如 "偏好阅读文学") | +| prefer_time_word | VARCHAR(128) | 否 | NULL | 偏好时段文案 (如 "偏好上午阅读") | +| read_records_word | VARCHAR(64) | 否 | NULL | 书籍分布模块标题文案 | +| read_distribution_word | VARCHAR(64) | 否 | NULL | 点评分布模块标题文案 | +| read_time_gears | VARCHAR(128) | 否 | NULL | 阅读时长档位 (JSON 数组) | +| style_type | VARCHAR(32) | 否 | NULL | 样式类型 (如 normal) | | raw_json | LONGTEXT | 否 | NULL | 原始 API 返回 JSON | | created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (id) -- idx_reading_stats_mode (mode, period) - -**记录数**:2 +**索引**:PK(id), uk_reading_stats_mode_period(mode, period), idx_reading_stats_mode(mode) --- -### 3.3 read_times - 阅读时长时间序列表 +### 3.6 read_times — 阅读时长时间序列 -> 按年/月/日分桶的阅读时长数据 +> 来源:`/readdata/detail` 的 `readTimes` 和 `dailyReadTimes` | 记录数:21 -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | id | BIGINT | 是 (PK) | AUTO | 自增主键 | -| mode | VARCHAR(30) | 是 | - | 统计维度 (含 _daily 后缀) | +| mode | VARCHAR(30) | 是 | - | 统计维度 (含 _daily 后缀表示每日数据) | | period | VARCHAR(20) | 是 | '' | 周期标识 | | bucket_timestamp | INT UNSIGNED | 是 | - | 分桶起始时间戳 | +| bucket_timestamp_dt | TIMESTAMP | 否 | NULL | 分桶起始时间 (可读格式) | | duration | INT UNSIGNED | 是 | 0 | 阅读时长 (秒) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (id) -- idx_read_times_mode_period (mode, period) -- idx_read_times_bucket (bucket_timestamp) - -**记录数**:21 +**索引**:PK(id), idx_read_times_mode_period(mode, period), idx_read_times_bucket(bucket_timestamp) --- -### 3.4 read_longest - 阅读时长排行表 +### 3.7 read_longest — 阅读时长排行 -> 阅读时间最久的书籍排名 +> 来源:`/readdata/detail` 的 `readLongest[]` | 记录数:20 -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | id | BIGINT | 是 (PK) | AUTO | 自增主键 | | mode | VARCHAR(20) | 是 | - | 统计维度 | | period | VARCHAR(20) | 是 | '' | 周期标识 | @@ -131,88 +223,220 @@ | author | VARCHAR(255) | 否 | NULL | 作者 | | cover | VARCHAR(512) | 否 | NULL | 封面图 URL | | read_time | INT UNSIGNED | 是 | 0 | 阅读时长 (秒) | -| tags | VARCHAR(512) | 否 | NULL | 标签 (JSON 数组) | +| record_reading_time | INT UNSIGNED | 否 | NULL | 朗读/记录类阅读时长 (秒) | +| tags | VARCHAR(512) | 否 | NULL | 标签 (JSON 数组,如 ["笔记最多"]) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (id) -- idx_read_longest_mode (mode, period) - -**记录数**:20 +**索引**:PK(id), idx_read_longest_mode_period(mode, period), idx_read_longest_book_id(book_id), idx_read_longest_read_time(read_time DESC) --- -### 3.5 prefer_categories - 偏好分类表 +### 3.8 read_stat_items — 阅读统计摘要项 -> 用户阅读的书籍分类统计 +> 来源:`/readdata/detail` 的 `readStat[]` | 记录数:8 -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| mode | VARCHAR(20) | 是 | - | 统计维度 | +| period | VARCHAR(20) | 是 | '' | 周期标识 | +| stat_name | VARCHAR(64) | 否 | NULL | 统计项名称 (读过/读完/阅读/笔记) | +| counts | VARCHAR(64) | 否 | NULL | 统计值文案 (如 "404本") | +| scheme | VARCHAR(512) | 否 | NULL | App 跳转链接 | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), idx_read_stat_items_mode_period(mode, period) + +--- + +### 3.9 prefer_categories — 偏好分类 + +> 来源:`/readdata/detail` 的 `preferCategory[]` | 记录数:16 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | id | BIGINT | 是 (PK) | AUTO | 自增主键 | | mode | VARCHAR(20) | 是 | - | 统计维度 | | period | VARCHAR(20) | 是 | '' | 周期标识 | | category_id | INT UNSIGNED | 否 | NULL | 分类 ID | -| category_title | VARCHAR(128) | 否 | NULL | 分类名称 | -| parent_category_title | VARCHAR(128) | 否 | NULL | 父分类名称 | -| val | DOUBLE | 否 | 0 | 偏好权重 | +| category_title | VARCHAR(128) | 否 | NULL | 分类名称 (如 "文学") | +| parent_category_id | INT UNSIGNED | 否 | NULL | 父分类 ID | +| parent_category_title | VARCHAR(128) | 否 | NULL | 父分类名称 (如 "精品小说") | +| category_type | TINYINT | 否 | 0 | 分类类型标记 (0=普通, 1/2=特殊) | +| val | DOUBLE | 否 | 0 | 偏好权重 (归一化相对值) | | reading_time | INT UNSIGNED | 否 | 0 | 该分类阅读时长 (秒) | | reading_count | INT UNSIGNED | 否 | 0 | 该分类阅读本数 | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (id) -- idx_prefer_cat_mode (mode, period) - -**记录数**:16 +**索引**:PK(id), idx_prefer_cat_mode_period(mode, period), idx_prefer_cat_category_id(category_id) --- -### 3.6 prefer_authors - 偏好作者表 +### 3.10 prefer_authors — 偏好作者 -> 用户阅读最多的作者统计 +> 来源:`/readdata/detail` 的 `preferAuthor[]` | 记录数:20 -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | id | BIGINT | 是 (PK) | AUTO | 自增主键 | | mode | VARCHAR(20) | 是 | - | 统计维度 | | period | VARCHAR(20) | 是 | '' | 周期标识 | | author_id | INT UNSIGNED | 否 | NULL | 作者 ID | -| name | VARCHAR(255) | 否 | NULL | 作者名称 | -| count | INT UNSIGNED | 否 | 0 | 阅读书本数 | -| read_time | VARCHAR(64) | 否 | NULL | 阅读时长 (格式化字符串) | +| name | VARCHAR(255) | 否 | NULL | 作者名 | +| count | INT UNSIGNED | 否 | 0 | 阅读该作者的书本数 | +| read_time | VARCHAR(64) | 否 | NULL | 阅读时长 (格式化字符串,如 "5小时30分钟") | +| user_json | TEXT | 否 | NULL | 作者关联用户信息 (JSON) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (id) -- idx_prefer_author_mode (mode, period) - -**记录数**:20 +**索引**:PK(id), idx_prefer_author_mode_period(mode, period), idx_prefer_author_name(name) --- -### 3.7 prefer_time - 24小时时段分布表 +### 3.11 prefer_publishers — 偏好出版社 -> 一天中每个小时的阅读时长分布 +> 来源:`/readdata/detail` 的 `preferPublisher[]` | 记录数:20 -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| mode | VARCHAR(20) | 是 | - | 统计维度 | +| period | VARCHAR(20) | 是 | '' | 周期标识 | +| name | VARCHAR(255) | 否 | NULL | 出版社名 | +| count | INT UNSIGNED | 否 | 0 | 阅读该出版社书籍本数 | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), idx_prefer_pub_mode_period(mode, period) + +--- + +### 3.12 prefer_cps — 偏好版权方 + +> 来源:`/readdata/detail` 的 `preferCp[]` | 记录数:20 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| mode | VARCHAR(20) | 是 | - | 统计维度 | +| period | VARCHAR(20) | 是 | '' | 周期标识 | +| count | INT UNSIGNED | 否 | 0 | 阅读该版权方书籍本数 | +| cp_name | VARCHAR(255) | 否 | NULL | 版权方名称 | +| cp_user_vid | VARCHAR(64) | 否 | NULL | 版权方用户 VID | +| cp_avatar | VARCHAR(512) | 否 | NULL | 版权方头像 URL | +| cp_role | VARCHAR(64) | 否 | NULL | 版权方角色 | +| copyright_json | TEXT | 否 | NULL | 版权方完整信息 (JSON) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), idx_prefer_cp_mode_period(mode, period) + +--- + +### 3.13 prefer_books — 偏好书籍卡片 + +> 来源:`/readdata/detail` 的 `preferBooks[]` | 记录数:24 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| mode | VARCHAR(20) | 是 | - | 统计维度 | +| period | VARCHAR(20) | 是 | '' | 周期标识 | +| book_id | VARCHAR(64) | 否 | NULL | 书籍 ID | +| title | VARCHAR(255) | 否 | NULL | 书名 | +| author | VARCHAR(255) | 否 | NULL | 作者 | +| cover | VARCHAR(512) | 否 | NULL | 封面图 URL | +| reason | TEXT | 否 | NULL | 推荐理由 | +| prefer_type | VARCHAR(64) | 否 | NULL | 偏好类型 | +| book_json | TEXT | 否 | NULL | 书籍完整信息 (JSON) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), idx_prefer_books_mode_period(mode, period), idx_prefer_books_book_id(book_id) + +--- + +### 3.14 prefer_time — 24小时阅读时段分布 + +> 来源:`/readdata/detail` 的 `preferTime[]` | 记录数:24 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | id | BIGINT | 是 (PK) | AUTO | 自增主键 | | mode | VARCHAR(20) | 是 | - | 统计维度 | | period | VARCHAR(20) | 是 | '' | 周期标识 | | hour_index | TINYINT UNSIGNED | 是 | - | 小时 (0-23) | | duration | INT UNSIGNED | 是 | 0 | 阅读时长 (秒) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (id) -- idx_prefer_time_mode (mode, period) - -**记录数**:24 +**索引**:PK(id), idx_prefer_time_mode_period(mode, period) --- -### 3.8 notebooks - 笔记本概览表 +### 3.15 read_rank — 好友阅读排行 -> 有笔记的书籍及其笔记统计 +> 来源:`/readdata/detail` 的 `rank` | 记录数:0 -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| mode | VARCHAR(20) | 是 | - | 统计维度 | +| period | VARCHAR(20) | 是 | '' | 周期标识 | +| rank_text | VARCHAR(255) | 否 | NULL | 排行文案 (如 "朋友中排第3名") | +| scheme | VARCHAR(512) | 否 | NULL | 排行跳转链接 | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), idx_read_rank_mode_period(mode, period) + +--- + +### 3.16 medals — 勋章 + +> 来源:`/readdata/detail` 的 `medals[]` | 记录数:30 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| mode | VARCHAR(20) | 是 | - | 统计维度 | +| period | VARCHAR(20) | 是 | '' | 周期标识 | +| medal_json | TEXT | 否 | NULL | 勋章信息 (JSON) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), idx_medals_mode_period(mode, period) + +--- + +### 3.17 year_reports — 年度报告 + +> 来源:`/readdata/detail` 的 `yearReport[]` | 记录数:0 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| mode | VARCHAR(20) | 是 | - | 统计维度 | +| period | VARCHAR(20) | 是 | '' | 周期标识 | +| report_year | VARCHAR(10) | 否 | NULL | 报告年份 | +| report_json | TEXT | 否 | NULL | 报告入口信息 (JSON) | +| monthly_times | VARCHAR(512) | 否 | NULL | 12个月阅读时长 (JSON 数组,单位秒) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), idx_year_reports_mode_period(mode, period), idx_year_reports_year(report_year) + +--- + +### 3.18 notebooks — 笔记本概览 + +> 来源:`/user/notebooks` | 记录数:32 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | book_id | VARCHAR(64) | 是 (PK) | - | 书籍 ID | | title | VARCHAR(255) | 否 | NULL | 书名 | | author | VARCHAR(255) | 否 | NULL | 作者 | @@ -220,83 +444,179 @@ | review_count | INT UNSIGNED | 是 | 0 | 想法/点评数 | | note_count | INT UNSIGNED | 是 | 0 | 划线数 | | bookmark_count | INT UNSIGNED | 是 | 0 | 书签数 | +| total_note_count | INT UNSIGNED | 是 | 0 | 笔记总数 (review+note+bookmark) | | reading_progress | INT UNSIGNED | 是 | 0 | 阅读进度 (0-100) | | marked_status | TINYINT(1) | 是 | 0 | 标记状态 (1=读完) | -| sort | BIGINT | 否 | NULL | 排序值 | +| sort | BIGINT | 否 | NULL | 排序值 (最近笔记时间) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (book_id) - -**记录数**:32 +**索引**:PK(book_id), idx_notebooks_marked_status(marked_status), idx_notebooks_sort(sort DESC) --- -### 3.9 read_stat_items - 统计摘要项表 +### 3.19 book_highlights — 划线内容 -> App 首页展示的阅读统计摘要 +> 来源:`/book/bookmarklist` 的 `updated[]` | 记录数:0(按需获取) -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | id | BIGINT | 是 (PK) | AUTO | 自增主键 | -| mode | VARCHAR(20) | 是 | - | 统计维度 | -| period | VARCHAR(20) | 是 | '' | 周期标识 | -| stat_name | VARCHAR(64) | 否 | NULL | 统计项名称 (读过/读完/阅读/笔记) | -| counts | VARCHAR(64) | 否 | NULL | 统计值文案 (如"404本") | -| scheme | VARCHAR(512) | 否 | NULL | App 跳转链接 | +| book_id | VARCHAR(64) | 是 | - | 书籍 ID | +| bookmark_id | VARCHAR(64) | 否 | NULL | 划线唯一 ID | +| chapter_uid | INT UNSIGNED | 否 | NULL | 所在章节 UID | +| mark_text | LONGTEXT | 否 | NULL | 划线原文内容 | +| create_time | INT UNSIGNED | 否 | NULL | 创建时间 (Unix 时间戳) | +| create_time_dt | TIMESTAMP | 否 | NULL | 创建时间 (可读格式) | +| type | TINYINT | 否 | NULL | 类型 (1=划线) | +| range_text | VARCHAR(64) | 否 | NULL | 位置范围 (如 "2959-3007") | +| color_style | TINYINT | 否 | NULL | 划线颜色样式 | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (id) -- idx_read_stat_items_mode (mode, period) - -**记录数**:8 +**索引**:PK(id), uk_highlights_bookmark(book_id, bookmark_id), idx_highlights_book_id(book_id), idx_highlights_chapter(book_id, chapter_uid), idx_highlights_create_time(create_time DESC) --- -### 3.10 archives - 书单归档表 +### 3.20 book_chapters — 章节信息 -> 用户创建的书单 +> 来源:`/book/chapterinfo` 和 `/book/bookmarklist` 的 `chapters` | 记录数:0(按需获取) -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | id | BIGINT | 是 (PK) | AUTO | 自增主键 | -| name | VARCHAR(255) | 否 | NULL | 书单名称 | -| book_ids | LONGTEXT | 否 | NULL | 书籍 ID 列表 (JSON 数组) | +| book_id | VARCHAR(64) | 是 | - | 书籍 ID | +| chapter_uid | INT UNSIGNED | 是 | - | 章节 UID | +| chapter_idx | INT UNSIGNED | 否 | NULL | 章节序号 | +| title | VARCHAR(255) | 否 | NULL | 章节标题 | +| word_count | INT UNSIGNED | 否 | NULL | 章节字数 | +| level | TINYINT | 否 | NULL | 目录层级 (1=一级, 2=二级) | +| update_time | INT UNSIGNED | 否 | NULL | 章节更新时间 (Unix 时间戳) | +| update_time_dt | TIMESTAMP | 否 | NULL | 章节更新时间 (可读格式) | +| price | INT UNSIGNED | 否 | 0 | 章节价格 (0=免费) | +| paid | TINYINT(1) | 是 | 0 | 是否已购买 (1=已购买) | +| is_mp_chapter | TINYINT(1) | 是 | 0 | 是否公众号章节 (1=是) | +| anchors | TEXT | 否 | NULL | 章节内锚点/子标题 (JSON 数组) | +| synckey | INT UNSIGNED | 否 | NULL | 同步 key (版本号) | +| chapter_update_time | INT UNSIGNED | 否 | NULL | 章节列表最后更新时间 (Unix 时间戳) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (id) - -**记录数**:6 +**索引**:PK(id), uk_book_chapter(book_id, chapter_uid), idx_chapters_book_idx(book_id, chapter_idx) --- -### 3.11 fetch_meta - 采集元信息表 +### 3.21 book_reviews — 个人想法/点评 -> 记录数据采集的元信息 +> 来源:`/review/list/mine` | 记录数:0(按需获取) -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| -| key | VARCHAR(64) | 是 (PK) | - | 元信息键名 | -| value | TEXT | 否 | NULL | 元信息值 | +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| book_id | VARCHAR(64) | 是 | - | 书籍 ID | +| review_id | VARCHAR(64) | 否 | NULL | 想法唯一 ID | +| content | LONGTEXT | 否 | NULL | 想法内容文本 | +| abstract | LONGTEXT | 否 | NULL | 想法对应的划线原文 (仅划线想法有值) | +| range_text | VARCHAR(64) | 否 | NULL | 划线原文位置范围 (如 "2959-3007") | +| chapter_uid | INT UNSIGNED | 否 | NULL | 章节 UID | +| chapter_idx | INT UNSIGNED | 否 | NULL | 章节序号 | +| chapter_name | VARCHAR(255) | 否 | NULL | 所在章节名 (章节点评时有值) | +| create_time | INT UNSIGNED | 否 | NULL | 创建时间 (Unix 时间戳) | +| create_time_dt | TIMESTAMP | 否 | NULL | 创建时间 (可读格式) | +| star | TINYINT | 否 | -1 | 评分 (0-5, -1=无评分) | +| is_finish | TINYINT(1) | 否 | 0 | 是否书评 (1=书评) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (key) - -**记录数**:1 +**索引**:PK(id), uk_reviews_review_id(review_id), idx_reviews_book_id(book_id), idx_reviews_chapter(book_id, chapter_uid), idx_reviews_create_time(create_time DESC) --- -### 3.12 book_details - 书籍详细信息表 +### 3.22 book_best_bookmarks — 热门划线 -> 来自微信读书 /book/info 接口,用户点击书籍时按需获取 +> 来源:`/book/bestbookmarks` | 记录数:0(按需获取) -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| book_id | VARCHAR(64) | 是 | - | 书籍 ID | +| bookmark_id | VARCHAR(64) | 否 | NULL | 划线唯一 ID | +| chapter_uid | INT UNSIGNED | 否 | NULL | 所在章节 UID | +| range_text | VARCHAR(64) | 否 | NULL | 划线位置范围 (如 "393-401") | +| mark_text | LONGTEXT | 否 | NULL | 划线原文文本 | +| total_count | INT UNSIGNED | 否 | 0 | 划线人数 | +| user_vid | VARCHAR(64) | 否 | NULL | 代表用户 VID | +| simplified_range | VARCHAR(64) | 否 | NULL | 简体书籍的 range (繁简体书专属) | +| traditional_range | VARCHAR(64) | 否 | NULL | 繁体书籍的 range (繁简体书专属) | +| synckey | INT UNSIGNED | 否 | NULL | 同步 key (数据版本号) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), uk_best_bm_book_bookmark(book_id, bookmark_id), idx_best_bm_book_id(book_id), idx_best_bm_total_count(total_count DESC) + +--- + +### 3.23 book_underlines — 划线热度统计 + +> 来源:`/book/underlines` | 记录数:0(按需获取) + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| book_id | VARCHAR(64) | 是 | - | 书籍 ID | +| chapter_uid | INT UNSIGNED | 是 | - | 章节 UID | +| range_text | VARCHAR(64) | 否 | NULL | 划线位置范围 (如 "393-401") | +| count | INT UNSIGNED | 否 | 0 | 划线人数 | +| score | DOUBLE | 否 | 0 | 热度分数 | +| type | TINYINT | 否 | NULL | 划线类型 | +| synckey | INT UNSIGNED | 否 | NULL | 同步 key | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), uk_underlines_book_ch_range(book_id, chapter_uid, range_text), idx_underlines_book_chapter(book_id, chapter_uid) + +--- + +### 3.24 public_reviews — 公开点评 + +> 来源:`/review/list` | 记录数:0(按需获取) + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| book_id | VARCHAR(64) | 是 | - | 书籍 ID | +| review_id | VARCHAR(64) | 否 | NULL | 点评唯一 ID | +| content | LONGTEXT | 否 | NULL | 点评文本内容 | +| html_content | LONGTEXT | 否 | NULL | 点评 HTML 富文本内容 | +| star | TINYINT | 否 | -1 | 评分 (20=一星, 40=二星, 60=三星, 80=四星, 100=五星) | +| is_finish | TINYINT(1) | 否 | 0 | 是否读完此书 (1=读完) | +| create_time | INT UNSIGNED | 否 | NULL | 创建时间 (Unix 时间戳) | +| create_time_dt | TIMESTAMP | 否 | NULL | 创建时间 (可读格式) | +| chapter_name | VARCHAR(255) | 否 | NULL | 所在章节名 | +| author_vid | VARCHAR(64) | 否 | NULL | 评论者用户 VID | +| author_name | VARCHAR(255) | 否 | NULL | 评论者昵称 | +| author_avatar | VARCHAR(512) | 否 | NULL | 评论者头像 URL | +| review_list_type | TINYINT | 否 | 0 | 筛选类型 (0=全部, 1=推荐, 2=不行, 3=最新, 4=一般) | +| idx | INT UNSIGNED | 否 | NULL | 序号 (用于翻页) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), uk_public_reviews_review_id(review_id), idx_public_reviews_book_id(book_id), idx_public_reviews_star(star), idx_public_reviews_create_time(create_time DESC) + +--- + +### 3.25 book_details — 书籍详细信息 + +> 来源:`/book/info` | 记录数:1(按需获取) + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | book_id | VARCHAR(64) | 是 (PK) | - | 书籍 ID | | title | VARCHAR(255) | 否 | NULL | 书名 | | author | VARCHAR(255) | 否 | NULL | 作者 | | translator | VARCHAR(255) | 否 | NULL | 译者 | | cover | VARCHAR(512) | 否 | NULL | 封面图 URL | -| intro | LONGTEXT | 否 | NULL | 书籍简介 | +| intro | LONGTEXT | 否 | NULL | 简介 | | category | VARCHAR(128) | 否 | NULL | 分类 | | publisher | VARCHAR(255) | 否 | NULL | 出版社 | | publish_time | VARCHAR(32) | 否 | NULL | 出版时间 | @@ -304,114 +624,46 @@ | word_count | INT UNSIGNED | 否 | 0 | 总字数 | | new_rating | DECIMAL(5,1) | 否 | 0 | 评分 (百分制) | | new_rating_count | INT UNSIGNED | 否 | 0 | 评分人数 | -| fetched_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 获取时间 | +| new_rating_detail | TEXT | 否 | NULL | 评分分布详情 (JSON) | +| deep_link | VARCHAR(512) | 否 | NULL | 跳转链接 | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (book_id) - -**记录数**:1 +**索引**:PK(book_id), idx_book_details_isbn(isbn), idx_book_details_category(category), idx_book_details_publisher(publisher) --- -### 3.13 book_progress - 阅读进度表 +### 3.26 book_progress — 阅读进度 -> 来自微信读书 /book/getprogress 接口 +> 来源:`/book/getprogress` | 记录数:1(按需获取) -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | book_id | VARCHAR(64) | 是 (PK) | - | 书籍 ID | | progress | TINYINT UNSIGNED | 是 | 0 | 阅读进度百分比 (0-100) | | chapter_uid | INT UNSIGNED | 否 | NULL | 当前阅读章节 UID | | chapter_offset | INT UNSIGNED | 否 | NULL | 当前章节内偏移量 | | update_time | INT UNSIGNED | 否 | NULL | 最后阅读时间 (Unix 时间戳) | +| update_time_dt | TIMESTAMP | 否 | NULL | 最后阅读时间 (可读格式) | | record_reading_time | INT UNSIGNED | 否 | 0 | 累计阅读时长 (秒) | | finish_time | INT UNSIGNED | 否 | NULL | 读完时间 (Unix 时间戳) | -| is_start_reading | TINYINT(1) | 是 | 0 | 是否已开始阅读 | -| fetched_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 获取时间 | +| finish_time_dt | TIMESTAMP | 否 | NULL | 读完时间 (可读格式) | +| is_start_reading | TINYINT(1) | 是 | 0 | 是否已开始阅读 (1=是) | +| server_timestamp | INT UNSIGNED | 否 | NULL | 服务端时间戳 | +| server_timestamp_dt | TIMESTAMP | 否 | NULL | 服务端时间 (可读格式) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (book_id) - -**记录数**:1 +**索引**:PK(book_id), idx_progress_finish_time(finish_time DESC), idx_progress_update_time(update_time DESC) --- -### 3.14 book_highlights - 划线内容表 +### 3.27 recommended_books — 推荐书籍 -> 来自微信读书 /book/bookmarklist 接口 +> 来源:`/book/recommend` | 记录数:12(按需获取) -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| -| id | BIGINT | 是 (PK) | AUTO | 自增主键 | -| book_id | VARCHAR(64) | 是 | - | 书籍 ID | -| bookmark_id | VARCHAR(64) | 否 | NULL | 划线唯一 ID | -| chapter_uid | INT UNSIGNED | 否 | NULL | 所在章节 UID | -| mark_text | LONGTEXT | 否 | NULL | 划线原文内容 | -| create_time | INT UNSIGNED | 否 | NULL | 创建时间 (Unix 时间戳) | -| type | TINYINT | 否 | NULL | 划线类型 | -| range_text | VARCHAR(64) | 否 | NULL | 位置范围 | -| color_style | TINYINT | 否 | NULL | 划线颜色样式 | -| fetched_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 获取时间 | - -**索引**: -- PRIMARY KEY (id) -- idx_highlights_book_id (book_id) -- idx_highlights_chapter (book_id, chapter_uid) - -**记录数**:142 - ---- - -### 3.15 book_chapters - 章节信息表 - -> 来自微信读书 /book/bookmarklist 接口的 chapters 字段 - -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| -| id | BIGINT | 是 (PK) | AUTO | 自增主键 | -| book_id | VARCHAR(64) | 是 | - | 书籍 ID | -| chapter_uid | INT UNSIGNED | 是 | - | 章节 UID | -| chapter_idx | INT UNSIGNED | 否 | NULL | 章节序号 | -| title | VARCHAR(255) | 否 | NULL | 章节标题 | - -**索引**: -- PRIMARY KEY (id) -- UNIQUE KEY uk_book_chapter (book_id, chapter_uid) - -**记录数**:77 - ---- - -### 3.16 book_reviews - 个人想法/点评表 - -> 来自微信读书 /review/list/mine 接口 - -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| -| id | BIGINT | 是 (PK) | AUTO | 自增主键 | -| book_id | VARCHAR(64) | 是 | - | 书籍 ID | -| review_id | VARCHAR(64) | 否 | NULL | 想法唯一 ID | -| content | LONGTEXT | 否 | NULL | 想法内容文本 | -| create_time | INT UNSIGNED | 否 | NULL | 创建时间 (Unix 时间戳) | -| star | TINYINT | 否 | -1 | 评分 (0-5, -1=无评分) | -| chapter_name | VARCHAR(255) | 否 | NULL | 所在章节名称 | -| is_finish | TINYINT(1) | 否 | 0 | 是否书评 (1=书评) | -| fetched_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 获取时间 | - -**索引**: -- PRIMARY KEY (id) -- idx_reviews_book_id (book_id) - -**记录数**:19 - ---- - -### 3.17 recommended_books - 推荐书籍表 - -> 来自微信读书 /book/recommend 接口 - -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | id | BIGINT | 是 (PK) | AUTO | 自增主键 | | book_id | VARCHAR(64) | 是 | - | 书籍 ID | | title | VARCHAR(255) | 否 | NULL | 书名 | @@ -424,24 +676,69 @@ | new_rating | DECIMAL(5,1) | 否 | 0 | 评分 (百分制) | | new_rating_count | INT UNSIGNED | 否 | 0 | 评分人数 | | rating_title | VARCHAR(32) | 否 | NULL | 评分标签 (神作/力荐等) | -| fetched_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 获取时间 | +| deep_link | VARCHAR(512) | 否 | NULL | 跳转链接 | +| search_idx | INT UNSIGNED | 否 | 0 | 结果序号 (用于翻页) | +| price | INT UNSIGNED | 否 | NULL | 价格 (分) | +| pay_type | INT UNSIGNED | 否 | NULL | 付费类型 | +| book_type | TINYINT | 否 | 0 | 书籍类型 (0=电子书) | +| batch_id | VARCHAR(32) | 否 | NULL | 推荐批次 ID | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (id) -- idx_recommended_book_id (book_id) -- idx_recommended_fetched (fetched_at DESC) - -**记录数**:48 +**索引**:PK(id), idx_recommended_book_id(book_id), idx_recommended_batch(batch_id), idx_recommended_rating(new_rating DESC) --- -### 3.18 search_results - 搜索结果表 +### 3.28 similar_books — 相似书推荐 -> 来自微信读书 /store/search 接口 +> 来源:`/book/similar` | 记录数:0(按需获取) -| 字段名 | 数据类型 | 是否必填 | 默认值 | 说明 | -|--------|----------|----------|--------|------| +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| | id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| source_book_id | VARCHAR(64) | 是 | - | 源书籍 ID | +| book_id | VARCHAR(64) | 是 | - | 推荐书籍 ID | +| title | VARCHAR(255) | 否 | NULL | 书名 | +| author | VARCHAR(255) | 否 | NULL | 作者 | +| cover | VARCHAR(512) | 否 | NULL | 封面图 URL | +| deep_link | VARCHAR(512) | 否 | NULL | 跳转链接 | +| idx | INT UNSIGNED | 否 | NULL | 结果序号 (用于翻页) | +| session_id | VARCHAR(64) | 否 | NULL | 翻页会话 ID | +| book_json | TEXT | 否 | NULL | 书籍完整信息 (JSON) | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), idx_similar_source(source_book_id), idx_similar_book_id(book_id) + +--- + +### 3.29 search_sessions — 搜索会话 + +> 来源:`/store/search` 的元数据 | 记录数:1(按需获取) + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| sid | VARCHAR(64) | 否 | NULL | 搜索会话 ID | +| keyword | VARCHAR(255) | 是 | - | 搜索关键词 | +| scope | INT UNSIGNED | 否 | NULL | 搜索类型 (0=全部, 10=电子书, 14=听书等) | +| has_more | TINYINT(1) | 是 | 0 | 是否有更多 (1=有) | +| max_idx | INT UNSIGNED | 否 | 0 | 当前翻页偏移 | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(id), idx_search_sessions_keyword(keyword), idx_search_sessions_sid(sid) + +--- + +### 3.30 search_results — 搜索结果 + +> 来源:`/store/search` 的 `results[].books[]` | 记录数:20(按需获取) + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| id | BIGINT | 是 (PK) | AUTO | 自增主键 | +| session_id | BIGINT | 否 | NULL | 关联搜索会话 ID | | keyword | VARCHAR(255) | 是 | - | 搜索关键词 | | book_id | VARCHAR(64) | 是 | - | 书籍 ID | | title | VARCHAR(255) | 否 | NULL | 书名 | @@ -454,15 +751,34 @@ | new_rating | DECIMAL(5,1) | 否 | 0 | 评分 (百分制) | | new_rating_count | INT UNSIGNED | 否 | 0 | 评分人数 | | rating_title | VARCHAR(32) | 否 | NULL | 评分标签 | -| search_idx | INT UNSIGNED | 否 | 0 | 搜索序号 | -| fetched_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 获取时间 | +| deep_link | VARCHAR(512) | 否 | NULL | 跳转链接 | +| pay_type | INT UNSIGNED | 否 | NULL | 付费类型 | +| price | INT UNSIGNED | 否 | NULL | 价格 (分) | +| soldout | TINYINT(1) | 是 | 0 | 是否下架 (1=下架) | +| search_idx | INT UNSIGNED | 否 | 0 | 搜索序号 (用于翻页) | +| result_scope | INT UNSIGNED | 否 | NULL | 回包分组类型 | +| result_title | VARCHAR(64) | 否 | NULL | 回包分组标题 (如 "电子书") | +| scope_count | INT UNSIGNED | 否 | NULL | 该分组总结果数 | +| current_count | INT UNSIGNED | 否 | NULL | 本次返回数量 | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | -**索引**: -- PRIMARY KEY (id) -- idx_search_keyword (keyword) -- idx_search_book_id (book_id) +**索引**:PK(id), idx_search_results_keyword(keyword), idx_search_results_book_id(book_id), idx_search_results_session(session_id) -**记录数**:20 +--- + +### 3.31 fetch_meta — 数据采集元信息 + +> 系统表 | 记录数:3 + +| 字段名 | 数据类型 | 必填 | 默认值 | 说明 | +|--------|----------|------|--------|------| +| key | VARCHAR(64) | 是 (PK) | - | 元信息键名 | +| value | TEXT | 否 | NULL | 元信息值 | +| created_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP | 记录创建时间 | +| updated_at | TIMESTAMP | 是 | CURRENT_TIMESTAMP ON UPDATE | 记录更新时间 | + +**索引**:PK(key) --- @@ -472,19 +788,29 @@ ``` books (书架) - ├── book_id ──── book_details (书籍详情) [1:1 按需] - ├── book_id ──── book_progress (阅读进度) [1:1 按需] - ├── book_id ──── book_highlights (划线) [1:N 按需] - ├── book_id ──── book_chapters (章节) [1:N 按需] - ├── book_id ──── book_reviews (想法) [1:N 按需] - └── book_id ──── notebooks (笔记本概览) [1:1 有笔记的书] + ├── book_id ──── book_details (书籍详情) [1:1 按需] + ├── book_id ──── book_progress (阅读进度) [1:1 按需] + ├── book_id ──── book_highlights (划线) [1:N 按需] + ├── book_id ──── book_chapters (章节) [1:N 按需] + ├── book_id ──── book_reviews (个人想法) [1:N 按需] + ├── book_id ──── book_best_bookmarks (热门划线) [1:N 按需] + ├── book_id ──── book_underlines (划线热度) [1:N 按需] + ├── book_id ──── public_reviews (公开点评) [1:N 按需] + ├── book_id ──── notebooks (笔记本概览) [1:1 有笔记的书] + └── book_id ──── similar_books (相似书) [1:N 按需, source_book_id] read_longest (阅读排行) - └── book_id ──── books (书架) [N:1 关联] + └── book_id ──── books (书架) [N:1 关联] + +prefer_books (偏好书籍卡片) + └── book_id ──── books (书架) [N:1 关联] recommended_books (推荐) └── book_id (独立,不关联 books) +search_sessions (搜索会话) + └── id ──── search_results (搜索结果) [1:N 关联] + search_results (搜索) └── book_id (独立,不关联 books) @@ -494,8 +820,10 @@ archives (书单) ### 4.2 逻辑分组 -- **静态数据组**(采集时全量写入):books, reading_stats, read_times, read_longest, prefer_categories, prefer_authors, prefer_time, notebooks, read_stat_items, archives, fetch_meta -- **动态数据组**(用户操作时按需写入):book_details, book_progress, book_highlights, book_chapters, book_reviews, recommended_books, search_results +| 分组 | 表名 | 说明 | +|------|------|------| +| **静态数据组**(采集时全量写入) | books, shelf_albums, shelf_meta, archives, reading_stats, read_times, read_longest, read_stat_items, prefer_categories, prefer_authors, prefer_publishers, prefer_cps, prefer_books, prefer_time, read_rank, medals, year_reports, notebooks, fetch_meta | 共 19 张表 | +| **动态数据组**(用户操作时按需写入) | book_details, book_progress, book_highlights, book_chapters, book_reviews, book_best_bookmarks, book_underlines, public_reviews, recommended_books, similar_books, search_sessions, search_results | 共 12 张表 | --- @@ -505,14 +833,15 @@ archives (书单) |------------|------|------------------| | VARCHAR(n) | 变长字符串(书名、作者等) | TEXT | | LONGTEXT | 长文本(简介、划线内容) | TEXT | -| TEXT | 中等文本 | TEXT | +| TEXT | 中等文本(JSON、简介等) | TEXT | | INT UNSIGNED | 无符号整数(时间戳、计数) | INTEGER | | TINYINT(1) | 布尔值(0/1) | INTEGER | -| TINYINT UNSIGNED | 小范围整数(小时 0-23、进度 0-100) | INTEGER | +| TINYINT | 小范围整数(评分、类型) | INTEGER | +| TINYINT UNSIGNED | 小范围无符号整数(小时 0-23、进度 0-100) | INTEGER | | BIGINT | 自增主键 | INTEGER | | DECIMAL(5,1) | 精度数值(评分) | REAL | -| DOUBLE | 浮点数(权重、日均时长) | REAL | -| TIMESTAMP | 时间戳(自动维护) | INTEGER (Unix 时间戳) | +| DOUBLE | 浮点数(权重、日均时长、热度分数) | REAL | +| TIMESTAMP | 时间戳(自动维护 + _dt 可读列) | TEXT (ISO 格式) | --- @@ -522,3 +851,18 @@ archives (书单) - 排序规则:`utf8mb4_unicode_ci`(不区分大小写,支持多语言排序) - 连接字符集:`utf8mb4`(PyMySQL 连接配置中指定) - 前端编码:`UTF-8`(HTML meta 标签声明) + +--- + +## 7. API 文档覆盖对照 + +| 官方 API 文档 | 对应数据表 | +|--------------|-----------| +| shelf.md | books, shelf_albums, shelf_meta, archives | +| readdata.md | reading_stats, read_times, read_longest, read_stat_items, prefer_categories, prefer_authors, prefer_publishers, prefer_cps, prefer_books, prefer_time, read_rank, medals, year_reports | +| notes.md | notebooks, book_highlights, book_chapters, book_reviews, book_best_bookmarks, book_underlines, public_reviews | +| book.md | book_details, book_progress | +| discover.md | recommended_books, similar_books | +| search.md | search_sessions, search_results | +| review.md | book_reviews, public_reviews | +| profile.md | (用户信息未入库) | diff --git a/docs/TECHNICAL_DOC.md b/docs/TECHNICAL_DOC.md index 4b02e65..ba7c4e8 100644 --- a/docs/TECHNICAL_DOC.md +++ b/docs/TECHNICAL_DOC.md @@ -1,6 +1,6 @@ # 微信读书数据展示网站 - 技术文档 -> 版本:1.2.0 | 更新时间:2026-07-17 | 作者:WorkBuddy +> 版本:2.0.0 | 更新时间:2026-07-17 | 作者:WorkBuddy | 数据模型版本:v2.0(31 表) --- @@ -31,8 +31,8 @@ | 前端 | HTML5 + CSS3 + 原生 JavaScript | 无框架依赖,轻量高效 | | 图表 | ECharts 5.5.0 | 年度趋势、时段分布、分类饼图、作者排行 | | 后端 | Python Flask | 轻量级 Web 框架,提供 RESTful API | -| 数据库 | MySQL 5.7+(主)/ SQLite(采集) | MySQL 为生产数据库,SQLite 用于数据采集中间存储 | -| 数据采集 | 微信读书 Agent Gateway API | 通过 weread-skills 技能提供的 API 接口 | +| 数据库 | MySQL 5.7+(主)/ SQLite(采集) | MySQL 为生产数据库,31 张表完整覆盖官方 API,SQLite 用于数据采集中间存储 | +| 数据采集 | 微信读书 Agent Gateway API | 通过 weread-skills 技能提供的 API 接口,覆盖 8 份官方 API 文档 | | 版本控制 | Git | dev 分支开发,master 分支发布,遵循 Conventional Commits | --- @@ -278,7 +278,30 @@ HTTP 503 Service Unavailable - 连接失败时显示固定告警横幅,阻止后续数据加载 - 使用 `db_required` 装饰器统一处理数据库连接异常 -### 4.4 Git 分支策略 +### 4.4 数据模型 v2.0 设计规范 + +基于微信读书官方 8 份 API 文档的完整字段分析,数据模型 v2.0 遵循以下设计规范: + +**双列时间戳模式**:所有整型 Unix 时间戳字段(`INT UNSIGNED`)均伴随一个 `_dt TIMESTAMP` 列,例如 `update_time` ↔ `update_time_dt`。便于直接查询展示,同时保留原始时间戳用于排序和计算。 + +**审计列**:每张表统一维护 `created_at`(创建时间)和 `updated_at`(更新时间),使用 MySQL `DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP` 自动维护。 + +**索引设计原则**: +- 主键:业务主键优先(如 `book_id`),无自然主键时使用 `BIGINT AUTO_INCREMENT` +- 唯一约束:防止重复数据(如 `uk_highlights_bookmark(book_id, bookmark_id)`) +- 复合索引:按实际查询模式设计(如 `idx_highlights_chapter(book_id, chapter_uid)`) +- 排序索引:常用排序字段加 `DESC`(如 `idx_books_read_update_time(read_update_time DESC)`) + +**表覆盖范围**:31 张表完整覆盖 8 份官方 API 文档: +- shelf.md → books, shelf_albums, shelf_meta, archives +- readdata.md → reading_stats, read_times, read_longest, read_stat_items, prefer_categories, prefer_authors, prefer_publishers, prefer_cps, prefer_books, prefer_time, read_rank, medals, year_reports +- notes.md → notebooks, book_highlights, book_chapters, book_reviews, book_best_bookmarks, book_underlines, public_reviews +- book.md → book_details, book_progress +- discover.md → recommended_books, similar_books +- search.md → search_sessions, search_results +- fetch_meta → 系统元信息 + +### 4.5 Git 分支策略 - **dev 分支**:日常开发,功能迭代 - **master 分支**:稳定发布版本,通过 `--no-ff` 合并 dev