feat: async dish recognition (Kafka/Watermill/SSE) + remove Wire + consolidate migrations

Async recognition pipeline:
- POST /ai/recognize-dish → 202 {job_id, queue_position, estimated_seconds}
- GET /ai/jobs/{id}/stream — SSE stream: queued → processing → done/failed
- Kafka topics: ai.recognize.paid (3 partitions) + ai.recognize.free (1 partition)
- 5-worker WorkerPool with priority loop (paid consumers first)
- SSEBroker via PostgreSQL LISTEN/NOTIFY
- Kafka adapter migrated from franz-go to Watermill (watermill-kafka/v2)
- Docker Compose: added Kafka + Zookeeper + kafka-init service
- Flutter: recognition_service.dart uses SSE; home_screen shows live job status

Remove google/wire (archived):
- Deleted wire.go (wireinject spec) and wire_gen.go
- Added cmd/server/init.go — plain Go manual DI, same initApp() logic
- Removed github.com/google/wire from go.mod

Consolidate migrations:
- Merged 001_initial_schema + 002_seed_data + 003_recognition_jobs into single 001_initial_schema.sql
- Deleted 002_seed_data.sql and 003_recognition_jobs.sql

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dbastrikin
2026-03-18 16:32:06 +02:00
parent ad00998344
commit 39193ec13c
22 changed files with 1574 additions and 582 deletions

View File

@@ -119,7 +119,7 @@ CREATE TABLE ingredient_category_translations (
);
-- ---------------------------------------------------------------------------
-- ingredients (canonical catalog — formerly ingredient_mappings)
-- ingredients (canonical catalog)
-- ---------------------------------------------------------------------------
CREATE TABLE ingredients (
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
@@ -151,7 +151,7 @@ CREATE TABLE ingredient_translations (
CREATE INDEX idx_ingredient_translations_ingredient_id ON ingredient_translations (ingredient_id);
-- ---------------------------------------------------------------------------
-- ingredient_aliases (relational, replaces JSONB aliases column)
-- ingredient_aliases
-- ---------------------------------------------------------------------------
CREATE TABLE ingredient_aliases (
ingredient_id UUID NOT NULL REFERENCES ingredients(id) ON DELETE CASCADE,
@@ -269,7 +269,7 @@ CREATE INDEX idx_recipes_calories ON recipes (calories_per_serving);
CREATE INDEX idx_recipes_source ON recipes (source);
-- ---------------------------------------------------------------------------
-- recipe_translations (per-language cooking notes only)
-- recipe_translations
-- ---------------------------------------------------------------------------
CREATE TABLE recipe_translations (
recipe_id UUID NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
@@ -348,7 +348,7 @@ CREATE TABLE product_ingredients (
);
-- ---------------------------------------------------------------------------
-- user_saved_recipes (thin bookmark — content lives in dishes + recipes)
-- user_saved_recipes
-- ---------------------------------------------------------------------------
CREATE TABLE user_saved_recipes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
@@ -407,7 +407,205 @@ CREATE TABLE meal_diary (
);
CREATE INDEX idx_meal_diary_user_date ON meal_diary (user_id, date);
-- ---------------------------------------------------------------------------
-- recognition_jobs
-- ---------------------------------------------------------------------------
CREATE TABLE recognition_jobs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v7(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
user_plan TEXT NOT NULL,
image_base64 TEXT NOT NULL,
mime_type TEXT NOT NULL DEFAULT 'image/jpeg',
lang TEXT NOT NULL DEFAULT 'en',
status TEXT NOT NULL DEFAULT 'pending',
-- pending | processing | done | failed
result JSONB,
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ
);
CREATE INDEX idx_recognition_jobs_user ON recognition_jobs (user_id, created_at DESC);
CREATE INDEX idx_recognition_jobs_pending ON recognition_jobs (status, user_plan, created_at ASC);
-- ---------------------------------------------------------------------------
-- Seed data: languages
-- ---------------------------------------------------------------------------
INSERT INTO languages (code, native_name, english_name, sort_order) VALUES
('en', 'English', 'English', 1),
('ru', 'Русский', 'Russian', 2),
('es', 'Español', 'Spanish', 3),
('de', 'Deutsch', 'German', 4),
('fr', 'Français', 'French', 5),
('it', 'Italiano', 'Italian', 6),
('pt', 'Português', 'Portuguese', 7),
('zh', '中文', 'Chinese (Simplified)', 8),
('ja', '日本語', 'Japanese', 9),
('ko', '한국어', 'Korean', 10),
('ar', 'العربية', 'Arabic', 11),
('hi', 'हिन्दी', 'Hindi', 12);
-- ---------------------------------------------------------------------------
-- Seed data: units + unit_translations
-- ---------------------------------------------------------------------------
INSERT INTO units (code, sort_order) VALUES
('g', 1),
('kg', 2),
('ml', 3),
('l', 4),
('pcs', 5),
('pack', 6);
INSERT INTO unit_translations (unit_code, lang, name) VALUES
('g', 'ru', 'г'),
('kg', 'ru', 'кг'),
('ml', 'ru', 'мл'),
('l', 'ru', 'л'),
('pcs', 'ru', 'шт'),
('pack', 'ru', 'уп');
-- ---------------------------------------------------------------------------
-- Seed data: ingredient_categories + ingredient_category_translations
-- ---------------------------------------------------------------------------
INSERT INTO ingredient_categories (slug, sort_order) VALUES
('dairy', 1),
('meat', 2),
('produce', 3),
('bakery', 4),
('frozen', 5),
('beverages', 6),
('other', 7);
INSERT INTO ingredient_category_translations (category_slug, lang, name) VALUES
('dairy', 'ru', 'Молочные продукты'),
('meat', 'ru', 'Мясо и птица'),
('produce', 'ru', 'Овощи и фрукты'),
('bakery', 'ru', 'Выпечка и хлеб'),
('frozen', 'ru', 'Замороженные'),
('beverages', 'ru', 'Напитки'),
('other', 'ru', 'Прочее');
-- ---------------------------------------------------------------------------
-- Seed data: cuisines + cuisine_translations
-- ---------------------------------------------------------------------------
INSERT INTO cuisines (slug, name, sort_order) VALUES
('italian', 'Italian', 1),
('french', 'French', 2),
('russian', 'Russian', 3),
('chinese', 'Chinese', 4),
('japanese', 'Japanese', 5),
('korean', 'Korean', 6),
('mexican', 'Mexican', 7),
('mediterranean', 'Mediterranean', 8),
('indian', 'Indian', 9),
('thai', 'Thai', 10),
('american', 'American', 11),
('georgian', 'Georgian', 12),
('spanish', 'Spanish', 13),
('german', 'German', 14),
('middle_eastern', 'Middle Eastern', 15),
('turkish', 'Turkish', 16),
('greek', 'Greek', 17),
('vietnamese', 'Vietnamese', 18),
('other', 'Other', 19);
INSERT INTO cuisine_translations (cuisine_slug, lang, name) VALUES
('italian', 'ru', 'Итальянская'),
('french', 'ru', 'Французская'),
('russian', 'ru', 'Русская'),
('chinese', 'ru', 'Китайская'),
('japanese', 'ru', 'Японская'),
('korean', 'ru', 'Корейская'),
('mexican', 'ru', 'Мексиканская'),
('mediterranean', 'ru', 'Средиземноморская'),
('indian', 'ru', 'Индийская'),
('thai', 'ru', 'Тайская'),
('american', 'ru', 'Американская'),
('georgian', 'ru', 'Грузинская'),
('spanish', 'ru', 'Испанская'),
('german', 'ru', 'Немецкая'),
('middle_eastern', 'ru', 'Ближневосточная'),
('turkish', 'ru', 'Турецкая'),
('greek', 'ru', 'Греческая'),
('vietnamese', 'ru', 'Вьетнамская'),
('other', 'ru', 'Другая');
-- ---------------------------------------------------------------------------
-- Seed data: tags + tag_translations
-- ---------------------------------------------------------------------------
INSERT INTO tags (slug, name, sort_order) VALUES
('vegan', 'Vegan', 1),
('vegetarian', 'Vegetarian', 2),
('gluten_free', 'Gluten-Free', 3),
('dairy_free', 'Dairy-Free', 4),
('healthy', 'Healthy', 5),
('quick', 'Quick', 6),
('spicy', 'Spicy', 7),
('sweet', 'Sweet', 8),
('soup', 'Soup', 9),
('salad', 'Salad', 10),
('main_course', 'Main Course', 11),
('appetizer', 'Appetizer', 12),
('breakfast', 'Breakfast', 13),
('dessert', 'Dessert', 14),
('grilled', 'Grilled', 15),
('baked', 'Baked', 16),
('fried', 'Fried', 17),
('raw', 'Raw', 18),
('fermented', 'Fermented', 19);
INSERT INTO tag_translations (tag_slug, lang, name) VALUES
('vegan', 'ru', 'Веганское'),
('vegetarian', 'ru', 'Вегетарианское'),
('gluten_free', 'ru', 'Без глютена'),
('dairy_free', 'ru', 'Без молока'),
('healthy', 'ru', 'Здоровое'),
('quick', 'ru', 'Быстрое'),
('spicy', 'ru', 'Острое'),
('sweet', 'ru', 'Сладкое'),
('soup', 'ru', 'Суп'),
('salad', 'ru', 'Салат'),
('main_course', 'ru', 'Основное блюдо'),
('appetizer', 'ru', 'Закуска'),
('breakfast', 'ru', 'Завтрак'),
('dessert', 'ru', 'Десерт'),
('grilled', 'ru', 'Жареное на гриле'),
('baked', 'ru', 'Запечённое'),
('fried', 'ru', 'Жареное'),
('raw', 'ru', 'Сырое'),
('fermented', 'ru', 'Ферментированное');
-- ---------------------------------------------------------------------------
-- Seed data: dish_categories + dish_category_translations
-- ---------------------------------------------------------------------------
INSERT INTO dish_categories (slug, name, sort_order) VALUES
('soup', 'Soup', 1),
('salad', 'Salad', 2),
('main_course', 'Main Course', 3),
('side_dish', 'Side Dish', 4),
('appetizer', 'Appetizer', 5),
('dessert', 'Dessert', 6),
('breakfast', 'Breakfast', 7),
('drink', 'Drink', 8),
('bread', 'Bread', 9),
('sauce', 'Sauce', 10),
('snack', 'Snack', 11);
INSERT INTO dish_category_translations (category_slug, lang, name) VALUES
('soup', 'ru', 'Суп'),
('salad', 'ru', 'Салат'),
('main_course', 'ru', 'Основное блюдо'),
('side_dish', 'ru', 'Гарнир'),
('appetizer', 'ru', 'Закуска'),
('dessert', 'ru', 'Десерт'),
('breakfast', 'ru', 'Завтрак'),
('drink', 'ru', 'Напиток'),
('bread', 'ru', 'Выпечка'),
('sauce', 'ru', 'Соус'),
('snack', 'ru', 'Снэк');
-- +goose Down
DROP TABLE IF EXISTS recognition_jobs;
DROP TABLE IF EXISTS meal_diary;
DROP TABLE IF EXISTS shopping_lists;
DROP TABLE IF EXISTS menu_items;
@@ -447,3 +645,4 @@ DROP TYPE IF EXISTS user_gender;
DROP TYPE IF EXISTS user_plan;
DROP FUNCTION IF EXISTS uuid_generate_v7();
DROP EXTENSION IF EXISTS pg_trgm;
DROP EXTENSION IF EXISTS pgcrypto;