-- ============================================================================= -- 洗车记录系统 - Migration 0001: 基础表(Node.js / better-sqlite3 版) -- ============================================================================= -- 基础 PRAGMA 由 server/src/db.js 统一设置(journal_mode=WAL / foreign_keys=ON / synchronous=NORMAL / busy_timeout=5000) -- ----------------------------------------------------------------------------- -- 1. chemicals - 药剂字典(Grocy 缓存层) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS chemicals ( grocy_product_id TEXT NOT NULL, name TEXT NOT NULL, category TEXT, unit TEXT NOT NULL DEFAULT 'ml', standard_dose REAL, notes TEXT, is_active INTEGER NOT NULL DEFAULT 1 CHECK (is_active IN (0, 1)), fetched_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), PRIMARY KEY (grocy_product_id) ); CREATE INDEX IF NOT EXISTS idx_chemicals_category ON chemicals(category); CREATE INDEX IF NOT EXISTS idx_chemicals_active ON chemicals(is_active); CREATE INDEX IF NOT EXISTS idx_chemicals_fetched ON chemicals(fetched_at); -- ----------------------------------------------------------------------------- -- 2. weather_snapshots - 天气快照 -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS weather_snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, snapshot_date TEXT NOT NULL, city TEXT NOT NULL, provider TEXT NOT NULL CHECK (provider IN ('qweather','openweathermap')), temp_c REAL, humidity INTEGER, weather_desc TEXT, weather_code TEXT, wind_kph REAL, precip_mm REAL, raw_json TEXT, fetched_at TEXT NOT NULL DEFAULT (datetime('now')) ); CREATE UNIQUE INDEX IF NOT EXISTS uk_weather_city_date ON weather_snapshots(city, snapshot_date); CREATE INDEX IF NOT EXISTS idx_weather_date ON weather_snapshots(snapshot_date); -- ----------------------------------------------------------------------------- -- 3. wash_records - 洗车记录 -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS wash_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, wash_date TEXT NOT NULL, wash_type TEXT NOT NULL CHECK (wash_type IN ('quick','full','detail','other')), weather_snapshot_id INTEGER, location TEXT, cost REAL NOT NULL DEFAULT 0, duration_min INTEGER, notes TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (weather_snapshot_id) REFERENCES weather_snapshots(id) ON DELETE SET NULL ); CREATE INDEX IF NOT EXISTS idx_wash_records_date ON wash_records(wash_date); CREATE INDEX IF NOT EXISTS idx_wash_records_type ON wash_records(wash_type); CREATE INDEX IF NOT EXISTS idx_wash_records_weather ON wash_records(weather_snapshot_id); -- ----------------------------------------------------------------------------- -- 4. chemical_usage - 药剂消耗 -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS chemical_usage ( id INTEGER PRIMARY KEY AUTOINCREMENT, usage_date TEXT NOT NULL, chemical_id TEXT NOT NULL, amount REAL NOT NULL, wash_record_id INTEGER, notes TEXT, sync_status TEXT NOT NULL DEFAULT 'pending' CHECK (sync_status IN ('pending','synced','failed')), sync_at TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')), FOREIGN KEY (chemical_id) REFERENCES chemicals(grocy_product_id) ON DELETE RESTRICT, FOREIGN KEY (wash_record_id) REFERENCES wash_records(id) ON DELETE SET NULL ); CREATE INDEX IF NOT EXISTS idx_usage_date ON chemical_usage(usage_date); CREATE INDEX IF NOT EXISTS idx_usage_chemical ON chemical_usage(chemical_id); CREATE INDEX IF NOT EXISTS idx_usage_wash ON chemical_usage(wash_record_id); CREATE INDEX IF NOT EXISTS idx_usage_sync ON chemical_usage(sync_status); -- ----------------------------------------------------------------------------- -- 5. settings - 运行时配置 KV -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS settings ( key TEXT NOT NULL PRIMARY KEY, value TEXT, is_secret INTEGER NOT NULL DEFAULT 0, description TEXT, updated_at TEXT NOT NULL DEFAULT (datetime('now')) ); -- 预置 11 个 key INSERT OR IGNORE INTO settings (key, value, is_secret, description) VALUES ('db_path', NULL, 0, '数据库路径(SQLite 模式)'), ('app_city', NULL, 0, '所在城市(用于天气查询)'), ('app_timezone', 'Asia/Shanghai', 0, 'PHP 时区(保留兼容)'), ('grocy_url', NULL, 0, 'Grocy 实例 URL'), ('grocy_api_token', NULL, 1, 'Grocy REST API token'), ('weather_provider', 'qweather', 0, '天气提供方 qweather/openweathermap'), ('qweather_api_key', NULL, 1, '和风天气 API key'), ('qweather_api_host', 'api.qweather.com', 0, '和风 API host'), ('openweathermap_api_key', NULL, 1, 'OpenWeatherMap API key'), ('backup_keep_count', '10', 0, '本地备份保留份数'), ('backup_dir', 'storage/backups', 0, '备份输出目录'); -- ----------------------------------------------------------------------------- -- 6. 视图 -- ----------------------------------------------------------------------------- DROP VIEW IF EXISTS v_wash_monthly_count; CREATE VIEW v_wash_monthly_count AS SELECT substr(wash_date, 1, 7) AS month, COUNT(*) AS wash_count, SUM(COALESCE(cost, 0)) AS total_cost FROM wash_records GROUP BY substr(wash_date, 1, 7) ORDER BY month DESC; DROP VIEW IF EXISTS v_chemical_monthly_usage; CREATE VIEW v_chemical_monthly_usage AS SELECT substr(cu.usage_date, 1, 7) AS month, c.grocy_product_id AS grocy_product_id, c.name AS chemical_name, c.unit AS unit, SUM(cu.amount) AS total_amount, COUNT(*) AS usage_count FROM chemical_usage cu JOIN chemicals c ON c.grocy_product_id = cu.chemical_id GROUP BY substr(cu.usage_date, 1, 7), c.grocy_product_id ORDER BY month DESC, total_amount DESC; DROP VIEW IF EXISTS v_last_wash; CREATE VIEW v_last_wash AS SELECT id AS wash_id, wash_date, wash_type, CAST(julianday('now') - julianday(wash_date) AS INTEGER) AS days_since FROM wash_records ORDER BY wash_date DESC, id DESC LIMIT 1;