feat: 洗车管理系统 v2.8 — 个人 detailer 单用户全栈应用
- 车辆 / 洗车 / 加油 / 充电 / 保养 / 保险 完整 CRUD + 软删 - AI 截图识别(5 类型 OCR schema):OpenAI 兼容 + MiniMax M3 - 化学品 / Grocy 实例对接 + 库存镜像同步 - 仪表盘:30 天频次 + 健康度 + 同比环比 + 油价趋势 + 年均养护 - 月度报表:Excel 6 sheet + PDF - PWA:manifest / SW / 离线缓存 / iOS 引导 - 安全:bcrypt + CSRF + 登录锁定(IP/用户/全局三级)+ 401 自动跳登录 + 表单草稿 - 高 ROI 8 功能:里程/提醒/成本/搜索/标签/通知/同比/成就 - 3 个新 migration(0016/0017/0018)+ 18 个迁移全幂等 - 101/101 测试通过(含 ipRateLimit / CSRF / retry / stats / tags / notifications) - 部署:宝塔面板文档 + PM2 + Nginx
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
-- =============================================================================
|
||||
-- 洗车记录系统 - 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;
|
||||
Reference in New Issue
Block a user