refactor: migrate ingredient aliases/categories to dedicated tables, drop spoonacular_id
- migration 012: create ingredient_categories + ingredient_category_translations tables (7 slugs, Russian names); add ingredient_aliases (ingredient_id, lang, alias) with GIN trigram index; migrate aliases JSONB from ingredient_mappings and ingredient_translations; drop aliases columns and spoonacular_id; add UNIQUE (canonical_name) as conflict key - ingredient/model: remove SpoonacularID, add CategoryName for localized display - ingredient/repository: conflict on canonical_name; GetByID/Search join category translations and aliases lateral; new UpsertAliases (pgx batch), UpsertCategoryTranslation; remove GetBySpoonacularID; split scan helpers into scanMappingWrite / scanMappingRead - gemini/recognition: add IngredientTranslation type; IngredientClassification now carries Translations []IngredientTranslation instead of CanonicalNameRu; update ClassifyIngredient prompt to English with structured translations array - recognition/handler: update ingredientRepo interface; saveClassification uses UpsertAliases and iterates Translations - recipe/model: remove SpoonacularID from RecipeIngredient - integration tests: remove SpoonacularID fixtures, replace GetBySpoonacularID tests with GetByID, add UpsertAliases and UpsertCategoryTranslation tests - Flutter: remove canonicalNameRu from IngredientMapping, add categoryName; displayName returns server-resolved canonicalName; regenerate .g.dart Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -40,18 +40,25 @@ type DishResult struct {
|
||||
SimilarDishes []string `json:"similar_dishes"`
|
||||
}
|
||||
|
||||
// IngredientTranslation holds the localized name and aliases for one language.
|
||||
type IngredientTranslation struct {
|
||||
Lang string `json:"lang"`
|
||||
Name string `json:"name"`
|
||||
Aliases []string `json:"aliases"`
|
||||
}
|
||||
|
||||
// IngredientClassification is the AI-produced classification of an unknown food item.
|
||||
type IngredientClassification struct {
|
||||
CanonicalName string `json:"canonical_name"`
|
||||
CanonicalNameRu string `json:"canonical_name_ru"`
|
||||
Category string `json:"category"`
|
||||
DefaultUnit string `json:"default_unit"`
|
||||
CaloriesPer100g *float64 `json:"calories_per_100g"`
|
||||
ProteinPer100g *float64 `json:"protein_per_100g"`
|
||||
FatPer100g *float64 `json:"fat_per_100g"`
|
||||
CarbsPer100g *float64 `json:"carbs_per_100g"`
|
||||
StorageDays int `json:"storage_days"`
|
||||
Aliases []string `json:"aliases"`
|
||||
CanonicalName string `json:"canonical_name"`
|
||||
Aliases []string `json:"aliases"` // English aliases
|
||||
Translations []IngredientTranslation `json:"translations"` // other languages
|
||||
Category string `json:"category"`
|
||||
DefaultUnit string `json:"default_unit"`
|
||||
CaloriesPer100g *float64 `json:"calories_per_100g"`
|
||||
ProteinPer100g *float64 `json:"protein_per_100g"`
|
||||
FatPer100g *float64 `json:"fat_per_100g"`
|
||||
CarbsPer100g *float64 `json:"carbs_per_100g"`
|
||||
StorageDays int `json:"storage_days"`
|
||||
}
|
||||
|
||||
// RecognizeReceipt uses the vision model to extract food items from a receipt photo.
|
||||
@@ -177,20 +184,21 @@ func (c *Client) RecognizeDish(ctx context.Context, imageBase64, mimeType string
|
||||
// ClassifyIngredient uses the text model to classify an unknown food item
|
||||
// and build an ingredient_mappings record for it.
|
||||
func (c *Client) ClassifyIngredient(ctx context.Context, name string) (*IngredientClassification, error) {
|
||||
prompt := fmt.Sprintf(`Классифицируй продукт питания: "%s".
|
||||
|
||||
Ответь ТОЛЬКО валидным JSON без markdown:
|
||||
prompt := fmt.Sprintf(`Classify the food product: "%s".
|
||||
Return ONLY valid JSON without markdown:
|
||||
{
|
||||
"canonical_name": "turkey_breast",
|
||||
"canonical_name_ru": "грудка индейки",
|
||||
"aliases": ["turkey breast"],
|
||||
"translations": [
|
||||
{"lang": "ru", "name": "грудка индейки", "aliases": ["грудка индейки", "филе индейки"]}
|
||||
],
|
||||
"category": "meat",
|
||||
"default_unit": "g",
|
||||
"calories_per_100g": 135,
|
||||
"protein_per_100g": 29,
|
||||
"fat_per_100g": 1,
|
||||
"carbs_per_100g": 0,
|
||||
"storage_days": 3,
|
||||
"aliases": ["грудка индейки", "филе индейки", "turkey breast"]
|
||||
"storage_days": 3
|
||||
}`, name)
|
||||
|
||||
messages := []map[string]string{
|
||||
|
||||
Reference in New Issue
Block a user