Files
lotteryLaravel/app/Services/Draw/DrawRngSeedDerivation.php
kang e27a00f260 feat: 更新玩法配置管理,简化字段并增强功能
- 将玩法相关的显示名称字段统一为 `display_name`,移除多语言字段。
- 在 `PlayTypePatchController` 中新增即时切换玩法开关的功能,并推送大厅更新。
- 优化多个控制器和服务中的权限检查与数据处理逻辑,提升代码可读性与维护性。
2026-05-25 14:34:24 +08:00

127 lines
4.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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 位号码00009999
*/
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;
}
}