feat: 扩展玩法配置快照字段并切换目录生效来源

This commit is contained in:
2026-05-16 10:27:59 +08:00
parent f7f6c58b02
commit 7daf0c3bba
15 changed files with 254 additions and 47 deletions

View File

@@ -37,10 +37,18 @@ final class PlayConfigItemsReplaceController extends Controller
$data = $request->validate([
'items' => ['required', 'array', 'min:1'],
'items.*.play_code' => ['required', 'string', 'max:32', Rule::exists('play_types', 'play_code')],
'items.*.category' => ['required', 'string', 'max:16'],
'items.*.dimension' => ['nullable', 'integer', 'min:0', 'max:255'],
'items.*.bet_mode' => ['nullable', 'string', 'max:32'],
'items.*.display_name_zh' => ['required', 'string', 'max:64'],
'items.*.display_name_en' => ['nullable', 'string', 'max:64'],
'items.*.display_name_ne' => ['nullable', 'string', 'max:64'],
'items.*.is_enabled' => ['sometimes', 'boolean'],
'items.*.min_bet_amount' => ['required', 'integer', 'min:0'],
'items.*.max_bet_amount' => ['required', 'integer', 'min:0'],
'items.*.display_order' => ['sometimes', 'integer'],
'items.*.supports_multi_number' => ['sometimes', 'boolean'],
'items.*.reserved_rule_json' => ['sometimes', 'nullable', 'array'],
'items.*.rule_text_zh' => ['sometimes', 'nullable', 'string'],
'items.*.rule_text_en' => ['sometimes', 'nullable', 'string'],
'items.*.rule_text_ne' => ['sometimes', 'nullable', 'string'],

View File

@@ -11,13 +11,21 @@ final class PlayConfigItem extends Model
protected $fillable = [
'version_id',
'play_code',
'category',
'dimension',
'bet_mode',
'display_name_zh',
'display_name_en',
'display_name_ne',
'is_enabled',
'min_bet_amount',
'max_bet_amount',
'display_order',
'supports_multi_number',
'rule_text_zh',
'rule_text_en',
'rule_text_ne',
'reserved_rule_json',
'extra_config_json',
];
@@ -25,10 +33,13 @@ final class PlayConfigItem extends Model
{
return [
'version_id' => 'integer',
'dimension' => 'integer',
'is_enabled' => 'boolean',
'min_bet_amount' => 'integer',
'max_bet_amount' => 'integer',
'display_order' => 'integer',
'supports_multi_number' => 'boolean',
'reserved_rule_json' => 'json',
'extra_config_json' => 'json',
];
}

View File

@@ -45,7 +45,9 @@ final class PlayConfigVersion extends Model
/** @return HasMany<PlayConfigItem, PlayConfigVersion> */
public function items(): HasMany
{
return $this->hasMany(PlayConfigItem::class, 'version_id');
return $this->hasMany(PlayConfigItem::class, 'version_id')
->orderBy('display_order')
->orderBy('play_code');
}
public function isDraft(): bool

View File

@@ -4,7 +4,6 @@ namespace App\Services\Config;
use App\Models\Currency;
use App\Models\OddsItem;
use App\Models\PlayType;
use App\Models\OddsVersion;
use App\Models\RiskCapItem;
use App\Models\PlayConfigItem;
@@ -37,8 +36,6 @@ final class EffectivePlayCatalogService
->where('status', ConfigVersionStatus::Active->value)
->firstOrFail();
$playTypes = PlayType::query()->orderBy('sort_order')->orderBy('play_code')->get();
/** @var Collection<string, PlayConfigItem> $configByCode */
$configByCode = PlayConfigItem::query()
->where('version_id', $playVersion->id)
@@ -58,26 +55,30 @@ final class EffectivePlayCatalogService
->orderBy('normalized_number')
->get();
$plays = $playTypes->map(function (PlayType $pt) use ($configByCode, $oddsByPlay): array {
$c = $configByCode->get($pt->play_code);
$items = $oddsByPlay->get($pt->play_code, collect());
$o = $this->pickPrimaryOddsItem($items);
$plays = $configByCode->values()
->sortBy([
['display_order', 'asc'],
['play_code', 'asc'],
])
->map(function (PlayConfigItem $c) use ($oddsByPlay): array {
$items = $oddsByPlay->get($c->play_code, collect());
$o = $this->pickPrimaryOddsItem($items);
return [
'play_code' => $pt->play_code,
'category' => $pt->category,
'dimension' => $pt->dimension,
'bet_mode' => $pt->bet_mode,
'display_name_zh' => $pt->display_name_zh,
'display_name_en' => $pt->display_name_en,
'display_name_ne' => $pt->display_name_ne,
'sort_order' => (int) $pt->sort_order,
'supports_multi_number' => (bool) $pt->supports_multi_number,
'master_enabled' => (bool) $pt->is_enabled,
'config' => $c === null ? null : $this->serializePlayConfigItem($c),
'odds' => $o === null ? null : $this->serializeOddsItem($o),
];
})->values()->all();
return [
'play_code' => $c->play_code,
'category' => $c->category,
'dimension' => $c->dimension === null ? null : (int) $c->dimension,
'bet_mode' => $c->bet_mode,
'display_name_zh' => $c->display_name_zh,
'display_name_en' => $c->display_name_en,
'display_name_ne' => $c->display_name_ne,
'sort_order' => (int) $c->display_order,
'supports_multi_number' => (bool) $c->supports_multi_number,
'master_enabled' => (bool) $c->is_enabled,
'config' => $this->serializePlayConfigItem($c),
'odds' => $o === null ? null : $this->serializeOddsItem($o),
];
})->values()->all();
return [
'currency_code' => $currency->code,
@@ -144,10 +145,18 @@ final class EffectivePlayCatalogService
private function serializePlayConfigItem(PlayConfigItem $r): array
{
return [
'category' => $r->category,
'dimension' => $r->dimension === null ? null : (int) $r->dimension,
'bet_mode' => $r->bet_mode,
'display_name_zh' => $r->display_name_zh,
'display_name_en' => $r->display_name_en,
'display_name_ne' => $r->display_name_ne,
'is_enabled' => (bool) $r->is_enabled,
'min_bet_amount' => (int) $r->min_bet_amount,
'max_bet_amount' => (int) $r->max_bet_amount,
'display_order' => (int) $r->display_order,
'supports_multi_number' => (bool) $r->supports_multi_number,
'reserved_rule_json' => $r->reserved_rule_json,
'rule_text_zh' => $r->rule_text_zh,
'rule_text_en' => $r->rule_text_en,
'rule_text_ne' => $r->rule_text_ne,

View File

@@ -54,10 +54,18 @@ final class PlayConfigStreamService
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_zh' => $row->display_name_zh,
'display_name_en' => $row->display_name_en,
'display_name_ne' => $row->display_name_ne,
'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,
@@ -69,10 +77,18 @@ final class PlayConfigStreamService
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_zh' => $pt->display_name_zh,
'display_name_en' => $pt->display_name_en,
'display_name_ne' => $pt->display_name_ne,
'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,
@@ -97,10 +113,18 @@ final class PlayConfigStreamService
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_zh' => $row['display_name_zh'] ?? null,
'display_name_en' => $row['display_name_en'] ?? null,
'display_name_ne' => $row['display_name_ne'] ?? 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,
@@ -221,6 +245,14 @@ final class PlayConfigStreamService
if ($maxBet < $minBet) {
$errors["items.$index.max_bet_amount"][] = '最大下注额不能小于最小下注额';
}
if ($row->display_name_zh === null || $row->display_name_zh === '') {
$errors["items.$index.display_name_zh"][] = '显示名称不能为空';
}
if ($row->display_order === null) {
$errors["items.$index.display_order"][] = '排序不能为空';
}
}
if ($errors !== []) {

View File

@@ -3,7 +3,6 @@
namespace App\Services\Ticket;
use App\Models\OddsItem;
use App\Models\PlayType;
use App\Lottery\ErrorCode;
use App\Models\OddsVersion;
use App\Models\RiskCapItem;
@@ -80,18 +79,10 @@ final class PlayCatalogResolver
}
/**
* @return array{play_type: PlayType, play_config: PlayConfigItem, odds_items: Collection<int, OddsItem>}
* @return array{play_config: PlayConfigItem, odds_items: Collection<int, OddsItem>}
*/
public function resolve(string $playCode, string $currencyCode): array
{
$playType = PlayType::query()->where('play_code', $playCode)->first();
if ($playType === null) {
throw new TicketOperationException('play_not_found', ErrorCode::BetPlayUnsupported->value);
}
if (! $playType->is_enabled) {
throw new TicketOperationException('play_master_disabled', ErrorCode::PlayModeClosed->value);
}
$playVersion = PlayConfigVersion::query()
->where('status', ConfigVersionStatus::Active->value)
->firstOrFail();
@@ -120,7 +111,6 @@ final class PlayCatalogResolver
}
return [
'play_type' => $playType,
'play_config' => $playConfig,
'odds_items' => $oddsItems,
];

View File

@@ -3,7 +3,6 @@
namespace App\Services\Ticket;
use App\Models\OddsItem;
use App\Models\PlayType;
use App\Lottery\ErrorCode;
use App\Models\PlayConfigItem;
use Illuminate\Support\Collection;
@@ -20,7 +19,7 @@ final class PlayRuleEngine
* @param Collection<int, OddsItem> $oddsItems
* @return array<string, mixed>
*/
public function evaluateLine(array $line, PlayType $playType, PlayConfigItem $playConfig, Collection $oddsItems): array
public function evaluateLine(array $line, PlayConfigItem $playConfig, Collection $oddsItems): array
{
$playCode = (string) $line['play_code'];
$dimension = $line['dimension'] ?? null;
@@ -61,9 +60,9 @@ final class PlayRuleEngine
'original_number' => (string) $line['number'],
'normalized_number' => $number,
'play_code' => $playCode,
'dimension' => $this->toDimensionInt(is_string($dimension) ? $dimension : null, $playType),
'dimension' => $this->toDimensionInt(is_string($dimension) ? $dimension : null, $playConfig),
'digit_slot' => $digitSlotInt,
'bet_mode' => $playType->bet_mode,
'bet_mode' => $playConfig->bet_mode,
'unit_bet_amount' => $unitBetAmount,
'total_bet_amount' => $totalBetAmount,
'rebate_rate_snapshot' => number_format($rebateRate, 4, '.', ''),
@@ -300,13 +299,13 @@ final class PlayRuleEngine
return $oddsItems->firstOrFail();
}
private function toDimensionInt(?string $dimension, PlayType $playType): ?int
private function toDimensionInt(?string $dimension, PlayConfigItem $playConfig): ?int
{
return match ($dimension) {
'D2' => 2,
'D3' => 3,
'D4' => 4,
default => $playType->dimension === null ? null : (int) $playType->dimension,
default => $playConfig->dimension === null ? null : (int) $playConfig->dimension,
};
}
}

View File

@@ -83,7 +83,6 @@ final class TicketPlacementService
}
$evaluated = $this->ruleEngine->evaluateLine(
(array) $line,
$resolved['play_type'],
$resolved['play_config'],
$resolved['odds_items'],
);

View File

@@ -54,7 +54,6 @@ final class TicketPreviewService
}
$evaluated = $this->ruleEngine->evaluateLine(
(array) $line,
$resolved['play_type'],
$resolved['play_config'],
$resolved['odds_items'],
);

View File

@@ -63,10 +63,18 @@ final class AdminConfigPresenter
return [
'id' => (int) $r->id,
'play_code' => $r->play_code,
'category' => $r->category,
'dimension' => $r->dimension === null ? null : (int) $r->dimension,
'bet_mode' => $r->bet_mode,
'display_name_zh' => $r->display_name_zh,
'display_name_en' => $r->display_name_en,
'display_name_ne' => $r->display_name_ne,
'is_enabled' => (bool) $r->is_enabled,
'min_bet_amount' => (int) $r->min_bet_amount,
'max_bet_amount' => (int) $r->max_bet_amount,
'display_order' => (int) $r->display_order,
'supports_multi_number' => (bool) $r->supports_multi_number,
'reserved_rule_json' => $r->reserved_rule_json,
'rule_text_zh' => $r->rule_text_zh,
'rule_text_en' => $r->rule_text_en,
'rule_text_ne' => $r->rule_text_ne,