- 将玩法相关的显示名称字段统一为 `display_name`,移除多语言字段。 - 在 `PlayTypePatchController` 中新增即时切换玩法开关的功能,并推送大厅更新。 - 优化多个控制器和服务中的权限检查与数据处理逻辑,提升代码可读性与维护性。
127 lines
4.1 KiB
PHP
127 lines
4.1 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Draw;
|
||
|
||
use App\Models\Draw;
|
||
use App\Models\DrawResultBatch;
|
||
use App\Models\DrawResultItem;
|
||
use Illuminate\Support\Facades\Crypt;
|
||
use Illuminate\Contracts\Encryption\DecryptException;
|
||
|
||
/**
|
||
* RNG 种子:CSPRNG 采集、SHA-256 摘要、Laravel 加密落库、确定性派生 4 位号码(算法 v1)。
|
||
*
|
||
* 验收:解密种子 → sha256(seed_hex) === rng_seed_hash → 复算 23 组号码与 draw_result_items 一致。
|
||
*/
|
||
final class DrawRngSeedDerivation
|
||
{
|
||
public const ALGORITHM_VERSION = 'v1';
|
||
|
||
/** 生成 32 字节随机种子(十六进制,64 字符) */
|
||
public static function generateSeedHex(): string
|
||
{
|
||
return bin2hex(random_bytes(32));
|
||
}
|
||
|
||
public static function hashSeedHex(string $seedHex): string
|
||
{
|
||
return hash('sha256', $seedHex);
|
||
}
|
||
|
||
public static function encryptSeedHex(string $seedHex): string
|
||
{
|
||
return Crypt::encryptString($seedHex);
|
||
}
|
||
|
||
public static function decryptSeedHex(string $encrypted): string
|
||
{
|
||
try {
|
||
return Crypt::decryptString($encrypted);
|
||
} catch (DecryptException $e) {
|
||
throw new \InvalidArgumentException('RNG seed decrypt failed', 0, $e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 由种子确定性派生第 $slotIndex 槽位的 4 位号码(0000–9999)。
|
||
*/
|
||
public static function deriveNumber4d(string $seedHex, int $drawId, int $slotIndex): string
|
||
{
|
||
$seedBinary = hex2bin($seedHex);
|
||
if ($seedBinary === false || strlen($seedBinary) !== 32) {
|
||
throw new \InvalidArgumentException('RNG seed must be 64 hex chars (32 bytes).');
|
||
}
|
||
|
||
$message = self::ALGORITHM_VERSION.'|draw:'.$drawId.'|slot:'.$slotIndex;
|
||
$digest = hash_hmac('sha256', $message, $seedBinary, true);
|
||
$chunk = substr($digest, 0, 4);
|
||
$unpacked = unpack('V', $chunk);
|
||
$value = ((int) ($unpacked[1] ?? 0)) % 10_000;
|
||
|
||
return str_pad((string) $value, 4, '0', STR_PAD_LEFT);
|
||
}
|
||
|
||
/**
|
||
* @return list<array{prize_type: string, prize_index: int, number_4d: string, suffix_3d: string, suffix_2d: string, head_digit: int|null, tail_digit: int|null}>
|
||
*/
|
||
public static function deriveAllSlotRows(string $seedHex, int $drawId): array
|
||
{
|
||
$rows = [];
|
||
foreach (DrawPrizeLayout::slots() as $slotIndex => $slot) {
|
||
$num = self::deriveNumber4d($seedHex, $drawId, $slotIndex);
|
||
$rows[] = [
|
||
'prize_type' => $slot['prize_type'],
|
||
'prize_index' => $slot['prize_index'],
|
||
'number_4d' => $num,
|
||
'suffix_3d' => substr($num, -3),
|
||
'suffix_2d' => substr($num, -2),
|
||
'head_digit' => $num !== '' ? (int) substr($num, 0, 1) : null,
|
||
'tail_digit' => $num !== '' ? (int) substr($num, 3, 1) : null,
|
||
];
|
||
}
|
||
|
||
return $rows;
|
||
}
|
||
|
||
/** 审计:校验批次种子摘要、密文可解密且号码可由种子复算。 */
|
||
public static function verifyBatchAudit(DrawResultBatch $batch, Draw $draw): bool
|
||
{
|
||
if ($batch->source_type !== 'rng') {
|
||
return false;
|
||
}
|
||
|
||
$encrypted = $batch->raw_seed_encrypted;
|
||
$hash = $batch->rng_seed_hash;
|
||
if (! is_string($encrypted) || $encrypted === '' || ! is_string($hash) || $hash === '') {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
$seedHex = self::decryptSeedHex($encrypted);
|
||
} catch (\InvalidArgumentException) {
|
||
return false;
|
||
}
|
||
|
||
if (self::hashSeedHex($seedHex) !== $hash) {
|
||
return false;
|
||
}
|
||
|
||
$expected = self::deriveAllSlotRows($seedHex, (int) $draw->id);
|
||
$items = $batch->items()->get();
|
||
|
||
if ($items->count() !== count($expected)) {
|
||
return false;
|
||
}
|
||
|
||
foreach ($expected as $row) {
|
||
$item = $items->first(fn (DrawResultItem $i) => $i->prize_type === $row['prize_type']
|
||
&& (int) $i->prize_index === $row['prize_index']);
|
||
if ($item === null || $item->number_4d !== $row['number_4d']) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
}
|