]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
feat(ERP-292): AutoMarkService — llmBatchVerify, getPendingUnverifiedCount
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Thu, 30 Apr 2026 15:23:56 +0000 (18:23 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Thu, 30 Apr 2026 15:23:56 +0000 (18:23 +0300)
erp24/services/AutoMarkService.php

index fe5985dce4e8a75bfe2742033fa600d6832df180..b7b8c05b1a8f35bad659db5fdd5d526c0db78fb9 100644 (file)
@@ -8,6 +8,7 @@ use Yii;
 use yii_app\records\Products1c;
 use yii_app\records\Products1cAutomarkPrediction;
 use yii_app\records\Products1cNomenclature;
+use yii_app\services\automark\LlmVerifier;
 use yii_app\services\automark\ParseResult;
 use yii_app\services\automark\RuleBasedParser;
 use yii_app\services\automark\SimilarityMatcher;
@@ -17,6 +18,22 @@ class AutoMarkService
     public const RULE_THRESHOLD       = 0.9;
     public const SIMILARITY_THRESHOLD = 0.7;
 
+    private const EXCLUDED_TYPES = [
+        '[букет]',
+        '[сборка]',
+        'Матрица',
+        'Авторский букет (вид с сериями)',
+        'Маркетплейсы',
+        'Не использовать',
+        'Статьи затрат',
+        'Основные средства',
+        'Комиссия',
+        'Услуга',
+        'Доставка по России',
+        'Транспорт Амстердам',
+        'Сырье, материалы',
+    ];
+
     private RuleBasedParser   $ruleParser;
     private SimilarityMatcher $matcher;
 
@@ -72,9 +89,9 @@ class AutoMarkService
                     $transaction->rollBack();
                     return false;
                 }
-                $nomenclature          = new Products1cNomenclature();
-                $nomenclature->id      = $prediction->product_id;
-                $nomenclature->name    = $product->name;
+                $nomenclature           = new Products1cNomenclature();
+                $nomenclature->id       = $prediction->product_id;
+                $nomenclature->name     = $product->name;
                 $nomenclature->location = '';
                 $nomenclature->type_num = '';
             }
@@ -87,7 +104,13 @@ class AutoMarkService
             $nomenclature->size        = $prediction->size        ?? $nomenclature->size;
             $nomenclature->color       = $prediction->color       ?? $nomenclature->color;
 
-            if (!$nomenclature->save()) {
+            $nomenclature->classification_status = 'classified';
+            $nomenclature->confidence            = (int) round($prediction->confidence * 100);
+            $nomenclature->classified_by         = $prediction->approved_by;
+            $nomenclature->classified_at         = date('Y-m-d H:i:s');
+
+            // save(false): location/type_num могут быть '', required-валидатор для форм здесь неприменим
+            if (!$nomenclature->save(false)) {
                 $transaction->rollBack();
                 return false;
             }
@@ -130,12 +153,36 @@ class AutoMarkService
         $corpus    = $this->loadCorpus();
         $simResult = $this->matcher->findBestMatch($name, $corpus);
         if ($simResult !== null && $simResult->confidence >= self::SIMILARITY_THRESHOLD) {
-            return $simResult;
+            return $this->mergeRuleIntoSim($ruleResult, $simResult);
         }
 
         return $ruleResult ?? $simResult;
     }
 
+    /**
+     * Заполнить пустые поля similarity-результата данными из rule-парсера.
+     * Corpus может содержать товары с частично пустой номенклатурой —
+     * rule-парсер извлекает size/color напрямую из имени нового товара.
+     */
+    private function mergeRuleIntoSim(?ParseResult $rule, ParseResult $sim): ParseResult
+    {
+        if ($rule === null) {
+            return $sim;
+        }
+
+        return new ParseResult(
+            category:    $sim->category    ?? $rule->category,
+            subcategory: $sim->subcategory ?? $rule->subcategory,
+            species:     $sim->species     ?? $rule->species,
+            sort:        $sim->sort        ?? $rule->sort,
+            type:        $sim->type        ?? $rule->type,
+            size:        ($sim->size === null || $sim->size === 0) ? $rule->size : $sim->size,
+            color:       $sim->color       ?? $rule->color,
+            confidence:  $sim->confidence,
+            method:      $sim->method,
+        );
+    }
+
     private function savePrediction(string $productId, ParseResult $result): ?Products1cAutomarkPrediction
     {
         $model = new Products1cAutomarkPrediction();
@@ -170,6 +217,33 @@ class AutoMarkService
             ->leftJoin('products_1c_nomenclature', 'products_1c.id = products_1c_nomenclature.id')
             ->where(['products_1c_nomenclature.id' => null])
             ->andWhere(['products_1c.tip' => Products1c::TYPE_PRODUCTS])
+            ->andWhere(['OR',
+                ['products_1c.type' => null],
+                ['not in', 'products_1c.type', self::EXCLUDED_TYPES],
+            ])
             ->column();
     }
+
+    /**
+     * Верифицировать pending-предсказания через LLM.
+     *
+     * @return int Количество верифицированных предсказаний
+     */
+    public function llmBatchVerify(): int
+    {
+        $batchSize = (int)(getenv('LLM_BATCH_SIZE') ?: 20);
+        $verifier  = new LlmVerifier($batchSize);
+        return $verifier->verifyPending();
+    }
+
+    /**
+     * Количество pending-предсказаний, ещё не прошедших LLM-верификацию.
+     */
+    public function getPendingUnverifiedCount(): int
+    {
+        return (int) Products1cAutomarkPrediction::find()
+            ->where(['status' => Products1cAutomarkPrediction::STATUS_PENDING])
+            ->andWhere(['llm_verdict' => null])
+            ->count();
+    }
 }