Files
lotteryLaravel/app/Services/Draw/DrawManualUpdateService.php
kang c74bec3f64 feat: 增强抽奖管理功能,支持手动创建、更新和删除期号
- 新增 API 路由和控制器,允许管理员手动创建、更新和删除抽奖期号。
- 更新抽奖调度逻辑,确保在抽奖时间和封盘时间的管理上更加灵活。
- 添加多语言支持的错误信息,提升用户体验。
- 更新测试用例,确保新功能的正确性和稳定性。
2026-05-25 18:00:22 +08:00

113 lines
4.0 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 Carbon\Carbon;
use App\Models\Draw;
use App\Models\TicketOrder;
use App\Lottery\DrawStatus;
use Illuminate\Support\Facades\DB;
/**
* 管理员修改未开奖期号的时间轴(仅 pending / 无注单的 open
*/
final class DrawManualUpdateService
{
public function __construct(
private readonly DrawTimelineBuilder $timeline,
) {}
/**
* @param array{
* draw_time: string,
* start_time?: string|null,
* close_time?: string|null,
* draw_no?: string|null,
* business_date?: string|null,
* sequence_no?: int|null,
* } $input
*/
public function update(Draw $draw, array $input, ?Carbon $now = null): Draw
{
$tz = (string) config('lottery.draw.timezone', 'UTC');
$nowUtc = ($now ?? Carbon::now())->utc();
return DB::transaction(function () use ($draw, $input, $tz, $nowUtc): Draw {
/** @var Draw $locked */
$locked = Draw::query()->whereKey($draw->id)->lockForUpdate()->firstOrFail();
$this->assertEditable($locked);
$drawLocal = Carbon::parse((string) $input['draw_time'], $tz);
$startLocal = isset($input['start_time']) && $input['start_time'] !== null && $input['start_time'] !== ''
? Carbon::parse((string) $input['start_time'], $tz)
: null;
$closeLocal = isset($input['close_time']) && $input['close_time'] !== null && $input['close_time'] !== ''
? Carbon::parse((string) $input['close_time'], $tz)
: null;
if ($startLocal === null || $closeLocal === null) {
$defaults = $this->timeline->windowsFromDrawLocal($drawLocal);
$startLocal ??= $defaults['start_local'];
$closeLocal ??= $defaults['close_local'];
}
if (! $startLocal->lt($closeLocal) || ! $closeLocal->lt($drawLocal)) {
throw new \RuntimeException('draw_timeline_invalid');
}
$businessDate = isset($input['business_date']) && $input['business_date'] !== ''
? (string) $input['business_date']
: $drawLocal->format('Y-m-d');
$sequenceNo = isset($input['sequence_no']) && $input['sequence_no'] !== null
? max(1, (int) $input['sequence_no'])
: (int) $locked->sequence_no;
$drawNo = isset($input['draw_no']) && trim((string) $input['draw_no']) !== ''
? trim((string) $input['draw_no'])
: (string) $locked->draw_no;
if (
Draw::query()
->where('draw_no', $drawNo)
->where('id', '!=', $locked->id)
->exists()
) {
throw new \RuntimeException('draw_no_exists');
}
$built = $this->timeline->buildFromLocals($startLocal, $closeLocal, $drawLocal, $nowUtc);
$locked->forceFill([
'draw_no' => $drawNo,
'business_date' => $businessDate,
'sequence_no' => $sequenceNo,
'status' => $built['status'],
'start_time' => $built['start_utc'],
'close_time' => $built['close_utc'],
'draw_time' => $built['draw_utc'],
])->save();
return $locked->refresh();
});
}
private function assertEditable(Draw $draw): void
{
if ($draw->resultBatches()->exists()) {
throw new \RuntimeException('draw_result_exists');
}
if (! in_array($draw->status, [DrawStatus::Pending->value, DrawStatus::Open->value], true)) {
throw new \RuntimeException('draw_not_editable');
}
if ($draw->status === DrawStatus::Open->value) {
$betTotal = (int) TicketOrder::query()->where('draw_id', $draw->id)->sum('total_actual_deduct');
if ($betTotal > 0) {
throw new \RuntimeException('draw_has_bets');
}
}
}
}