]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
review fixes
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 6 May 2026 07:39:06 +0000 (10:39 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 6 May 2026 07:39:06 +0000 (10:39 +0300)
erp24/commands/AuthController.php
erp24/controllers/AutoMarkController.php
erp24/migrations/m260506_120000_add_automark_rbac_permissions.php [new file with mode: 0644]
erp24/records/AdminGroup.php
erp24/services/AutoMarkService.php
erp24/services/automark/LlmVerifier.php

index 1b3819a92f748e92e1e32a6a1d29b3c93aa6f44d..1d8573556bfda93487319620f2de8e603132c88a 100644 (file)
@@ -14,19 +14,35 @@ use yii_app\records\CrmMenu;
 
 class AuthController extends Controller
 {
+    /**
+     * Принудительный запуск без проверки кэша.
+     * Используется при прерванном предыдущем запуске: php yii auth/init --force=1
+     */
+    public bool $force = false;
+
+    public function options($actionID): array
+    {
+        return array_merge(parent::options($actionID), ['force']);
+    }
+
     /**
      * @throws Exception
      * @throws \Exception
      */
     public function actionInit() {
-        if (Yii::$app->cache->get("dirtyAuthSettings") ?? true) {
-            Yii::$app->cache->set("dirtyAuthSettings", false);
-            echo "В настройках разрешений есть изменения. Продолжаю выполнение...\n";
-        } else {
+        $isDirty = Yii::$app->cache->get("dirtyAuthSettings") ?? true;
+        if (!$this->force && !$isDirty) {
             echo "Нет изменений в настройках разрешений\n";
             return 'ok';
         }
 
+        if ($this->force) {
+            echo "Принудительный запуск (--force).\n";
+        }
+
+        Yii::$app->cache->set("dirtyAuthSettings", false);
+        echo "В настройках разрешений есть изменения. Продолжаю выполнение...\n";
+
         $auth = Yii::$app->authManager;
         $auth->removeAll();
 
index 37e164d82dc18d7435a944b801743507ef88405b..4fbc6e086a0e506655ab747feb946fff288cfebf 100644 (file)
@@ -5,6 +5,7 @@ declare(strict_types=1);
 namespace app\controllers;
 
 use Yii;
+use yii\filters\AccessControl;
 use yii\web\Controller;
 use yii\web\NotFoundHttpException;
 use yii\web\Response;
@@ -14,6 +15,24 @@ use yii_app\services\AutoMarkService;
 
 class AutoMarkController extends Controller
 {
+    public function behaviors(): array
+    {
+        return [
+            'access' => [
+                'class' => AccessControl::class,
+                'rules' => [
+                    [
+                        'allow' => true,
+                        'roles' => ['@'],
+                        'matchCallback' => function ($rule, $action) {
+                            return Yii::$app->user->can('automarkReview');
+                        },
+                    ],
+                ],
+            ],
+        ];
+    }
+
     public function actionIndex(): string
     {
         $query = Products1cAutomarkPrediction::find()
@@ -64,11 +83,14 @@ class AutoMarkController extends Controller
                     $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()) {
+                    if (!$prediction->save()) {
+                        $transaction->rollBack();
+                        Yii::$app->session->setFlash('error', 'Не удалось сохранить предсказание.');
+                    } else {
                         $service->applyApprovedPrediction($prediction->id);
+                        $transaction->commit();
+                        Yii::$app->session->setFlash('success', 'Разметка применена.');
                     }
-                    $transaction->commit();
-                    Yii::$app->session->setFlash('success', 'Разметка применена.');
                 } catch (\Exception) {
                     $transaction->rollBack();
                     Yii::$app->session->setFlash('error', 'Ошибка при применении разметки.');
@@ -76,8 +98,11 @@ class AutoMarkController extends Controller
             } elseif ($action === 'reject') {
                 $prediction->status     = Products1cAutomarkPrediction::STATUS_REJECTED;
                 $prediction->updated_at = date('Y-m-d H:i:s');
-                $prediction->save();
-                Yii::$app->session->setFlash('info', 'Предсказание отклонено.');
+                if ($prediction->save()) {
+                    Yii::$app->session->setFlash('info', 'Предсказание отклонено.');
+                } else {
+                    Yii::$app->session->setFlash('error', 'Не удалось отклонить предсказание.');
+                }
             }
 
             return $this->redirect(['index']);
diff --git a/erp24/migrations/m260506_120000_add_automark_rbac_permissions.php b/erp24/migrations/m260506_120000_add_automark_rbac_permissions.php
new file mode 100644 (file)
index 0000000..554b30d
--- /dev/null
@@ -0,0 +1,145 @@
+<?php
+
+declare(strict_types=1);
+
+use yii\db\Migration;
+use yii_app\records\AdminGroup;
+
+/**
+ * Добавить RBAC-разрешение automarkReview для авторазметки (ERP-292).
+ *
+ * Даёт доступ к /auto-mark/* группам:
+ *   - 1  (DIRECTOR)
+ *   - 10 (GROUP_RS_DIRECTOR — Директор розничной сети)
+ *   - 81 (GROUP_IT)
+ *   - 82 (GROUP_QC_MANAGER — Менеджер контроля качества)
+ *
+ * После применения ОБЯЗАТЕЛЬНО запустить:
+ *   php yii auth/init
+ */
+class m260506_120000_add_automark_rbac_permissions extends Migration
+{
+    private const PERMISSION = 'automarkReview';
+
+    private const TARGET_GROUP_IDS = [
+        AdminGroup::DIRECTOR,        // 1
+        AdminGroup::GROUP_RS_DIRECTOR, // 10
+        AdminGroup::GROUP_IT,        // 81
+        AdminGroup::GROUP_KIK_MANAGER, // 82
+    ];
+
+    private const AUTOMARK_TABLE  = 'erp24.products_1c_automark_predictions';
+    private const COMPOUND_INDEX  = 'idx_automark_status_llm_verdict';
+
+    public function safeUp(): void
+    {
+        $this->addRbacPermission();
+        $this->addCompoundIndex();
+
+        Yii::$app->cache->set('dirtyAuthSettings', true);
+
+        echo "\n";
+        echo "==============================================================\n";
+        echo " ВАЖНО: теперь запустите:\n";
+        echo "   php yii auth/init\n";
+        echo " чтобы разрешение automarkReview создалось в auth_item\n";
+        echo " и назначилось пользователям целевых групп.\n";
+        echo "==============================================================\n";
+    }
+
+    public function safeDown(): void
+    {
+        $this->removeRbacPermission();
+
+        if ($this->hasIndex(self::COMPOUND_INDEX)) {
+            $this->dropIndex(self::COMPOUND_INDEX, self::AUTOMARK_TABLE);
+        }
+
+        Yii::$app->cache->set('dirtyAuthSettings', true);
+
+        echo "\nПосле отката запустите: php yii auth/init\n";
+    }
+
+    private function addRbacPermission(): void
+    {
+        foreach (self::TARGET_GROUP_IDS as $groupId) {
+            $row = (new \yii\db\Query())
+                ->from('admin_group_rbac_config')
+                ->where(['admin_group_id' => $groupId])
+                ->one();
+
+            if ($row !== false && $row !== null) {
+                $existing = array_filter(explode(',', (string) $row['config']));
+                if (in_array(self::PERMISSION, $existing, true)) {
+                    echo "  Группа {$groupId}: разрешение уже есть, пропускаем\n";
+                    continue;
+                }
+                $existing[] = self::PERMISSION;
+                $this->update(
+                    'admin_group_rbac_config',
+                    ['config' => implode(',', $existing)],
+                    ['admin_group_id' => $groupId]
+                );
+                echo "  Группа {$groupId}: добавлено разрешение " . self::PERMISSION . "\n";
+            } else {
+                $this->insert('admin_group_rbac_config', [
+                    'admin_group_id' => $groupId,
+                    'config'         => self::PERMISSION,
+                ]);
+                echo "  Группа {$groupId}: создана запись с разрешением " . self::PERMISSION . "\n";
+            }
+        }
+    }
+
+    private function removeRbacPermission(): void
+    {
+        foreach (self::TARGET_GROUP_IDS as $groupId) {
+            $row = (new \yii\db\Query())
+                ->from('admin_group_rbac_config')
+                ->where(['admin_group_id' => $groupId])
+                ->one();
+
+            if ($row === false || $row === null) {
+                continue;
+            }
+
+            $filtered = array_values(array_diff(
+                array_filter(explode(',', (string) $row['config'])),
+                [self::PERMISSION]
+            ));
+
+            $this->update(
+                'admin_group_rbac_config',
+                ['config' => implode(',', $filtered)],
+                ['admin_group_id' => $groupId]
+            );
+        }
+    }
+
+    private function addCompoundIndex(): void
+    {
+        $tableSchema = $this->db->getTableSchema(self::AUTOMARK_TABLE);
+        if ($tableSchema === null) {
+            return;
+        }
+
+        if (!$this->hasIndex(self::COMPOUND_INDEX)) {
+            $this->createIndex(
+                self::COMPOUND_INDEX,
+                self::AUTOMARK_TABLE,
+                ['status', 'llm_verdict']
+            );
+            echo "  Создан составной индекс (status, llm_verdict)\n";
+        }
+    }
+
+    private function hasIndex(string $indexName): bool
+    {
+        $parts = explode('.', self::AUTOMARK_TABLE, 2);
+        $schema = $parts[0];
+        return (new \yii\db\Query())
+            ->from('pg_indexes')
+            ->where(['schemaname' => $schema, 'indexname' => $indexName])
+            ->exists($this->db);
+    }
+}
index 74e779b8a2d82adf41f2a23879b51674635b42c8..7ce6e93f617eb6791cfb4d994724cd15b402c049 100755 (executable)
@@ -37,6 +37,7 @@ class AdminGroup extends ActiveRecord
     const GROUP_BUSH_DIRECTOR = 7;
     const GROUP_OPERATIONAL_DIRECTOR = 51;
     const GROUP_IT = 81;
+    const GROUP_KIK_MANAGER = 82; // Менеджер контроля качества
     const GROUP_FINANCE_DIRECTOR = 9;
 
     /**
index 494090b29842826a09a4009c1b63e71e561ef553..2c1ee4176ffae4996d36215f9f18671a7f4f6a5f 100644 (file)
@@ -36,6 +36,7 @@ class AutoMarkService
 
     private RuleBasedParser   $ruleParser;
     private SimilarityMatcher $matcher;
+    private ?array            $corpusCache = null;
 
     public function __construct()
     {
@@ -208,7 +209,7 @@ class AutoMarkService
 
     private function loadCorpus(): array
     {
-        return Products1cNomenclature::find()
+        return $this->corpusCache ??= Products1cNomenclature::find()
             ->select(['id', 'name', 'category', 'subcategory', 'species', 'sort', 'type', 'size', 'color'])
             ->where(['not', ['category' => null]])
             ->asArray()
index cbf2b86ea430a20caabccf3bf6cd1202c760177c..b84bf5aa6b9f4b1c62a1e9afb626730faffa28aa 100644 (file)
@@ -48,38 +48,56 @@ TAXONOMY;
 
     /**
      * Верифицировать все pending-предсказания без LLM-вердикта.
+     * Загружает записи батчами (cursor by id) — не грузит всё в память сразу.
      *
      * @param callable|null $progressCallback function(string $event, array<string, mixed> $data): void
      * @return int Количество верифицированных предсказаний
      */
     public function verifyPending(?callable $progressCallback = null): int
     {
-        $predictions = Products1cAutomarkPrediction::find()
-            ->with('product')
+        $totalPredictions = (int) Products1cAutomarkPrediction::find()
             ->where(['status' => Products1cAutomarkPrediction::STATUS_PENDING])
             ->andWhere(['llm_verdict' => null])
-            ->all();
+            ->count();
 
-        if (empty($predictions)) {
+        if ($totalPredictions === 0) {
             return 0;
         }
 
-        $batches  = array_chunk($predictions, $this->batchSize);
-        $totalBatches = count($batches);
-        $totalPredictions = count($predictions);
+        $totalBatches = (int) ceil($totalPredictions / $this->batchSize);
         $verified = 0;
         $failedBatches = 0;
         $firstError = null;
         $processedPredictions = 0;
+        $batchNumber = 0;
+        $lastId = 0;
 
-        foreach ($batches as $index => $batch) {
-            $batchNumber = $index + 1;
-            $items   = $this->enrichBatch($batch);
-            $payload = $this->buildEffectiveUserPrompt(self::buildBatchPayload($items));
-            $batchIds = array_map(
-                static fn(Products1cAutomarkPrediction $prediction): int => (int) $prediction->id,
-                $batch
-            );
+        do {
+            $batch = Products1cAutomarkPrediction::find()
+                ->with('product')
+                ->where(['status' => Products1cAutomarkPrediction::STATUS_PENDING])
+                ->andWhere(['llm_verdict' => null])
+                ->andWhere(['>', 'id', $lastId])
+                ->orderBy(['id' => SORT_ASC])
+                ->limit($this->batchSize)
+                ->all();
+
+            if (empty($batch)) {
+                break;
+            }
+
+            $batchNumber++;
+            $lastId = (int) end($batch)->id;
+
+            /** @var array<int, Products1cAutomarkPrediction> $predictionsById */
+            $predictionsById = [];
+            foreach ($batch as $prediction) {
+                $predictionsById[(int) $prediction->id] = $prediction;
+            }
+
+            $items    = $this->enrichBatch($batch);
+            $payload  = $this->buildEffectiveUserPrompt(self::buildBatchPayload($items));
+            $batchIds = array_keys($predictionsById);
 
             $progressCallback?->__invoke('batch_start', [
                 'batchNumber' => $batchNumber,
@@ -93,7 +111,7 @@ TAXONOMY;
             try {
                 $raw     = $this->client->chat($this->buildEffectiveSystemPrompt(), $payload);
                 $results = self::parseVerifyResults($raw);
-                $saved = $this->saveResults($results);
+                $saved   = $this->saveResults($results, $predictionsById);
                 $verified += $saved;
                 $processedPredictions += count($batch);
 
@@ -109,7 +127,7 @@ TAXONOMY;
                 ]);
             } catch (\RuntimeException $e) {
                 $failedBatches++;
-                $firstError ??= sprintf('batch %d: %s', $index + 1, $e->getMessage());
+                $firstError ??= sprintf('batch %d: %s', $batchNumber, $e->getMessage());
                 $processedPredictions += count($batch);
 
                 $progressCallback?->__invoke('batch_error', [
@@ -120,9 +138,8 @@ TAXONOMY;
                     'processedPredictions' => $processedPredictions,
                     'totalPredictions' => $totalPredictions,
                 ]);
-                continue;
             }
-        }
+        } while (count($batch) === $this->batchSize);
 
         if ($verified === 0 && $failedBatches > 0) {
             throw new \RuntimeException('LLM verification failed for all batches, first error: ' . $firstError);
@@ -249,18 +266,15 @@ TAXONOMY;
 
     /**
      * @param LlmVerifyResult[] $results
+     * @param array<int, Products1cAutomarkPrediction> $predictionsById Уже загруженные из БД предсказания
      */
-    private function saveResults(array $results): int
+    private function saveResults(array $results, array $predictionsById): int
     {
         $count = 0;
         $now   = date('Y-m-d H:i:s');
 
         foreach ($results as $result) {
-            $prediction = Products1cAutomarkPrediction::find()
-                ->with('product')
-                ->where(['id' => $result->predictionId])
-                ->one();
-
+            $prediction = $predictionsById[$result->predictionId] ?? null;
             if ($prediction === null) {
                 continue;
             }