- 将玩法相关的显示名称字段统一为 `display_name`,移除多语言字段。 - 在 `PlayTypePatchController` 中新增即时切换玩法开关的功能,并推送大厅更新。 - 优化多个控制器和服务中的权限检查与数据处理逻辑,提升代码可读性与维护性。
339 lines
13 KiB
PHP
339 lines
13 KiB
PHP
<?php
|
||
|
||
namespace App\Services\Config;
|
||
|
||
use App\Models\PlayType;
|
||
use App\Models\AdminUser;
|
||
use Illuminate\Http\Request;
|
||
use App\Services\AuditLogger;
|
||
use App\Models\PlayConfigItem;
|
||
use App\Models\PlayConfigVersion;
|
||
use Illuminate\Support\Facades\DB;
|
||
use App\Lottery\ConfigVersionStatus;
|
||
use App\Services\Draw\LotteryHallRealtimeBroadcaster;
|
||
use App\Http\Middleware\RecordAdminApiAudit;
|
||
use Illuminate\Validation\ValidationException;
|
||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||
|
||
/** 后台:玩法配置版本({@see play_config_versions} / {@see play_config_items}) */
|
||
final class PlayConfigStreamService
|
||
{
|
||
public function __construct(
|
||
private readonly LotteryHallRealtimeBroadcaster $hallRealtime,
|
||
) {}
|
||
|
||
/** @return LengthAwarePaginator<int, PlayConfigVersion> */
|
||
public function paginate(?string $status, int $perPage, int $page = 1): LengthAwarePaginator
|
||
{
|
||
$q = PlayConfigVersion::query()->orderByDesc('id');
|
||
if ($status !== null && $status !== '') {
|
||
$q->where('status', $status);
|
||
}
|
||
|
||
return $q->paginate($perPage, ['*'], 'page', max(1, $page));
|
||
}
|
||
|
||
public function createDraft(AdminUser $admin, ?string $reason, ?int $cloneFromVersionId): PlayConfigVersion
|
||
{
|
||
$nextNo = (int) (PlayConfigVersion::query()->max('version_no') ?? 0) + 1;
|
||
|
||
return DB::transaction(function () use ($admin, $reason, $cloneFromVersionId, $nextNo): PlayConfigVersion {
|
||
$draft = PlayConfigVersion::query()->create([
|
||
'version_no' => $nextNo,
|
||
'status' => ConfigVersionStatus::Draft->value,
|
||
'effective_at' => null,
|
||
'updated_by' => $admin->id,
|
||
'reason' => $reason,
|
||
]);
|
||
|
||
$source = null;
|
||
if ($cloneFromVersionId !== null) {
|
||
$source = PlayConfigVersion::query()->whereKey($cloneFromVersionId)->firstOrFail();
|
||
} else {
|
||
$source = PlayConfigVersion::query()
|
||
->where('status', ConfigVersionStatus::Active->value)
|
||
->first();
|
||
}
|
||
|
||
if ($source !== null) {
|
||
foreach ($source->items()->orderBy('play_code')->get() as $row) {
|
||
PlayConfigItem::query()->create([
|
||
'version_id' => $draft->id,
|
||
'play_code' => $row->play_code,
|
||
'category' => $row->category,
|
||
'dimension' => $row->dimension,
|
||
'bet_mode' => $row->bet_mode,
|
||
'display_name' => $row->display_name,
|
||
'is_enabled' => $row->is_enabled,
|
||
'min_bet_amount' => $row->min_bet_amount,
|
||
'max_bet_amount' => $row->max_bet_amount,
|
||
'display_order' => $row->display_order,
|
||
'supports_multi_number' => $row->supports_multi_number,
|
||
'reserved_rule_json' => $row->reserved_rule_json,
|
||
'rule_text_zh' => $row->rule_text_zh,
|
||
'rule_text_en' => $row->rule_text_en,
|
||
'rule_text_ne' => $row->rule_text_ne,
|
||
'extra_config_json' => $row->extra_config_json,
|
||
]);
|
||
}
|
||
} else {
|
||
foreach (PlayType::query()->orderBy('sort_order')->orderBy('play_code')->get() as $pt) {
|
||
PlayConfigItem::query()->create([
|
||
'version_id' => $draft->id,
|
||
'play_code' => $pt->play_code,
|
||
'category' => $pt->category,
|
||
'dimension' => $pt->dimension,
|
||
'bet_mode' => $pt->bet_mode,
|
||
'display_name' => $pt->display_name,
|
||
'is_enabled' => (bool) $pt->is_enabled,
|
||
'min_bet_amount' => 100,
|
||
'max_bet_amount' => 500_000_000,
|
||
'display_order' => (int) $pt->sort_order,
|
||
'supports_multi_number' => (bool) $pt->supports_multi_number,
|
||
'reserved_rule_json' => $pt->reserved_rule_json,
|
||
'rule_text_zh' => null,
|
||
'rule_text_en' => null,
|
||
'rule_text_ne' => null,
|
||
'extra_config_json' => null,
|
||
]);
|
||
}
|
||
}
|
||
|
||
return $draft->fresh(['items']);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* @param array<int, array<string, mixed>> $items
|
||
*/
|
||
/**
|
||
* 即时切换当前生效玩法开关(无需发布草稿),并推送大厅 WS。
|
||
*/
|
||
public function patchActivePlayToggle(AdminUser $admin, string $playCode, bool $enabled, ?Request $request = null): PlayConfigItem
|
||
{
|
||
/** @var PlayConfigVersion $active */
|
||
$active = PlayConfigVersion::query()
|
||
->where('status', ConfigVersionStatus::Active->value)
|
||
->firstOrFail();
|
||
|
||
/** @var PlayConfigItem $item */
|
||
$item = PlayConfigItem::query()
|
||
->where('version_id', $active->id)
|
||
->where('play_code', $playCode)
|
||
->firstOrFail();
|
||
|
||
$before = ['is_enabled' => (bool) $item->is_enabled];
|
||
if ($before['is_enabled'] === $enabled) {
|
||
return $item;
|
||
}
|
||
|
||
DB::transaction(function () use ($item, $enabled, $active, $admin, $playCode): void {
|
||
$item->forceFill(['is_enabled' => $enabled])->save();
|
||
$active->forceFill(['updated_by' => $admin->id])->save();
|
||
|
||
PlayType::query()
|
||
->where('play_code', $playCode)
|
||
->update(['is_enabled' => $enabled]);
|
||
});
|
||
|
||
$this->hallRealtime->notifyPlayToggle($playCode, $enabled, 'play toggle applied to active config');
|
||
|
||
AuditLogger::recordForAdmin(
|
||
$admin,
|
||
$request,
|
||
moduleCode: 'play_config',
|
||
actionCode: 'toggle_active',
|
||
targetType: 'play_config_item',
|
||
targetId: $playCode,
|
||
beforeJson: $before,
|
||
afterJson: ['is_enabled' => $enabled, 'active_version_id' => $active->id],
|
||
);
|
||
$request?->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||
|
||
return $item->refresh();
|
||
}
|
||
|
||
public function replaceItems(PlayConfigVersion $draft, array $items, AdminUser $admin): void
|
||
{
|
||
DB::transaction(function () use ($draft, $items, $admin): void {
|
||
PlayConfigItem::query()->where('version_id', $draft->id)->delete();
|
||
|
||
foreach ($items as $row) {
|
||
PlayConfigItem::query()->create([
|
||
'version_id' => $draft->id,
|
||
'play_code' => (string) $row['play_code'],
|
||
'category' => $row['category'] ?? null,
|
||
'dimension' => $row['dimension'] ?? null,
|
||
'bet_mode' => $row['bet_mode'] ?? null,
|
||
'display_name' => $row['display_name'] ?? null,
|
||
'is_enabled' => (bool) ($row['is_enabled'] ?? true),
|
||
'min_bet_amount' => (int) ($row['min_bet_amount'] ?? 0),
|
||
'max_bet_amount' => (int) ($row['max_bet_amount'] ?? 0),
|
||
'display_order' => (int) ($row['display_order'] ?? 0),
|
||
'supports_multi_number' => (bool) ($row['supports_multi_number'] ?? false),
|
||
'reserved_rule_json' => $row['reserved_rule_json'] ?? null,
|
||
'rule_text_zh' => isset($row['rule_text_zh']) ? (string) $row['rule_text_zh'] : null,
|
||
'rule_text_en' => isset($row['rule_text_en']) ? (string) $row['rule_text_en'] : null,
|
||
'rule_text_ne' => isset($row['rule_text_ne']) ? (string) $row['rule_text_ne'] : null,
|
||
'extra_config_json' => $row['extra_config_json'] ?? null,
|
||
]);
|
||
}
|
||
|
||
$draft->forceFill(['updated_by' => $admin->id])->save();
|
||
});
|
||
}
|
||
|
||
public function publish(PlayConfigVersion $draft, AdminUser $admin, ?Request $request = null): void
|
||
{
|
||
$this->validatePublishableDraft($draft);
|
||
$before = $this->snapshotVersion($draft);
|
||
$currentItems = PlayConfigItem::query()
|
||
->whereIn('version_id', PlayConfigVersion::query()
|
||
->select('id')
|
||
->where('status', ConfigVersionStatus::Active->value))
|
||
->get()
|
||
->keyBy('play_code');
|
||
|
||
DB::transaction(function () use ($draft, $admin): void {
|
||
/** @var PlayConfigVersion|null $current */
|
||
$current = PlayConfigVersion::query()
|
||
->where('status', ConfigVersionStatus::Active->value)
|
||
->lockForUpdate()
|
||
->first();
|
||
|
||
if ($current !== null) {
|
||
$current->forceFill(['status' => ConfigVersionStatus::Archived->value])->save();
|
||
}
|
||
|
||
$draft->forceFill([
|
||
'status' => ConfigVersionStatus::Active->value,
|
||
'effective_at' => now(),
|
||
'updated_by' => $admin->id,
|
||
])->save();
|
||
});
|
||
|
||
$after = $this->snapshotVersion($draft->fresh(['items']));
|
||
$this->broadcastToggleDiffs($currentItems, $draft->items()->get());
|
||
|
||
AuditLogger::recordForAdmin(
|
||
$admin,
|
||
$request,
|
||
moduleCode: 'play_config',
|
||
actionCode: 'publish',
|
||
targetType: 'play_config_version',
|
||
targetId: (string) $draft->id,
|
||
beforeJson: $before,
|
||
afterJson: $after,
|
||
);
|
||
$request?->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||
}
|
||
|
||
/**
|
||
* @param \Illuminate\Support\Collection<string, PlayConfigItem> $currentItems
|
||
* @param \Illuminate\Support\Collection<int, PlayConfigItem> $nextItems
|
||
*/
|
||
private function broadcastToggleDiffs(\Illuminate\Support\Collection $currentItems, \Illuminate\Support\Collection $nextItems): void
|
||
{
|
||
foreach ($nextItems as $next) {
|
||
$current = $currentItems->get($next->play_code);
|
||
if ($current === null || (bool) $current->is_enabled === (bool) $next->is_enabled) {
|
||
continue;
|
||
}
|
||
|
||
$this->hallRealtime->notifyPlayToggle(
|
||
(string) $next->play_code,
|
||
(bool) $next->is_enabled,
|
||
'play config version published',
|
||
);
|
||
}
|
||
}
|
||
|
||
public function deleteVersion(PlayConfigVersion $version, AdminUser $admin, ?Request $request = null): void
|
||
{
|
||
$before = $this->snapshotVersion($version);
|
||
|
||
DB::transaction(function () use ($version): void {
|
||
$version->delete();
|
||
});
|
||
|
||
AuditLogger::recordForAdmin(
|
||
$admin,
|
||
$request,
|
||
moduleCode: 'play_config',
|
||
actionCode: 'delete',
|
||
targetType: 'play_config_version',
|
||
targetId: (string) $version->id,
|
||
beforeJson: $before,
|
||
afterJson: null,
|
||
);
|
||
$request?->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||
}
|
||
|
||
/** @return array<string, mixed> */
|
||
private function snapshotVersion(PlayConfigVersion $v): array
|
||
{
|
||
return [
|
||
'id' => $v->id,
|
||
'version_no' => $v->version_no,
|
||
'status' => $v->status,
|
||
'effective_at' => $v->effective_at?->toIso8601String(),
|
||
'items_count' => $v->items()->count(),
|
||
];
|
||
}
|
||
|
||
private function validatePublishableDraft(PlayConfigVersion $draft): void
|
||
{
|
||
$items = $draft->items()->orderBy('play_code')->get();
|
||
$allowedPlayCodes = array_fill_keys(
|
||
PlayType::query()->pluck('play_code')->all(),
|
||
true,
|
||
);
|
||
|
||
$errors = [];
|
||
$seenPlayCodes = [];
|
||
|
||
if ($items->isEmpty()) {
|
||
$errors['items'][] = '草稿至少需要一条玩法配置';
|
||
}
|
||
|
||
foreach ($items as $index => $row) {
|
||
$playCode = (string) $row->play_code;
|
||
$minBet = (int) $row->min_bet_amount;
|
||
$maxBet = (int) $row->max_bet_amount;
|
||
|
||
if (! isset($allowedPlayCodes[$playCode])) {
|
||
$errors["items.$index.play_code"][] = '玩法不存在';
|
||
}
|
||
|
||
if (isset($seenPlayCodes[$playCode])) {
|
||
$errors["items.$index.play_code"][] = '玩法重复';
|
||
}
|
||
$seenPlayCodes[$playCode] = true;
|
||
|
||
if ($minBet < 0) {
|
||
$errors["items.$index.min_bet_amount"][] = '最小下注额不能小于 0';
|
||
}
|
||
|
||
if ($maxBet < 0) {
|
||
$errors["items.$index.max_bet_amount"][] = '最大下注额不能小于 0';
|
||
}
|
||
|
||
if ($maxBet < $minBet) {
|
||
$errors["items.$index.max_bet_amount"][] = '最大下注额不能小于最小下注额';
|
||
}
|
||
|
||
if ($row->display_name === null || $row->display_name === '') {
|
||
$errors["items.$index.display_name"][] = '显示名称不能为空';
|
||
}
|
||
|
||
if ($row->display_order === null) {
|
||
$errors["items.$index.display_order"][] = '排序不能为空';
|
||
}
|
||
}
|
||
|
||
if ($errors !== []) {
|
||
throw ValidationException::withMessages($errors);
|
||
}
|
||
}
|
||
}
|