From 253bce35fa308ea08b90186959013fd188670f39 Mon Sep 17 00:00:00 2001 From: fomichev Date: Wed, 6 May 2026 10:39:06 +0300 Subject: [PATCH] review fixes --- erp24/commands/AuthController.php | 24 ++- erp24/controllers/AutoMarkController.php | 35 ++++- ...6_120000_add_automark_rbac_permissions.php | 145 ++++++++++++++++++ erp24/records/AdminGroup.php | 1 + erp24/services/AutoMarkService.php | 3 +- erp24/services/automark/LlmVerifier.php | 64 +++++--- 6 files changed, 237 insertions(+), 35 deletions(-) create mode 100644 erp24/migrations/m260506_120000_add_automark_rbac_permissions.php diff --git a/erp24/commands/AuthController.php b/erp24/commands/AuthController.php index 1b3819a9..1d857355 100644 --- a/erp24/commands/AuthController.php +++ b/erp24/commands/AuthController.php @@ -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(); diff --git a/erp24/controllers/AutoMarkController.php b/erp24/controllers/AutoMarkController.php index 37e164d8..4fbc6e08 100644 --- a/erp24/controllers/AutoMarkController.php +++ b/erp24/controllers/AutoMarkController.php @@ -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 index 00000000..554b30d0 --- /dev/null +++ b/erp24/migrations/m260506_120000_add_automark_rbac_permissions.php @@ -0,0 +1,145 @@ +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); + } +} diff --git a/erp24/records/AdminGroup.php b/erp24/records/AdminGroup.php index 74e779b8..7ce6e93f 100755 --- a/erp24/records/AdminGroup.php +++ b/erp24/records/AdminGroup.php @@ -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; /** diff --git a/erp24/services/AutoMarkService.php b/erp24/services/AutoMarkService.php index 494090b2..2c1ee417 100644 --- a/erp24/services/AutoMarkService.php +++ b/erp24/services/AutoMarkService.php @@ -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() diff --git a/erp24/services/automark/LlmVerifier.php b/erp24/services/automark/LlmVerifier.php index cbf2b86e..b84bf5aa 100644 --- a/erp24/services/automark/LlmVerifier.php +++ b/erp24/services/automark/LlmVerifier.php @@ -48,38 +48,56 @@ TAXONOMY; /** * Верифицировать все pending-предсказания без LLM-вердикта. + * Загружает записи батчами (cursor by id) — не грузит всё в память сразу. * * @param callable|null $progressCallback function(string $event, array $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 $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 $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; } -- 2.39.5