-- ============================================================================= -- 洗车记录系统 - Migration 0001: 基础表 (MySQL 8.x) -- ============================================================================= -- ----------------------------------------------------------------------------- -- 1. chemicals - 药剂字典(Grocy 缓存层) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS chemicals ( grocy_product_id VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, category VARCHAR(255) DEFAULT NULL, unit VARCHAR(50) NOT NULL DEFAULT 'ml', standard_dose DOUBLE DEFAULT NULL, notes TEXT DEFAULT NULL, is_active TINYINT(1) NOT NULL DEFAULT 1, fetched_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (grocy_product_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX idx_chemicals_category ON chemicals(category); CREATE INDEX idx_chemicals_active ON chemicals(is_active); CREATE INDEX idx_chemicals_fetched ON chemicals(fetched_at); -- ----------------------------------------------------------------------------- -- 2. weather_snapshots - 天气快照 -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS weather_snapshots ( id INT AUTO_INCREMENT PRIMARY KEY, snapshot_date VARCHAR(10) NOT NULL, city VARCHAR(100) NOT NULL, provider VARCHAR(50) NOT NULL, temp_c DOUBLE DEFAULT NULL, humidity INT DEFAULT NULL, weather_desc VARCHAR(255) DEFAULT NULL, weather_code VARCHAR(20) DEFAULT NULL, wind_kph DOUBLE DEFAULT NULL, precip_mm DOUBLE DEFAULT NULL, raw_json TEXT DEFAULT NULL, fetched_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE UNIQUE INDEX uk_weather_city_date ON weather_snapshots(city, snapshot_date); CREATE INDEX idx_weather_date ON weather_snapshots(snapshot_date); -- ----------------------------------------------------------------------------- -- 3. wash_records - 洗车记录 -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS wash_records ( id INT AUTO_INCREMENT PRIMARY KEY, wash_date VARCHAR(10) NOT NULL, wash_type VARCHAR(20) NOT NULL, weather_snapshot_id INT DEFAULT NULL, location VARCHAR(255) DEFAULT NULL, cost DOUBLE NOT NULL DEFAULT 0, duration_min INT DEFAULT NULL, notes TEXT DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT chk_wash_type CHECK (wash_type IN ('quick','full','detail','other')), CONSTRAINT fk_wash_weather FOREIGN KEY (weather_snapshot_id) REFERENCES weather_snapshots(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX idx_wash_records_date ON wash_records(wash_date); CREATE INDEX idx_wash_records_type ON wash_records(wash_type); CREATE INDEX idx_wash_records_weather ON wash_records(weather_snapshot_id); -- ----------------------------------------------------------------------------- -- 4. chemical_usage - 药剂消耗 -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS chemical_usage ( id INT AUTO_INCREMENT PRIMARY KEY, usage_date VARCHAR(10) NOT NULL, chemical_id VARCHAR(255) NOT NULL, amount DOUBLE NOT NULL, wash_record_id INT DEFAULT NULL, notes TEXT DEFAULT NULL, sync_status VARCHAR(20) NOT NULL DEFAULT 'pending', sync_at DATETIME DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT chk_sync_status CHECK (sync_status IN ('pending','synced','failed')), CONSTRAINT fk_usage_chemical FOREIGN KEY (chemical_id) REFERENCES chemicals(grocy_product_id) ON DELETE RESTRICT, CONSTRAINT fk_usage_wash FOREIGN KEY (wash_record_id) REFERENCES wash_records(id) ON DELETE SET NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; CREATE INDEX idx_usage_date ON chemical_usage(usage_date); CREATE INDEX idx_usage_chemical ON chemical_usage(chemical_id); CREATE INDEX idx_usage_wash ON chemical_usage(wash_record_id); CREATE INDEX idx_usage_sync ON chemical_usage(sync_status); -- ----------------------------------------------------------------------------- -- 5. settings - 运行时配置 KV -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS settings ( `key` VARCHAR(100) NOT NULL PRIMARY KEY, value TEXT DEFAULT NULL, is_secret TINYINT(1) NOT NULL DEFAULT 0, description TEXT DEFAULT NULL, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; INSERT IGNORE INTO settings (`key`, value, is_secret, description) VALUES ('app_city', NULL, 0, '所在城市(用于天气查询)'), ('app_timezone', 'Asia/Shanghai', 0, '时区'), ('grocy_url', NULL, 0, 'Grocy 实例 URL'), ('grocy_api_token', NULL, 1, 'Grocy REST API token'), ('backup_keep_count', '10', 0, '本地备份保留份数'), ('backup_dir', 'storage/backups', 0, '备份输出目录'); -- ----------------------------------------------------------------------------- -- 6. views -- ----------------------------------------------------------------------------- DROP VIEW IF EXISTS v_wash_monthly_count; CREATE VIEW v_wash_monthly_count AS SELECT SUBSTRING(wash_date, 1, 7) AS month, COUNT(*) AS wash_count, SUM(COALESCE(cost, 0)) AS total_cost FROM wash_records GROUP BY SUBSTRING(wash_date, 1, 7) ORDER BY month DESC; DROP VIEW IF EXISTS v_chemical_monthly_usage; CREATE VIEW v_chemical_monthly_usage AS SELECT SUBSTRING(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 SUBSTRING(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, DATEDIFF(NOW(), STR_TO_DATE(wash_date, '%Y-%m-%d')) AS days_since FROM wash_records ORDER BY wash_date DESC, id DESC LIMIT 1;