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:
@@ -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;
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
-- +goose Up
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 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);
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 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', 'уп');
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 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', 'Прочее');
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 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', 'Другая');
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 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', 'Ферментированное');
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- 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
|
||||
DELETE FROM dish_category_translations;
|
||||
DELETE FROM dish_categories;
|
||||
DELETE FROM tag_translations;
|
||||
DELETE FROM tags;
|
||||
DELETE FROM cuisine_translations;
|
||||
DELETE FROM cuisines;
|
||||
DELETE FROM ingredient_category_translations;
|
||||
DELETE FROM ingredient_categories;
|
||||
DELETE FROM unit_translations;
|
||||
DELETE FROM units;
|
||||
DELETE FROM languages;
|
||||
Reference in New Issue
Block a user