$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;
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');
+ }
}
}
->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;
}
$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]
+ );
+ }
}
}
$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();
'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());
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);
}
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;
$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;
'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
],
]);
} 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());
if ($species !== null && $category === null) {
$category = 'Срезка';
+ } elseif ($species !== null && $category === 'Горшечные_растения') {
+ // Вид из SPECIES_SREZY + горшечный маркер — противоречие; срезка приоритетнее
+ $category = 'Срезка';
}
$subcategory = $species !== null ? (self::SPECIES_SUBCATEGORY[$species] ?? null) : null;
?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);
}
}
]);
}
- [$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),
]);
<?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() ?>