-- Схема БД проекта «Бюджет» (MariaDB/InnoDB). -- Модель согласована в .claude/memory/бюджет_текущий_план.md — сверяться туда при любых изменениях. SET NAMES utf8mb4; CREATE TABLE users ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, login VARCHAR(64) NOT NULL, password VARCHAR(255) NOT NULL, name VARCHAR(128) NOT NULL, email VARCHAR(128) NULL, date_add DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, last_login DATETIME NULL, PRIMARY KEY (id), UNIQUE KEY uq_users_login (login) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; CREATE TABLE accounts ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL, type_acc ENUM ('cash', 'bank', 'savings') NOT NULL, title VARCHAR(128) NOT NULL, summa DECIMAL(15, 2) NOT NULL DEFAULT 0, currency CHAR(3) NOT NULL DEFAULT 'RUB', -- ISO 4217; курс — Services\CurrencyRate (ЦБ РФ), не хранится тут description VARCHAR(255) NULL, -- свободное поле, например "счёт жены" — без отдельной структуры color VARCHAR(20) NULL, -- пользовательский акцент счёта (oklch(...) строкой); NULL — цвет по умолчанию для type_acc privacy ENUM ('shared', 'personal') NOT NULL DEFAULT 'shared', -- визуальный признак из дизайн-хендоффа; логики доступа за ним нет (мультипользовательский режим не в приоритете) include_in_total TINYINT(1) NOT NULL DEFAULT 1, -- чисто ручной переключатель пользователя, не системное правило по типу/валюте (дефолт 0 для savings — просто стартовое значение) is_delete TINYINT(1) NOT NULL DEFAULT 0, `order` INT NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY ix_accounts_user_id (user_id), CONSTRAINT fk_accounts_user FOREIGN KEY (user_id) REFERENCES users (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- Ставка savings-счёта хранится историей, а не полем в accounts — старые периоды не должны -- меняться задним числом при смене ставки; та же структура пригодится для кредитов позже. CREATE TABLE account_rates ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, account_id INT UNSIGNED NOT NULL, rate_percent DECIMAL(5, 2) NOT NULL, valid_from DATE NOT NULL, valid_to DATE NULL, PRIMARY KEY (id), KEY ix_account_rates_account_id (account_id), CONSTRAINT fk_account_rates_account FOREIGN KEY (account_id) REFERENCES accounts (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- Необязательная надстройка над savings-счётом: без записи здесь — просто копилка без цели. CREATE TABLE goals ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, account_id INT UNSIGNED NOT NULL, target_summa DECIMAL(15, 2) NOT NULL, deadline DATE NOT NULL, PRIMARY KEY (id), UNIQUE KEY uq_goals_account_id (account_id), CONSTRAINT fk_goals_account FOREIGN KEY (account_id) REFERENCES accounts (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- Группа статей — самостоятельная сущность, не статья с детьми. Раньше группой считалась -- обычная строка categories, на которую другие строки ссылались через parent_id — это позволяло -- одной и той же записи «случайно» становиться то листом (со своим планом в budgets), то группой, -- и при добавлении первого ребёнка старый план листа не чистился, оставаясь мусором в budgets. -- Разделение на две таблицы делает такое структурно невозможным: budgets.categorie_id может -- указывать только на categories (см. ниже), у group_id там просто нет своей строки в budgets. CREATE TABLE category_groups ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL, title VARCHAR(128) NOT NULL, type ENUM ('income', 'expense') NOT NULL, is_delete TINYINT(1) NOT NULL DEFAULT 0, `order` INT NOT NULL DEFAULT 0, PRIMARY KEY (id), KEY ix_category_groups_user_id (user_id), CONSTRAINT fk_category_groups_user FOREIGN KEY (user_id) REFERENCES users (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; CREATE TABLE categories ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL, group_id INT UNSIGNED NULL, -- NULL — статья без группы (верхний уровень), см. category_groups title VARCHAR(128) NOT NULL, type ENUM ('income', 'expense') NOT NULL, is_delete TINYINT(1) NOT NULL DEFAULT 0, -- архивация вместо удаления, тот же приём что у accounts — сохраняет историю транзакций `order` INT NOT NULL DEFAULT 0, -- формульные статьи (см. бюджет_текущий_план.md): настраиваются на статье ДОХОДА, ссылаются на -- привязанную статью РАСХОДА + дефолтный процент; у остальных категорий оба поля NULL linked_expense_categorie_id INT UNSIGNED NULL, formula_pct DECIMAL(5, 2) NULL, PRIMARY KEY (id), KEY ix_categories_user_id (user_id), KEY ix_categories_group_id (group_id), CONSTRAINT fk_categories_user FOREIGN KEY (user_id) REFERENCES users (id), -- удаление группы не должно тянуть за собой статьи — они просто станут без группы CONSTRAINT fk_categories_group FOREIGN KEY (group_id) REFERENCES category_groups (id) ON DELETE SET NULL, CONSTRAINT fk_categories_linked_expense FOREIGN KEY (linked_expense_categorie_id) REFERENCES categories (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- categorie_id NULL — обычный transfer (в т.ч. пополнение копилки/цели): деньги просто -- переехали между своими счетами, план/факт по категориям это не видит (см. бюджет_текущий_план.md). -- account_f_id/account_in_id: income заполняет только _in, expense — только _f, transfer — оба. CREATE TABLE transactions ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL, categorie_id INT UNSIGNED NULL, account_f_id INT UNSIGNED NULL, account_in_id INT UNSIGNED NULL, date DATE NOT NULL, summa DECIMAL(15, 2) NOT NULL, currency CHAR(3) NULL, -- валюта операции (обычно = валюте счёта списания); NULL = рубли rate_to_rub DECIMAL(10, 4) NULL, -- курс ЦБ РФ, зафиксированный на момент операции; NULL для рублёвых type ENUM ('income', 'expense', 'transfer') NOT NULL, PRIMARY KEY (id), KEY ix_transactions_user_date (user_id, date), KEY ix_transactions_categorie_id (categorie_id), KEY ix_transactions_account_f_id (account_f_id), KEY ix_transactions_account_in_id (account_in_id), CONSTRAINT fk_transactions_user FOREIGN KEY (user_id) REFERENCES users (id), CONSTRAINT fk_transactions_categorie FOREIGN KEY (categorie_id) REFERENCES categories (id), CONSTRAINT fk_transactions_account_f FOREIGN KEY (account_f_id) REFERENCES accounts (id), CONSTRAINT fk_transactions_account_in FOREIGN KEY (account_in_id) REFERENCES accounts (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- Лог правок/удалений transactions — цепочка снимков (не old/new-пары), достаточно для отката: -- откат edit — snapshot предыдущей записи этой же transaction_id; восстановление после delete — -- snapshot записи, предшествующей записи delete (сама delete-запись snapshot не хранит); откат -- create — просто удалить операцию, snapshot не нужен. Механизм самого отката — не реализован, -- здесь только лог. CREATE TABLE transaction_history ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, transaction_id INT UNSIGNED NOT NULL, user_id INT UNSIGNED NOT NULL, changed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, action ENUM ('create', 'edit', 'delete') NOT NULL, snapshot JSON NULL, -- полный слепок transactions на этот момент; NULL для action='delete' PRIMARY KEY (id), -- без FK на transactions.id намеренно: после физического удаления операции строка transactions -- пропадает, а история по ней должна остаться читаемой KEY ix_transaction_history_transaction_id (transaction_id), CONSTRAINT fk_transaction_history_user FOREIGN KEY (user_id) REFERENCES users (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- Факт не хранится — считается на лету суммой transactions по категории за месяц. -- plan_summ хранится всегда (и у формульных строк тоже) — у формульных пересчитывается и -- перезаписывается автоматически при правке суммы/процента статьи-источника (кэш, как accounts.summa). CREATE TABLE budgets ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, categorie_id INT UNSIGNED NOT NULL, plan_summ DECIMAL(15, 2) NOT NULL, year SMALLINT UNSIGNED NOT NULL, month TINYINT UNSIGNED NOT NULL, type ENUM ('income', 'expense') NOT NULL, -- каким процентом посчитан plan_summ этого месяца — только для строк формульной статьи расхода, -- NULL для обычных строк (см. категорию categories.linked_expense_categorie_id) formula_pct_used DECIMAL(5, 2) NULL, PRIMARY KEY (id), UNIQUE KEY uq_budgets_categorie_period (categorie_id, year, month), CONSTRAINT fk_budgets_categorie FOREIGN KEY (categorie_id) REFERENCES categories (id) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_unicode_ci;