65b0bb04f8
把 CarLog v2.8 全套源码 + 配置导入到 i 仓库作为 baseline: - server/src/ (13 个路由 + middleware + services + config) - server/migrations/ (0001~0018 共 18 个迁移 + mysql) - server/test/ (12 文件 101 测试) - client/src/ (20 个 view + components + stores + api + composables) - client/public/ + client/scripts/ - 全部配置文件 (.editorconfig, .eslintrc.json, .prettierrc.json, vitest.config.js, lighthouserc.json, .pa11yci.json, package.json, carlog-init.sql) - .husky/pre-commit (git hooks) - docs/install/ (宝塔部署文档) 不含: - node_modules/ (本地 npm install) - .env (敏感, 走 .env.example) - *.zip / *.log / *.sqlite / .DS_Store 新增文档 docs/DEV-PLAN.md: - Phase 1: 平台基座 (019 migration + 3 个 platform 路由 + 3 个 view) - Phase 2: CarLog 子系统化 (后端 routes/ → subsystems/carlog/ + 前端 views/ → views/subsystems/carlog/ + 元数据驱动菜单) - Phase 3: 验证 (测试 + E2E + DB 完整性) - 交付清单 + commit 模板 + 给 Mavis review 的材料 后续 Trae 实施, 提交后我 code review + 跑测试。
460 lines
28 KiB
SQL
460 lines
28 KiB
SQL
-- ============================================================
|
||
-- 洗车管理系统 MySQL 一键初始化(v2.1)
|
||
-- 适用 MySQL 8.0+ / utf8mb4
|
||
--
|
||
-- 用法:
|
||
-- 1. 宝塔:选 carlog 库 →『导入』→ 选本文件
|
||
-- 2. 命令行:mysql -uroot -p carlog < carlog-init.sql
|
||
--
|
||
-- 完全幂等,反复重跑不会破坏数据
|
||
-- ============================================================
|
||
|
||
DROP PROCEDURE IF EXISTS _add_index_if_missing;
|
||
CREATE PROCEDURE _add_index_if_missing(IN p_table VARCHAR(64), IN p_index VARCHAR(64), IN p_def TEXT, IN p_unique INT) BEGIN
|
||
IF NOT EXISTS (SELECT 1 FROM information_schema.statistics WHERE table_schema = DATABASE() AND table_name = p_table AND index_name = p_index) THEN
|
||
IF p_unique = 1 THEN SET @s = CONCAT('CREATE UNIQUE INDEX ', p_index, ' ON ', p_table, ' ', p_def);
|
||
ELSE SET @s = CONCAT('CREATE INDEX ', p_index, ' ON ', p_table, ' ', p_def); END IF;
|
||
PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st;
|
||
END IF;
|
||
END;
|
||
|
||
DROP PROCEDURE IF EXISTS _try_sql;
|
||
CREATE PROCEDURE _try_sql(IN p_sql TEXT) BEGIN
|
||
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN END;
|
||
SET @s = p_sql;
|
||
PREPARE st FROM @s; EXECUTE st; DEALLOCATE PREPARE st;
|
||
END;
|
||
|
||
|
||
-- >>> 0001_init.sql
|
||
-- ==========================================================
|
||
-- =============================================================================
|
||
-- 洗车记录系统 - 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;
|
||
CALL _add_index_if_missing('chemicals', 'idx_chemicals_category', '(category)', 0);
|
||
CALL _add_index_if_missing('chemicals', 'idx_chemicals_active', '(is_active)', 0);
|
||
CALL _add_index_if_missing('chemicals', 'idx_chemicals_fetched', '(fetched_at)', 0);
|
||
-- -----------------------------------------------------------------------------
|
||
-- 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;
|
||
CALL _add_index_if_missing('weather_snapshots', 'uk_weather_city_date', '(city, snapshot_date)', 1);
|
||
CALL _add_index_if_missing('weather_snapshots', 'idx_weather_date', '(snapshot_date)', 0);
|
||
-- -----------------------------------------------------------------------------
|
||
-- 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;
|
||
CALL _add_index_if_missing('wash_records', 'idx_wash_records_date', '(wash_date)', 0);
|
||
CALL _add_index_if_missing('wash_records', 'idx_wash_records_type', '(wash_type)', 0);
|
||
CALL _add_index_if_missing('wash_records', 'idx_wash_records_weather', '(weather_snapshot_id)', 0);
|
||
-- -----------------------------------------------------------------------------
|
||
-- 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;
|
||
CALL _add_index_if_missing('chemical_usage', 'idx_usage_date', '(usage_date)', 0);
|
||
CALL _add_index_if_missing('chemical_usage', 'idx_usage_chemical', '(chemical_id)', 0);
|
||
CALL _add_index_if_missing('chemical_usage', 'idx_usage_wash', '(wash_record_id)', 0);
|
||
CALL _add_index_if_missing('chemical_usage', 'idx_usage_sync', '(sync_status)', 0);
|
||
-- -----------------------------------------------------------------------------
|
||
-- 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;
|
||
CALL _try_sql('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;
|
||
CALL _try_sql('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;
|
||
CALL _try_sql('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');
|
||
|
||
-- >>> 0002_auth.sql
|
||
-- ==========================================================
|
||
-- 0002_auth.sql - 用户认证 + 防撞库 (MySQL)
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
username VARCHAR(50) NOT NULL UNIQUE,
|
||
password_hash VARCHAR(255) NOT NULL,
|
||
role VARCHAR(20) NOT NULL DEFAULT 'user',
|
||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||
last_login_at DATETIME DEFAULT NULL,
|
||
last_login_ip VARCHAR(45) DEFAULT NULL,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
CONSTRAINT chk_role CHECK (role IN ('user','admin'))
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('users', 'idx_users_active', '(is_active)', 0);
|
||
CREATE TABLE IF NOT EXISTS login_attempts (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
attempted_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
ip_address VARCHAR(45) NOT NULL,
|
||
username VARCHAR(50) NOT NULL,
|
||
success TINYINT(1) NOT NULL,
|
||
user_agent VARCHAR(500) DEFAULT NULL,
|
||
failure_reason VARCHAR(100) DEFAULT NULL,
|
||
CONSTRAINT chk_success CHECK (success IN (0, 1))
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('login_attempts', 'idx_attempts_ip_time', '(ip_address, attempted_at)', 0);
|
||
CALL _add_index_if_missing('login_attempts', 'idx_attempts_user_time', '(username, attempted_at)', 0);
|
||
CALL _add_index_if_missing('login_attempts', 'idx_attempts_time', '(attempted_at)', 0);
|
||
CREATE TABLE IF NOT EXISTS auth_locks (
|
||
lock_key VARCHAR(100) PRIMARY KEY,
|
||
lock_type VARCHAR(10) NOT NULL,
|
||
target VARCHAR(50) NOT NULL,
|
||
locked_until DATETIME NOT NULL,
|
||
reason VARCHAR(255) DEFAULT NULL,
|
||
attempts INT NOT NULL DEFAULT 0,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT chk_lock_type CHECK (lock_type IN ('ip','user'))
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('auth_locks', 'idx_locks_until', '(locked_until)', 0);
|
||
INSERT IGNORE INTO settings (`key`, value, is_secret, description) VALUES
|
||
('session_lifetime_days', '30', 0, '登录 session 有效期(天)'),
|
||
('session_cookie_secure', 'auto', 0, 'Cookie secure 标志:true/false/auto'),
|
||
('login_max_failures_ip', '5', 0, '每 IP 允许的最大连续失败次数'),
|
||
('login_max_failures_user', '5', 0, '每用户名允许的最大连续失败次数'),
|
||
('login_lock_minutes_ip', '15', 0, 'IP 级别锁定时长(分钟)'),
|
||
('login_lock_minutes_user', '30', 0, '用户名级别锁定时长(分钟)'),
|
||
('login_global_max_failures', '10', 0, '触发全局 IP 封锁的失败次数'),
|
||
('login_global_lock_hours', '1', 0, '全局 IP 封锁时长(小时)'),
|
||
('login_attempts_retention_days', '30', 0, 'login_attempts 保留天数'),
|
||
('csrf_token_lifetime_hours', '12', 0, 'CSRF token 有效期(小时)'),
|
||
('bcrypt_cost', '12', 0, 'bcrypt cost factor');
|
||
|
||
-- >>> 0003_vehicles.sql
|
||
-- ==========================================================
|
||
-- 0003_vehicles.sql - 车辆管理 (MySQL)
|
||
CREATE TABLE IF NOT EXISTS vehicles (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
name VARCHAR(100) NOT NULL,
|
||
plate VARCHAR(20) DEFAULT NULL,
|
||
type VARCHAR(20) NOT NULL DEFAULT 'car',
|
||
color VARCHAR(30) DEFAULT NULL,
|
||
notes TEXT DEFAULT NULL,
|
||
is_active TINYINT(1) NOT NULL DEFAULT 1,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
CONSTRAINT chk_vehicle_type CHECK (type IN ('car','suv','mpv','truck','other'))
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('vehicles', 'idx_vehicles_active', '(is_active)', 0);
|
||
CALL _add_index_if_missing('vehicles', 'idx_vehicles_sort', '(sort_order)', 0);
|
||
CALL _try_sql('ALTER TABLE wash_records ADD COLUMN vehicle_id INT DEFAULT NULL');
|
||
CALL _add_index_if_missing('wash_records', 'idx_wash_records_vehicle', '(vehicle_id)', 0);
|
||
DROP VIEW IF EXISTS v_last_wash;
|
||
CALL _try_sql('CREATE VIEW v_last_wash AS SELECT w.id AS wash_id, w.wash_date, w.wash_type, w.vehicle_id, v.name AS vehicle_name, DATEDIFF(NOW(), STR_TO_DATE(w.wash_date, ''%Y-%m-%d'')) AS days_since FROM wash_records w LEFT JOIN vehicles v ON v.id = w.vehicle_id ORDER BY w.wash_date DESC, w.id DESC LIMIT 1');
|
||
|
||
-- >>> 0004_grocy_full.sql
|
||
-- ==========================================================
|
||
-- 0004_grocy_full.sql - Grocy 主数据同步字段 (MySQL)
|
||
CALL _try_sql('ALTER TABLE chemicals ADD COLUMN description TEXT DEFAULT NULL, ADD COLUMN current_amount DOUBLE NOT NULL DEFAULT 0, ADD COLUMN current_value DOUBLE NOT NULL DEFAULT 0, ADD COLUMN min_stock_amount DOUBLE NOT NULL DEFAULT 0, ADD COLUMN best_before_date VARCHAR(20) DEFAULT NULL, ADD COLUMN location VARCHAR(255) DEFAULT NULL, ADD COLUMN product_group_id INT DEFAULT NULL, ADD COLUMN qu_id INT DEFAULT NULL, ADD COLUMN location_id INT DEFAULT NULL, ADD COLUMN picture_file_name VARCHAR(255) DEFAULT NULL, ADD COLUMN last_synced_at DATETIME DEFAULT NULL');
|
||
CALL _add_index_if_missing('chemicals', 'idx_chem_amount', '(current_amount)', 0);
|
||
CALL _add_index_if_missing('chemicals', 'idx_chem_pg', '(product_group_id)', 0);
|
||
CALL _add_index_if_missing('chemicals', 'idx_chem_synced', '(last_synced_at)', 0);
|
||
INSERT IGNORE INTO settings (`key`, value, is_secret, description) VALUES
|
||
('grocy_sync_batch', '50', 0, 'Grocy 扣减同步每批条数'),
|
||
('grocy_low_stock_pct', '20', 0, '低库存阈值(百分比)'),
|
||
('grocy_pull_auto', '0', 0, 'Grocy 拉取模式:0=手动,1=启动时自动拉');
|
||
|
||
-- >>> 0005_inventory_detail.sql
|
||
-- ==========================================================
|
||
-- 0005_inventory_detail.sql (MySQL)
|
||
CALL _try_sql('ALTER TABLE chemicals ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT ''manual'', ADD COLUMN grocy_last_pulled_at DATETIME DEFAULT NULL');
|
||
CREATE TABLE IF NOT EXISTS category_mappings (
|
||
grocy_group_id INT PRIMARY KEY,
|
||
display_name VARCHAR(100) NOT NULL,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CREATE TABLE IF NOT EXISTS chemical_inventory_log (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
chemical_id VARCHAR(255) NOT NULL,
|
||
change_type VARCHAR(20) NOT NULL,
|
||
amount_delta DOUBLE NOT NULL,
|
||
amount_after DOUBLE DEFAULT NULL,
|
||
source VARCHAR(20) NOT NULL DEFAULT 'local',
|
||
source_ref VARCHAR(255) DEFAULT NULL,
|
||
note TEXT DEFAULT NULL,
|
||
occurred_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
CONSTRAINT chk_change_type CHECK (change_type IN ('purchase','consume','inventory','transfer','adjust'))
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('chemical_inventory_log', 'idx_invlog_chem', '(chemical_id, occurred_at DESC)', 0);
|
||
CALL _add_index_if_missing('chemical_inventory_log', 'idx_invlog_type', '(change_type)', 0);
|
||
INSERT IGNORE INTO settings (`key`, value, is_secret, description) VALUES
|
||
('grocy_categories_json', '[]', 0, 'Grocy 分类映射 JSON');
|
||
|
||
-- >>> 0006_unit_conversion.sql
|
||
-- ==========================================================
|
||
-- 0006_unit_conversion.sql (MySQL)
|
||
CALL _try_sql('ALTER TABLE chemicals ADD COLUMN qu_factor DOUBLE NOT NULL DEFAULT 1.0, ADD COLUMN consume_unit_id INT DEFAULT NULL, ADD COLUMN consume_unit_name VARCHAR(100) DEFAULT NULL');
|
||
CALL _try_sql('ALTER TABLE chemical_usage ADD COLUMN unit VARCHAR(50) DEFAULT NULL, ADD COLUMN stock_amount DOUBLE DEFAULT NULL, ADD COLUMN consume_unit_id INT DEFAULT NULL');
|
||
|
||
-- >>> 0007_vehicle_logs.sql
|
||
-- ==========================================================
|
||
-- 0007_vehicle_logs.sql (MySQL)
|
||
CREATE TABLE IF NOT EXISTS maintenance_records (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
vehicle_id INT NOT NULL,
|
||
maint_date VARCHAR(10) NOT NULL,
|
||
odometer_km INT DEFAULT NULL,
|
||
total_cost DOUBLE NOT NULL DEFAULT 0,
|
||
shop VARCHAR(255) DEFAULT NULL,
|
||
items_json JSON NOT NULL DEFAULT ('[]'),
|
||
next_due_date VARCHAR(10) DEFAULT NULL,
|
||
next_due_km 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
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('maintenance_records', 'idx_maint_vehicle_date', '(vehicle_id, maint_date DESC)', 0);
|
||
CALL _add_index_if_missing('maintenance_records', 'idx_maint_date', '(maint_date DESC)', 0);
|
||
CREATE TABLE IF NOT EXISTS refuel_records (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
vehicle_id INT NOT NULL,
|
||
refuel_date VARCHAR(10) NOT NULL,
|
||
odometer_km INT DEFAULT NULL,
|
||
liters DOUBLE NOT NULL,
|
||
price_per_liter DOUBLE DEFAULT NULL,
|
||
total_cost DOUBLE NOT NULL,
|
||
fuel_type VARCHAR(20) DEFAULT NULL,
|
||
is_full TINYINT(1) NOT NULL DEFAULT 0,
|
||
station VARCHAR(255) 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
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('refuel_records', 'idx_refuel_vehicle_date', '(vehicle_id, refuel_date DESC)', 0);
|
||
CALL _add_index_if_missing('refuel_records', 'idx_refuel_date', '(refuel_date DESC)', 0);
|
||
CREATE TABLE IF NOT EXISTS charging_records (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
vehicle_id INT NOT NULL,
|
||
charge_date VARCHAR(10) NOT NULL,
|
||
odometer_km INT DEFAULT NULL,
|
||
kwh DOUBLE NOT NULL,
|
||
price_per_kwh DOUBLE DEFAULT NULL,
|
||
total_cost DOUBLE NOT NULL,
|
||
charge_type VARCHAR(20) DEFAULT NULL,
|
||
start_soc INT DEFAULT NULL,
|
||
end_soc INT DEFAULT NULL,
|
||
station VARCHAR(255) 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
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('charging_records', 'idx_charging_vehicle_date', '(vehicle_id, charge_date DESC)', 0);
|
||
CALL _add_index_if_missing('charging_records', 'idx_charging_date', '(charge_date DESC)', 0);
|
||
DROP VIEW IF EXISTS v_recent_logs;
|
||
CALL _try_sql('CREATE VIEW v_recent_logs AS SELECT ''maintenance'' AS log_type, id, vehicle_id, maint_date AS log_date, total_cost, odometer_km, shop AS location FROM maintenance_records UNION ALL SELECT ''refuel'' AS log_type, id, vehicle_id, refuel_date AS log_date, total_cost, odometer_km, station AS location FROM refuel_records UNION ALL SELECT ''charging'' AS log_type, id, vehicle_id, charge_date AS log_date, total_cost, odometer_km, station AS location FROM charging_records');
|
||
|
||
-- >>> 0008_mileage_and_insurance.sql
|
||
-- ==========================================================
|
||
-- 0008_mileage_and_insurance.sql (MySQL)
|
||
CALL _try_sql('ALTER TABLE maintenance_records ADD COLUMN ev_km INT DEFAULT NULL, ADD COLUMN hev_km INT DEFAULT NULL');
|
||
CREATE TABLE IF NOT EXISTS insurance_records (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
vehicle_id INT NOT NULL,
|
||
insurance_type VARCHAR(50) NOT NULL,
|
||
company VARCHAR(100) DEFAULT NULL,
|
||
policy_no VARCHAR(100) DEFAULT NULL,
|
||
start_date VARCHAR(10) NOT NULL,
|
||
end_date VARCHAR(10) NOT NULL,
|
||
premium DOUBLE DEFAULT NULL,
|
||
coverage_amount DOUBLE DEFAULT NULL,
|
||
notes TEXT DEFAULT NULL,
|
||
attachment_path VARCHAR(500) DEFAULT NULL,
|
||
attachment_name VARCHAR(255) DEFAULT NULL,
|
||
attachment_mime VARCHAR(100) DEFAULT NULL,
|
||
attachment_size INT DEFAULT NULL,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('insurance_records', 'idx_insurance_vehicle', '(vehicle_id)', 0);
|
||
CALL _add_index_if_missing('insurance_records', 'idx_insurance_end_date', '(end_date)', 0);
|
||
|
||
-- >>> 0009_vehicle_powertrain.sql
|
||
-- ==========================================================
|
||
-- 0009_vehicle_powertrain.sql (MySQL)
|
||
CALL _try_sql('ALTER TABLE vehicles ADD COLUMN powertrain VARCHAR(10) NOT NULL DEFAULT ''ice''');
|
||
|
||
-- >>> 0010_operation_logs.sql
|
||
-- ==========================================================
|
||
-- 0010_operation_logs.sql (MySQL)
|
||
CREATE TABLE IF NOT EXISTS operation_logs (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id INT DEFAULT NULL,
|
||
username VARCHAR(50) DEFAULT NULL,
|
||
action VARCHAR(50) NOT NULL,
|
||
target_type VARCHAR(50) NOT NULL,
|
||
target_ids TEXT NOT NULL,
|
||
target_summary TEXT DEFAULT NULL,
|
||
detail_json TEXT DEFAULT NULL,
|
||
ip VARCHAR(45) DEFAULT NULL,
|
||
user_agent VARCHAR(500) DEFAULT NULL,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('operation_logs', 'idx_oplog_created', '(created_at DESC)', 0);
|
||
CALL _add_index_if_missing('operation_logs', 'idx_oplog_user_time', '(username, created_at DESC)', 0);
|
||
CALL _add_index_if_missing('operation_logs', 'idx_oplog_action', '(action, target_type, created_at DESC)', 0);
|
||
|
||
-- >>> 0011_soft_delete.sql
|
||
-- ==========================================================
|
||
-- 0011_soft_delete.sql (MySQL)
|
||
CALL _try_sql('ALTER TABLE vehicles ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0');
|
||
CALL _try_sql('ALTER TABLE wash_records ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0');
|
||
CALL _try_sql('ALTER TABLE chemical_usage ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0');
|
||
CALL _try_sql('ALTER TABLE maintenance_records ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0');
|
||
CALL _try_sql('ALTER TABLE refuel_records ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0');
|
||
CALL _try_sql('ALTER TABLE charging_records ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0');
|
||
CALL _try_sql('ALTER TABLE insurance_records ADD COLUMN is_deleted TINYINT(1) NOT NULL DEFAULT 0');
|
||
CALL _add_index_if_missing('vehicles', 'ix_vehicles_is_deleted', '(is_deleted)', 0);
|
||
CALL _add_index_if_missing('wash_records', 'ix_wash_records_is_deleted', '(is_deleted)', 0);
|
||
CALL _add_index_if_missing('maintenance_records', 'ix_maintenance_is_deleted', '(is_deleted)', 0);
|
||
CALL _add_index_if_missing('refuel_records', 'ix_refuel_is_deleted', '(is_deleted)', 0);
|
||
CALL _add_index_if_missing('charging_records', 'ix_charging_is_deleted', '(is_deleted)', 0);
|
||
CALL _add_index_if_missing('insurance_records', 'ix_insurance_is_deleted', '(is_deleted)', 0);
|
||
|
||
-- >>> 0012_operation_logs_recovery.sql
|
||
-- ==========================================================
|
||
-- 0012_operation_logs_recovery.sql (MySQL)
|
||
CALL _try_sql('ALTER TABLE operation_logs ADD COLUMN recovered_at DATETIME DEFAULT NULL');
|
||
|
||
-- >>> 0013_weather_wttr.sql
|
||
-- ==========================================================
|
||
-- 0013_weather_wttr.sql (MySQL)
|
||
-- 删除旧 CHECK 并重建(MySQL 允许 ALTER TABLE 改 CHECK,但为保险用 ALTER COLUMN)
|
||
CALL _try_sql('ALTER TABLE weather_snapshots MODIFY COLUMN provider VARCHAR(50) NOT NULL');
|
||
|
||
-- >>> 0014_grocy_auth.sql
|
||
-- ==========================================================
|
||
-- 0014_grocy_auth.sql (MySQL)
|
||
INSERT IGNORE INTO settings (`key`, value, is_secret, description) VALUES
|
||
('grocy_username', '', 1, 'Grocy 用户名(session cookie 鉴权)'),
|
||
('grocy_password', '', 1, 'Grocy 密码(session cookie 鉴权)'),
|
||
('app_city_default', '', 0, '天气默认城市(永久生效)');
|
||
CREATE TABLE IF NOT EXISTS grocy_sync_logs (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
action VARCHAR(50) NOT NULL,
|
||
status VARCHAR(20) NOT NULL,
|
||
ok_count INT NOT NULL DEFAULT 0,
|
||
fail_count INT NOT NULL DEFAULT 0,
|
||
detail TEXT DEFAULT NULL,
|
||
started_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
finished_at DATETIME DEFAULT NULL
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
CALL _add_index_if_missing('grocy_sync_logs', 'idx_grocy_sync_logs_action', '(action)', 0);
|
||
CALL _add_index_if_missing('grocy_sync_logs', 'idx_grocy_sync_logs_started', '(started_at DESC)', 0);
|
||
|
||
-- >>> 0015_wash_photos.sql
|
||
-- ==========================================================
|
||
-- 0015_wash_photos.sql (MySQL) — 洗车对比照(before / after / detail)
|
||
CREATE TABLE IF NOT EXISTS wash_photos (
|
||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||
wash_id INT NOT NULL,
|
||
photo_type VARCHAR(20) NOT NULL DEFAULT 'detail', -- before / after / detail / scene
|
||
file_path VARCHAR(500) NOT NULL,
|
||
file_name VARCHAR(255) NOT NULL,
|
||
mime_type VARCHAR(50) DEFAULT NULL,
|
||
file_size INT DEFAULT NULL,
|
||
width INT DEFAULT NULL,
|
||
height INT DEFAULT NULL,
|
||
caption VARCHAR(255) DEFAULT NULL,
|
||
sort_order INT NOT NULL DEFAULT 0,
|
||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||
is_deleted TINYINT(1) NOT NULL DEFAULT 0,
|
||
INDEX idx_wash_photos_wash (wash_id, is_deleted),
|
||
INDEX idx_wash_photos_type (photo_type),
|
||
INDEX idx_wash_photos_created (created_at DESC)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
DROP PROCEDURE IF EXISTS _add_index_if_missing;
|
||
DROP PROCEDURE IF EXISTS _try_sql;
|
||
|
||
CREATE TABLE IF NOT EXISTS schema_migrations (filename VARCHAR(255) PRIMARY KEY, applied_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0001_init.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0002_auth.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0003_vehicles.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0004_grocy_full.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0005_inventory_detail.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0006_unit_conversion.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0007_vehicle_logs.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0008_mileage_and_insurance.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0009_vehicle_powertrain.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0010_operation_logs.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0011_soft_delete.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0012_operation_logs_recovery.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0013_weather_wttr.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0014_grocy_auth.sql');
|
||
INSERT IGNORE INTO schema_migrations (filename) VALUES ('0015_wash_photos.sql');
|