You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

184 lines
6.9 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Support;
/**
* 赛道打分表:结构校验、提交总分校验。
*
* 约定:
* - 配置时 Σ max_score 必须 = 100(作评审参考)
* - 提交时填写项目总分:0 ≤ line_total ≤ 100
*/
class TrackScoringSheet
{
public const FULL_SCORE = 100.0;
/**
* @param mixed $raw
* @return array{ok: true, sheet: array{version: int, title: string, full_score: float, items: list<array{key: string, sort: int, category: string, title: string, criteria: string, max_score: float}>}}|array{ok: false, message: string}
*/
public static function parseAndValidateConfig(mixed $raw): array
{
if ($raw === null) {
return ['ok' => false, 'message' => '打分表不能为空'];
}
if (is_string($raw)) {
$trimmed = trim($raw);
if ($trimmed === '') {
return ['ok' => false, 'message' => '打分表不能为空'];
}
try {
$raw = json_decode($trimmed, true, 512, JSON_THROW_ON_ERROR);
} catch (\JsonException) {
return ['ok' => false, 'message' => '打分表 JSON 格式不正确'];
}
}
if (! is_array($raw)) {
return ['ok' => false, 'message' => '打分表须为 JSON 对象'];
}
$title = isset($raw['title']) && is_string($raw['title']) ? trim($raw['title']) : '';
$itemsRaw = $raw['items'] ?? null;
if (! is_array($itemsRaw) || count($itemsRaw) === 0) {
return ['ok' => false, 'message' => '打分表至少包含一项指标'];
}
$items = [];
$keys = [];
$sumMax = 0.0;
foreach ($itemsRaw as $idx => $row) {
if (! is_array($row)) {
return ['ok' => false, 'message' => '第 '.((int) $idx + 1).' 项指标格式无效'];
}
$key = isset($row['key']) && is_string($row['key']) ? trim($row['key']) : '';
if ($key === '' || ! preg_match('/^[a-zA-Z][a-zA-Z0-9_]{0,63}$/', $key)) {
return ['ok' => false, 'message' => '第 '.((int) $idx + 1).' 项指标 key 无效(字母开头,字母数字下划线)'];
}
if (isset($keys[$key])) {
return ['ok' => false, 'message' => '指标 key 重复:'.$key];
}
$keys[$key] = true;
$category = isset($row['category']) && is_string($row['category']) ? trim($row['category']) : '';
$itemTitle = isset($row['title']) && is_string($row['title']) ? trim($row['title']) : '';
$criteria = isset($row['criteria']) && is_string($row['criteria']) ? trim($row['criteria']) : '';
if ($category === '' || $itemTitle === '') {
return ['ok' => false, 'message' => '第 '.((int) $idx + 1).' 项须填写指标分类与评审指标'];
}
if (! isset($row['max_score']) || ! is_numeric($row['max_score'])) {
return ['ok' => false, 'message' => '「'.$itemTitle.'」分值无效'];
}
$max = (float) $row['max_score'];
if ($max <= 0 || $max > self::FULL_SCORE) {
return ['ok' => false, 'message' => '「'.$itemTitle.'」分值须在 0~100 之间且大于 0'];
}
$sort = isset($row['sort']) && is_numeric($row['sort']) ? (int) $row['sort'] : ((int) $idx + 1);
$items[] = [
'key' => $key,
'sort' => $sort,
'category' => $category,
'title' => $itemTitle,
'criteria' => $criteria,
'max_score' => $max,
];
$sumMax += $max;
}
usort($items, fn (array $a, array $b) => $a['sort'] <=> $b['sort'] ?: strcmp($a['key'], $b['key']));
if (abs($sumMax - self::FULL_SCORE) > 0.0001) {
return [
'ok' => false,
'message' => '各项分值之和须为 '.self::FULL_SCORE.' 分,当前为 '.rtrim(rtrim(number_format($sumMax, 4, '.', ''), '0'), '.'),
];
}
$version = isset($raw['version']) && is_numeric($raw['version']) ? (int) $raw['version'] : 1;
return [
'ok' => true,
'sheet' => [
'version' => max(1, $version),
'title' => $title,
'full_score' => self::FULL_SCORE,
'items' => $items,
],
];
}
/**
* @param array<string, mixed>|null $sheet
*/
public static function isConfigured(?array $sheet): bool
{
if ($sheet === null) {
return false;
}
$parsed = self::parseAndValidateConfig($sheet);
return $parsed['ok'] === true;
}
/**
* @param array{version: int, title: string, full_score: float, items: list<array{key: string, sort: int, category: string, title: string, criteria: string, max_score: float}>} $sheet
* @param array<string, mixed> $payload 期望 { line_total: number }(兼容 total)
* @return array{ok: true, scores: array<string, float>, line_total: float, payload: array<string, mixed>}|array{ok: false, message: string}
*/
public static function normalizeSubmitPayload(array $sheet, array $payload): array
{
$raw = $payload['line_total'] ?? $payload['total'] ?? null;
if ($raw === null || $raw === '') {
return ['ok' => false, 'message' => '请填写项目总分'];
}
if (! is_numeric($raw)) {
return ['ok' => false, 'message' => '项目总分须为数字'];
}
$lineTotal = (float) $raw;
$full = (float) ($sheet['full_score'] ?? self::FULL_SCORE);
if ($lineTotal < 0) {
return ['ok' => false, 'message' => '项目总分不能小于 0'];
}
if ($lineTotal > $full) {
$fullLabel = rtrim(rtrim(number_format($full, 4, '.', ''), '0'), '.');
return ['ok' => false, 'message' => '项目总分不得超过 '.$fullLabel.' 分'];
}
$outPayload = [
'line_total' => $lineTotal,
'scores' => [],
'sheet_snapshot' => [
'version' => $sheet['version'],
'title' => $sheet['title'],
'full_score' => $sheet['full_score'],
'items' => array_map(static fn (array $it) => [
'key' => $it['key'],
'sort' => $it['sort'],
'category' => $it['category'],
'title' => $it['title'],
'max_score' => $it['max_score'],
], $sheet['items']),
],
];
if (isset($payload['comment']) && is_string($payload['comment'])) {
$outPayload['comment'] = trim($payload['comment']);
}
return [
'ok' => true,
'scores' => [],
'line_total' => $lineTotal,
'payload' => $outPayload,
];
}
}