]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
review fixes 2 feature_fomichev_ERP-292_automarkup origin/feature_fomichev_ERP-292_automarkup
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 6 May 2026 08:16:35 +0000 (11:16 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 6 May 2026 08:16:35 +0000 (11:16 +0300)
erp24/controllers/AutoMarkController.php
erp24/jobs/AutoMarkPredictionJob.php
erp24/migrations/m260506_120000_add_automark_rbac_permissions.php
erp24/services/AutoMarkService.php
erp24/services/automark/CoordinatorLlmTransport.php
erp24/services/automark/LlmVerifier.php
erp24/services/automark/OpenAiCompatibleLlmTransport.php
erp24/services/automark/RuleBasedParser.php
erp24/views/auto-mark/index.php
erp24/views/auto-mark/review.php

index 4fbc6e086a0e506655ab747feb946fff288cfebf..41a4cee6f34c6dde17ca894d320a9522d19eddf3 100644 (file)
@@ -74,26 +74,30 @@ class AutoMarkController extends Controller
         $prediction = $this->findPrediction($id);
 
         if (Yii::$app->request->isPost) {
+            if (!$prediction->isPending()) {
+                Yii::$app->session->setFlash('error', 'Предсказание уже обработано.');
+                return $this->redirect(['index']);
+            }
+
             $action = Yii::$app->request->post('action');
             $service = new AutoMarkService();
 
             if ($action === 'approve') {
-                $transaction = \Yii::$app->db->beginTransaction();
-                try {
-                    $prediction->status      = Products1cAutomarkPrediction::STATUS_APPROVED;
-                    $prediction->approved_by = Yii::$app->user->id;
-                    $prediction->updated_at  = date('Y-m-d H:i:s');
-                    if (!$prediction->save()) {
-                        $transaction->rollBack();
-                        Yii::$app->session->setFlash('error', 'Не удалось сохранить предсказание.');
-                    } else {
-                        $service->applyApprovedPrediction($prediction->id);
-                        $transaction->commit();
-                        Yii::$app->session->setFlash('success', 'Разметка применена.');
+                $prediction->status      = Products1cAutomarkPrediction::STATUS_APPROVED;
+                $prediction->approved_by = Yii::$app->user->id;
+                $prediction->updated_at  = date('Y-m-d H:i:s');
+                if (!$prediction->save()) {
+                    Yii::$app->session->setFlash('error', 'Не удалось сохранить предсказание.');
+                } else {
+                    try {
+                        if ($service->applyApprovedPrediction($prediction->id)) {
+                            Yii::$app->session->setFlash('success', 'Разметка применена.');
+                        } else {
+                            Yii::$app->session->setFlash('error', 'Не удалось применить разметку в номенклатуру.');
+                        }
+                    } catch (\Exception) {
+                        Yii::$app->session->setFlash('error', 'Ошибка при применении разметки.');
                     }
-                } catch (\Exception) {
-                    $transaction->rollBack();
-                    Yii::$app->session->setFlash('error', 'Ошибка при применении разметки.');
                 }
             } elseif ($action === 'reject') {
                 $prediction->status     = Products1cAutomarkPrediction::STATUS_REJECTED;
index 4bbd17416e0915a63a6b6a6f691361c199fb63eb..b9bcdb2c2dc1738ed883471361f92440d15e07be 100644 (file)
@@ -13,6 +13,9 @@ class AutoMarkPredictionJob extends \yii\base\BaseObject implements JobInterface
 
     public function execute($queue): void
     {
-        (new AutoMarkService())->predictForProduct($this->productId);
+        $result = (new AutoMarkService())->predictForProduct($this->productId);
+        if ($result === null) {
+            \Yii::error("AutoMarkPredictionJob: не удалось создать предсказание для productId={$this->productId}", 'automark');
+        }
     }
 }
index 554b30d08fcd798352e5a3d518cc1e90be4f8609..add9e22bd459f26129b7ee02dff7cd3ca92ef626 100644 (file)
@@ -69,7 +69,7 @@ class m260506_120000_add_automark_rbac_permissions extends Migration
                 ->one();
 
             if ($row !== false && $row !== null) {
-                $existing = array_filter(explode(',', (string) $row['config']));
+                $existing = array_filter(array_map('trim', explode(',', (string) $row['config'])));
                 if (in_array(self::PERMISSION, $existing, true)) {
                     echo "  Группа {$groupId}: разрешение уже есть, пропускаем\n";
                     continue;
@@ -104,15 +104,19 @@ class m260506_120000_add_automark_rbac_permissions extends Migration
             }
 
             $filtered = array_values(array_diff(
-                array_filter(explode(',', (string) $row['config'])),
+                array_filter(array_map('trim', explode(',', (string) $row['config']))),
                 [self::PERMISSION]
             ));
 
-            $this->update(
-                'admin_group_rbac_config',
-                ['config' => implode(',', $filtered)],
-                ['admin_group_id' => $groupId]
-            );
+            if ($filtered === []) {
+                $this->delete('admin_group_rbac_config', ['admin_group_id' => $groupId]);
+            } else {
+                $this->update(
+                    'admin_group_rbac_config',
+                    ['config' => implode(',', $filtered)],
+                    ['admin_group_id' => $groupId]
+                );
+            }
         }
     }
 
index 2c1ee4176ffae4996d36215f9f18671a7f4f6a5f..1f72cf3b7c629206cb5535a71e9348031d9b2b22 100644 (file)
@@ -106,11 +106,6 @@ class AutoMarkService
             $nomenclature->size        = $prediction->size        ?? $nomenclature->size;
             $nomenclature->color       = $prediction->color       ?? $nomenclature->color;
 
-            $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();
index 98b9df5c8811c5a277c5b79fcabae3ac40a24a10..0f59ffc84d80d326578e49137e304517b5ab06c9 100644 (file)
@@ -42,7 +42,7 @@ final class CoordinatorLlmTransport implements LlmTransportInterface
                 'json' => $payload,
             ]);
         } catch (GuzzleException $e) {
-            throw new \RuntimeException('LLM HTTP ошибка: ' . $e->getMessage(), 0, $e);
+            throw new \RuntimeException('LLM HTTP ошибка: ' . $e->getCode());
         }
 
         return self::parseAnalyzeResponse((string) $response->getBody());
@@ -68,7 +68,7 @@ final class CoordinatorLlmTransport implements LlmTransportInterface
             throw new \RuntimeException('Coordinator вернул невалидный JSON');
         }
 
-        if (($decoded['ok'] ?? true) === false) {
+        if (array_key_exists('ok', $decoded) && !$decoded['ok']) {
             $message = $decoded['error'] ?? $decoded['detail']['title'] ?? 'Remote coordinator error';
             throw new \RuntimeException((string) $message);
         }
index b84bf5aa6b9f4b1c62a1e9afb626730faffa28aa..71cd69b754abd6f089ce12ed804d12014ff8dfc2 100644 (file)
@@ -154,13 +154,13 @@ TAXONOMY;
         foreach ($predictions as $prediction) {
             $items[] = [
                 'id'          => $prediction->id,
-                'name'        => $prediction->product->name ?? '',
+                'name'        => $prediction->product?->name ?? '',
                 'category'    => $prediction->category,
                 'subcategory' => $prediction->subcategory,
                 'species'     => $prediction->species,
                 'size'        => $prediction->size,
                 'color'       => $prediction->color,
-                'examples'    => $this->getSimilarExamples($prediction->product->name ?? ''),
+                'examples'    => $this->getSimilarExamples($prediction->product?->name ?? ''),
             ];
         }
         return $items;
@@ -312,9 +312,14 @@ TAXONOMY;
             $corrections['species'] = $result->species;
             $corrections['size'] = $result->size;
             $corrections['color'] = $result->color;
+        } elseif (!$result->isApproved() && !$result->hasCorrections()) {
+            \Yii::warning(
+                "LLM вернул verdict=rejected без corrected_prediction для id={$result->predictionId}",
+                'automark'
+            );
         }
 
-        $name = (string) ($prediction->product->name ?? '');
+        $name = (string) ($prediction->product?->name ?? '');
         $explicitSize = $this->extractExplicitSize($name);
         if ($explicitSize !== null && (float) ($prediction->size ?? 0) !== $explicitSize && !array_key_exists('size', $corrections)) {
             $corrections['size'] = $explicitSize;
@@ -385,7 +390,7 @@ PROMPT;
                 'similar_examples' => $item['examples'],
             ];
         }
-        return json_encode($payload, JSON_UNESCAPED_UNICODE);
+        return json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE);
     }
 
     public function buildEffectiveSystemPrompt(): string
index 12b84166688d20941e3f4a869d09c7fb1247537a..6e10574997457ace61f2d79ee2d4959b88f27a1f 100644 (file)
@@ -40,7 +40,7 @@ final class OpenAiCompatibleLlmTransport implements LlmTransportInterface
                 ],
             ]);
         } catch (GuzzleException $e) {
-            throw new \RuntimeException('LLM HTTP ошибка: ' . $e->getMessage(), 0, $e);
+            throw new \RuntimeException('LLM HTTP ошибка: ' . $e->getCode());
         }
 
         return self::parseChatCompletionResponse((string) $response->getBody());
index c73d89cdd0a99031454dc2f19c51b69e95423d2f..e71b82edb379b7e10b62f9e2294f3ed3918e42a1 100644 (file)
@@ -81,6 +81,9 @@ class RuleBasedParser
 
         if ($species !== null && $category === null) {
             $category = 'Срезка';
+        } elseif ($species !== null && $category === 'Горшечные_растения') {
+            // Вид из SPECIES_SREZY + горшечный маркер — противоречие; срезка приоритетнее
+            $category = 'Срезка';
         }
 
         $subcategory = $species !== null ? (self::SPECIES_SUBCATEGORY[$species] ?? null) : null;
@@ -185,11 +188,21 @@ class RuleBasedParser
         ?float $size
     ): float {
         $score = 0.5;
-        if ($category !== null) $score += 0.2;
-        if ($species !== null)  $score += 0.15;
-        if ($color !== null)    $score += 0.1;
-        if ($size !== null)     $score += 0.05;
-        if ($sort !== null)     $score += 0.05;
+        if ($category !== null) {
+            $score += 0.2;
+        }
+        if ($species !== null) {
+            $score += 0.15;
+        }
+        if ($color !== null) {
+            $score += 0.1;
+        }
+        if ($size !== null) {
+            $score += 0.05;
+        }
+        if ($sort !== null) {
+            $score += 0.05;
+        }
         return min(1.0, $score);
     }
 }
index f52d2d4612306c139739930465c137f40b8101f3..39ab2437f003cd6ba43dbefebccff70423f56ba4 100644 (file)
@@ -87,7 +87,7 @@ $formatSize = static function ($value): string {
                         ]);
                     }
 
-                    [$color, $label] = $badgeMap[$m->llm_verdict] ?? ['#374151', $m->llm_verdict];
+                    [$color, $label] = $badgeMap[$m->llm_verdict] ?? ['#374151', Html::encode($m->llm_verdict)];
                     return Html::tag('span', $label, [
                         'style' => sprintf('color:%s;font-weight:700;', $color),
                     ]);
index c4486de714b503fe74ab83c4b37425fd86ea455e..1b22454ffe03f2a15243979b870ecfd889ba1c81 100644 (file)
@@ -43,11 +43,13 @@ if ($prediction->size !== null && (float) $prediction->size > 0.0) {
     <?php if ($prediction->isPending()): ?>
     <div class="d-flex gap-2">
         <?= Html::beginForm(['review', 'id' => $prediction->id], 'post') ?>
+            <?= Html::hiddenInput(Yii::$app->request->csrfParam, Yii::$app->request->csrfToken) ?>
             <?= Html::hiddenInput('action', 'approve') ?>
             <?= Html::submitButton('Применить разметку', ['class' => 'btn btn-success']) ?>
         <?= Html::endForm() ?>
 
         <?= Html::beginForm(['review', 'id' => $prediction->id], 'post') ?>
+            <?= Html::hiddenInput(Yii::$app->request->csrfParam, Yii::$app->request->csrfToken) ?>
             <?= Html::hiddenInput('action', 'reject') ?>
             <?= Html::submitButton('Отклонить', ['class' => 'btn btn-danger']) ?>
         <?= Html::endForm() ?>