# -*- coding: utf-8 -*- """ziwei-power SQLite 数据库操作层""" import sqlite3 import json import os from datetime import datetime DB_DIR = os.path.join(os.path.expanduser('~'), '.workbuddy', 'data', 'ziwei-power') os.makedirs(DB_DIR, exist_ok=True) DB_PATH = os.path.join(DB_DIR, 'ziwei_power.db') # 当前数据库 schema 版本 —— 改表结构时必须 +1 并补迁移逻辑 CURRENT_SCHEMA_VERSION = 4 def get_conn(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn def _get_schema_version(conn): """读取当前数据库的 schema 版本,无表时返回 0""" conn.execute(''' CREATE TABLE IF NOT EXISTS schema_version ( version INTEGER NOT NULL ) ''') row = conn.execute('SELECT version FROM schema_version').fetchone() return row['version'] if row else 0 def _set_schema_version(conn, version): """写入 schema 版本""" conn.execute('DELETE FROM schema_version') conn.execute('INSERT INTO schema_version (version) VALUES (?)', (version,)) def init_db(): """初始化数据库表 & 自动迁移""" conn = get_conn() current = _get_schema_version(conn) # ── 迁移步骤(按版本号递增)──────────────────────── if current < 1: # v1: 初始表结构 conn.execute(''' CREATE TABLE IF NOT EXISTS checkins ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT UNIQUE NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) ''') if current < 2: # v2: 心愿清单 conn.execute(''' CREATE TABLE IF NOT EXISTS wishes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, priority TEXT NOT NULL DEFAULT '中', deadline TEXT NOT NULL DEFAULT '', done INTEGER NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0, created_at TEXT NOT NULL ) ''') if current < 3: # v3: 优先级改为四象限 conn.execute("ALTER TABLE wishes ADD COLUMN quadrant TEXT NOT NULL DEFAULT '重要不紧急'") conn.execute("UPDATE wishes SET quadrant = CASE priority WHEN '高' THEN '重要紧急' WHEN '中' THEN '重要不紧急' WHEN '低' THEN '不紧急不重要' ELSE '重要不紧急' END WHERE quadrant = '重要不紧急'") if current < 4: # v4: 日打卡 → 周打卡 # 备份旧表 → 聚合 → 新建周表 existing = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='checkins'").fetchone() if existing: # 检查是否已有备份表(幂等) backup_exists = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='checkins_daily_backup'").fetchone() if not backup_exists: conn.execute("ALTER TABLE checkins RENAME TO checkins_daily_backup") # 重新创建 checkins 表(结构不变,date 语义变为周号) conn.execute(''' CREATE TABLE IF NOT EXISTS checkins ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT UNIQUE NOT NULL, data TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ) ''') # 聚合日数据到周 if not backup_exists: _aggregate_daily_to_weekly(conn) # ── 将来加字段/改表在此扩展 ── # if current < 2: # conn.execute('ALTER TABLE checkins ADD COLUMN tags TEXT DEFAULT ""') # # 可选:对已有行做数据补全 # conn.execute("UPDATE checkins SET tags = '[]' WHERE tags IS NULL") # ── 写入最新版本号 ── _set_schema_version(conn, CURRENT_SCHEMA_VERSION) conn.commit() conn.close() def get_checkin(date_str): """获取某天的打卡记录,返回 dict 或 None""" conn = get_conn() row = conn.execute('SELECT * FROM checkins WHERE date = ?', (date_str,)).fetchone() conn.close() if row: return { 'id': row['id'], 'date': row['date'], 'data': json.loads(row['data']), 'created_at': row['created_at'], 'updated_at': row['updated_at'] } return None def save_checkin(date_str, data_dict): """保存或更新打卡记录""" now = datetime.now().isoformat() conn = get_conn() existing = conn.execute('SELECT id FROM checkins WHERE date = ?', (date_str,)).fetchone() json_data = json.dumps(data_dict, ensure_ascii=False) if existing: conn.execute( 'UPDATE checkins SET data = ?, updated_at = ? WHERE date = ?', (json_data, now, date_str) ) else: conn.execute( 'INSERT INTO checkins (date, data, created_at, updated_at) VALUES (?, ?, ?, ?)', (date_str, json_data, now, now) ) conn.commit() conn.close() def delete_checkin(date_str): """删除某天的打卡记录""" conn = get_conn() conn.execute('DELETE FROM checkins WHERE date = ?', (date_str,)) conn.commit() conn.close() def get_all_checkins(): """获取所有打卡记录,按日期倒序""" conn = get_conn() rows = conn.execute('SELECT * FROM checkins ORDER BY date DESC').fetchall() conn.close() results = [] for row in rows: results.append({ 'id': row['id'], 'date': row['date'], 'data': json.loads(row['data']), 'created_at': row['created_at'], 'updated_at': row['updated_at'] }) return results # ── 心愿清单 CRUD ────────────────────────────────── def get_wishes(): """获取所有心愿,按 sort_order 排序""" conn = get_conn() rows = conn.execute('SELECT * FROM wishes ORDER BY sort_order').fetchall() conn.close() return [dict(row) for row in rows] def save_wish(name, quadrant, deadline): """新增一条心愿""" now = datetime.now().isoformat() conn = get_conn() max_order = conn.execute('SELECT COALESCE(MAX(sort_order), -1) + 1 AS n FROM wishes').fetchone()['n'] conn.execute( 'INSERT INTO wishes (name, quadrant, deadline, done, sort_order, created_at) VALUES (?, ?, ?, 0, ?, ?)', (name, quadrant, deadline, max_order, now) ) conn.commit() wish_id = conn.execute('SELECT last_insert_rowid()').fetchone()[0] conn.close() return wish_id def update_wish(wish_id, **kwargs): """更新心愿字段""" allowed = ['name', 'quadrant', 'deadline', 'done'] updates = {k: v for k, v in kwargs.items() if k in allowed} if not updates: return conn = get_conn() sets = ', '.join(f'{k} = ?' for k in updates) vals = list(updates.values()) + [wish_id] conn.execute(f'UPDATE wishes SET {sets} WHERE id = ?', vals) conn.commit() conn.close() def delete_wish(wish_id): """删除心愿""" conn = get_conn() conn.execute('DELETE FROM wishes WHERE id = ?', (wish_id,)) conn.commit() conn.close() def reorder_wishes(order_list): """批量更新排序:order_list = [id1, id2, ...]""" conn = get_conn() for idx, wid in enumerate(order_list): conn.execute('UPDATE wishes SET sort_order = ? WHERE id = ?', (idx, wid)) conn.commit() conn.close() def get_weeks_list(): """返回所有已有打卡记录的周号列表""" conn = get_conn() rows = conn.execute('SELECT DISTINCT date FROM checkins ORDER BY date').fetchall() conn.close() return [r['date'] for r in rows] def get_daily_backup(week_str): """获取某周的原始日记录(从备份表)""" from datetime import datetime as _dt, timedelta if '-W' not in week_str: return [] year_part, week_part = week_str.split('-W') try: year = int(year_part) week = int(week_part) except (ValueError, IndexError): return [] jan1 = _dt(year, 1, 1) delta = timedelta(days=(week - 1) * 7 - jan1.weekday()) monday = jan1 + delta dates = [monday.strftime('%Y-%m-%d')] for i in range(1, 7): dates.append((monday + timedelta(days=i)).strftime('%Y-%m-%d')) conn = get_conn() placeholders = ','.join('?' for _ in dates) exists = conn.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='checkins_daily_backup'").fetchone() if not exists: conn.close() return [] rows = conn.execute( f"SELECT date, data FROM checkins_daily_backup WHERE date IN ({placeholders}) ORDER BY date", dates ).fetchall() conn.close() results = [] for row in rows: results.append({'date': row['date'], 'data': json.loads(row['data'])}) return results def _aggregate_daily_to_weekly(conn): """将 checkins_daily_backup 的日数据聚合为周数据,写入 checkins""" from datetime import datetime as _dt rows = conn.execute('SELECT date, data FROM checkins_daily_backup ORDER BY date').fetchall() weekly = {} for row in rows: try: d = _dt.strptime(row['date'], '%Y-%m-%d') except ValueError: continue iso_year, iso_week, _ = d.isocalendar() week_key = f"{iso_year}-W{iso_week:02d}" if week_key not in weekly: weekly[week_key] = {'morning': [], 'evening': [], 'study': []} data = json.loads(row['data']) for mi in data.get('morning', []): text = mi if isinstance(mi, str) else mi.get('text', '') if text: existing_texts = {x if isinstance(x, str) else x.get('text', '') for x in weekly[week_key]['morning']} if text not in existing_texts: weekly[week_key]['morning'].append(mi) for ei in data.get('evening', []): mst = ei.get('mistake', '') if isinstance(ei, dict) else '' if mst: existing = {x.get('mistake', '') if isinstance(x, dict) else x for x in weekly[week_key]['evening']} if mst not in existing: weekly[week_key]['evening'].append(ei if isinstance(ei, dict) else {'mistake': ei, 'improvement': ''}) existing_names = {} for si in weekly[week_key]['study']: nm = si.get('name', '') if isinstance(si, dict) else str(si) if nm: existing_names[nm] = si for si in data.get('study', []): nm = si.get('name', '') if isinstance(si, dict) else str(si) if nm and nm not in existing_names: weekly[week_key]['study'].append(si) existing_names[nm] = si now = _dt.now().isoformat() for week_key in sorted(weekly.keys()): conn.execute( 'INSERT INTO checkins (date, data, created_at, updated_at) VALUES (?, ?, ?, ?)', (week_key, json.dumps(weekly[week_key], ensure_ascii=False), now, now) )