"""数据库操作层 — 封装所有 SQLite 操作。""" import sqlite3 import os from datetime import datetime DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "books.db") def _connect(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row conn.execute("PRAGMA journal_mode=WAL") return conn def _column_exists(conn, table, column): """检查列是否已存在。""" cols = conn.execute(f"PRAGMA table_info({table})").fetchall() return any(c["name"] == column for c in cols) def init_db(): """初始化数据库和表结构。""" conn = _connect() conn.execute(""" CREATE TABLE IF NOT EXISTS books ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, author TEXT, translator TEXT, publisher TEXT, pub_date TEXT, cover_url TEXT, format TEXT DEFAULT 'paper', status TEXT DEFAULT 'to-read', rating INTEGER, douban_score REAL, goodreads_score REAL, tags TEXT DEFAULT '', notes TEXT, start_date TEXT, finish_date TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')) ) """) # v2 新增字段 — 兼容已有数据库 new_cols = [ ("\"group\"", "TEXT DEFAULT ''"), ("isbn", "TEXT DEFAULT ''"), ("priority", "TEXT DEFAULT ''"), ("read_reason", "TEXT DEFAULT ''"), ("read_order", "INTEGER"), ("cover_blob", "BLOB"), ("cover_local", "TEXT DEFAULT ''"), ("title_en", "TEXT DEFAULT ''"), ("author_en", "TEXT DEFAULT ''"), ("douban_url", "TEXT DEFAULT ''"), ("goodreads_url","TEXT DEFAULT ''"), ] for col_name, col_def in new_cols: plain = col_name.strip('"') if not _column_exists(conn, "books", plain): conn.execute(f"ALTER TABLE books ADD COLUMN {col_name} {col_def}") conn.commit() conn.close() def add_book(title, *, author=None, translator=None, publisher=None, pub_date=None, cover_url=None, format="paper", status="to-read", rating=None, douban_score=None, goodreads_score=None, tags="", notes=None, start_date=None, finish_date=None, group="", isbn="", priority="", read_reason="", read_order=None, title_en="", author_en="", douban_url="", goodreads_url=""): """添加一本书,返回新书 ID。""" conn = _connect() cur = conn.execute(""" INSERT INTO books (title, author, translator, publisher, pub_date, cover_url, format, status, rating, douban_score, goodreads_score, tags, notes, start_date, finish_date, "group", isbn, priority, read_reason, read_order, title_en, author_en, douban_url, goodreads_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, (title, author, translator, publisher, pub_date, cover_url, format, status, rating, douban_score, goodreads_score, tags, notes, start_date, finish_date, group, isbn, priority, read_reason, read_order, title_en, author_en, douban_url, goodreads_url)) conn.commit() book_id = cur.lastrowid conn.close() return book_id def update_book(book_id, **kwargs): """更新书目字段。只更新传入的字段。""" if not kwargs: return allowed = { "title", "author", "translator", "publisher", "pub_date", "cover_url", "format", "status", "rating", "douban_score", "goodreads_score", "tags", "notes", "start_date", "finish_date", "group", "isbn", "priority", "read_reason", "read_order", "cover_blob", "cover_local", "title_en", "author_en", "douban_url", "goodreads_url", } fields = {k: v for k, v in kwargs.items() if k in allowed} if not fields: return fields["updated_at"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") set_clause = ", ".join(f"{k} = ?" for k in fields) values = list(fields.values()) + [book_id] conn = _connect() conn.execute(f"UPDATE books SET {set_clause} WHERE id = ?", values) conn.commit() conn.close() def delete_book(book_id): """删除一本书。""" conn = _connect() conn.execute("DELETE FROM books WHERE id = ?", (book_id,)) conn.commit() conn.close() def get_book(book_id): """获取单本书详情,返回 dict 或 None。""" conn = _connect() row = conn.execute("SELECT * FROM books WHERE id = ?", (book_id,)).fetchone() conn.close() return dict(row) if row else None def list_books(*, status=None, format=None, tag=None, group=None): """查询书目列表,支持按状态/格式/标签/分组过滤。""" conn = _connect() query = "SELECT * FROM books WHERE 1=1" params = [] if status: query += " AND status = ?" params.append(status) if format: query += " AND format = ?" params.append(format) if tag: query += " AND (',' || tags || ',' LIKE ?)" params.append(f"%,{tag},%") if group: query += ' AND "group" = ?' params.append(group) query += " ORDER BY updated_at DESC" rows = conn.execute(query, params).fetchall() conn.close() return [dict(r) for r in rows] def get_stats(): """获取统计数据。""" conn = _connect() total = conn.execute("SELECT COUNT(*) FROM books").fetchone()[0] by_status = {} for row in conn.execute("SELECT status, COUNT(*) as cnt FROM books GROUP BY status"): by_status[row["status"]] = row["cnt"] by_format = {} for row in conn.execute("SELECT format, COUNT(*) as cnt FROM books GROUP BY format"): by_format[row["format"]] = row["cnt"] by_group = {} for row in conn.execute('SELECT "group", COUNT(*) as cnt FROM books WHERE "group" != "" GROUP BY "group"'): by_group[row["group"]] = row["cnt"] conn.close() return { "total": total, "by_status": by_status, "by_format": by_format, "by_group": by_group, } def get_all_tags(): """获取所有不重复的标签列表。""" conn = _connect() rows = conn.execute("SELECT tags FROM books WHERE tags != ''").fetchall() conn.close() tag_set = set() for row in rows: for t in row["tags"].split(","): t = t.strip() if t: tag_set.add(t) return sorted(tag_set)