feat(draw): 支持多玩法provider及本地化结算时区功能
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
lotterLaravel E2E / e2e-api (push) Has been cancelled

- Draw相关控制器添加provider_code和provider_name字段支持
- 允许手动录入开奖批次时指定provider_code
- RNG开奖批次生成支持多provider,分别生成对应批次数据
- DrawPublishService和DrawManualResultService中支持基于provider_code管理开奖版本和发布流程
- DrawResultViewService调整,支持按provider汇总及筛选开奖结果
- Settlement处理逻辑调整,按provider区分待结算票据及批次,分开结算
- 结算期间管理相关服务支持使用站点本地结算时区计算开账建议和日期处理
- 增加AdminSite的settlement_timezone字段及相关请求验证和数据存取支持
- 优化AdminSettlementPeriod相关代码,防止存在未结清票据时关账
- Ticket明细返回接口添加结算时区信息,提升用户时间体验
- 多处查询排序和版本号处理改进,保证多provider数据正确顺序与一致性
This commit is contained in:
2026-07-09 15:48:21 +08:00
parent c6c10f1397
commit d4779660c8
38 changed files with 1121 additions and 271 deletions

View File

@@ -3,26 +3,33 @@
namespace App\Services\AgentSettlement;
use Carbon\Carbon;
use App\Support\AgentSettlementSiteTimezone;
use Illuminate\Support\Facades\DB;
/** 开账弹窗:建议账期与日历标记(已有账期 / 待入账 / 未结清)。 */
/** 开账弹窗:建议账期与日历标记(已有账期 / 待入账 / 未结清 / 已关账漏账)。 */
final class SettlementPeriodOpenHintsService
{
/**
* @return array{
* suggested_start: string,
* suggested_end: string,
* settlement_timezone: string,
* occupied_period_dates: list<string>,
* pending_activity_dates: list<string>,
* orphan_activity_dates: list<string>,
* unpaid_bill_dates: list<string>
* }
*/
public function hints(int $adminSiteId): array
{
$siteCode = (string) DB::table('admin_sites')->where('id', $adminSiteId)->value('code');
$site = DB::table('admin_sites')->where('id', $adminSiteId)->first(['code', 'settlement_timezone']);
$siteCode = (string) ($site->code ?? '');
if ($siteCode === '') {
return $this->emptyHints();
}
$timezone = AgentSettlementSiteTimezone::isValid((string) ($site->settlement_timezone ?? ''))
? (string) $site->settlement_timezone
: AgentSettlementSiteTimezone::forSiteId($adminSiteId);
$periodRows = DB::table('settlement_periods')
->where('admin_site_id', $adminSiteId)
@@ -31,7 +38,7 @@ final class SettlementPeriodOpenHintsService
$occupiedPeriodDates = [];
foreach ($periodRows as $row) {
foreach ($this->expandPeriodToUtcDays((string) $row->period_start, (string) $row->period_end) as $day) {
foreach ($this->expandPeriodToLocalDays((string) $row->period_start, (string) $row->period_end, $timezone) as $day) {
$occupiedPeriodDates[$day] = true;
}
}
@@ -42,18 +49,25 @@ final class SettlementPeriodOpenHintsService
->orderByDesc('period_end')
->first();
$pendingActivityDates = DB::table('share_ledger as sl')
$pendingLedgerRows = DB::table('share_ledger as sl')
->join('players as p', 'p.id', '=', 'sl.player_id')
->where('p.site_code', $siteCode)
->whereNull('sl.settlement_period_id')
->whereNull('sl.reversal_of_id')
->selectRaw('DATE(sl.settled_at) as activity_day')
->groupBy('activity_day')
->orderBy('activity_day')
->pluck('activity_day')
->map(static fn ($day): string => (string) $day)
->values()
->all();
->orderBy('sl.settled_at')
->get(['sl.settled_at']);
$pendingActivityDates = [];
$orphanActivityDates = [];
foreach ($pendingLedgerRows as $row) {
$day = AgentSettlementSiteTimezone::localDayFromUtc($row->settled_at, $timezone);
$pendingActivityDates[$day] = true;
if (isset($occupiedPeriodDates[$day])) {
$orphanActivityDates[$day] = true;
}
}
$pendingActivityDates = array_keys($pendingActivityDates);
$orphanActivityDates = array_keys($orphanActivityDates);
$unpaidPeriodRows = DB::table('settlement_periods as sp')
->where('sp.admin_site_id', $adminSiteId)
@@ -70,18 +84,20 @@ final class SettlementPeriodOpenHintsService
$unpaidBillDates = [];
foreach ($unpaidPeriodRows as $row) {
foreach ($this->expandPeriodToUtcDays((string) $row->period_start, (string) $row->period_end) as $day) {
foreach ($this->expandPeriodToLocalDays((string) $row->period_start, (string) $row->period_end, $timezone) as $day) {
$unpaidBillDates[$day] = true;
}
}
$suggested = $this->suggestRange($lastPeriod, $pendingActivityDates, $occupiedPeriodDates);
$suggested = $this->suggestRange($lastPeriod, $pendingActivityDates, $occupiedPeriodDates, $timezone);
return [
'suggested_start' => $suggested['start'],
'suggested_end' => $suggested['end'],
'settlement_timezone' => $timezone,
'occupied_period_dates' => array_keys($occupiedPeriodDates),
'pending_activity_dates' => $pendingActivityDates,
'orphan_activity_dates' => $orphanActivityDates,
'unpaid_bill_dates' => array_keys($unpaidBillDates),
];
}
@@ -91,10 +107,15 @@ final class SettlementPeriodOpenHintsService
* @param array<string, true> $occupiedPeriodDates
* @return array{start: string, end: string}
*/
private function suggestRange(?object $lastPeriod, array $pendingActivityDates, array $occupiedPeriodDates): array
private function suggestRange(
?object $lastPeriod,
array $pendingActivityDates,
array $occupiedPeriodDates,
string $timezone,
): array
{
$lastEndDay = $lastPeriod !== null
? Carbon::parse((string) $lastPeriod->period_end)->utc()->startOfDay()
? Carbon::parse((string) $lastPeriod->period_end, 'UTC')->timezone($timezone)->startOfDay()
: null;
$freePending = array_values(array_filter(
@@ -103,8 +124,8 @@ final class SettlementPeriodOpenHintsService
));
if ($freePending !== []) {
$minDay = Carbon::parse($freePending[0])->utc()->startOfDay();
$maxDay = Carbon::parse($freePending[array_key_last($freePending)])->utc()->startOfDay();
$minDay = Carbon::parse($freePending[0])->startOfDay();
$maxDay = Carbon::parse($freePending[array_key_last($freePending)])->startOfDay();
$startDay = $lastEndDay !== null
? ($lastEndDay->copy()->addDay()->lessThanOrEqualTo($minDay) ? $lastEndDay->copy()->addDay() : $minDay)
: $minDay;
@@ -119,7 +140,7 @@ final class SettlementPeriodOpenHintsService
if ($lastEndDay !== null) {
$startDay = $lastEndDay->copy()->addDay();
$endDay = Carbon::now('UTC')->subDay()->startOfDay();
$endDay = Carbon::now($timezone)->subDay()->startOfDay();
if ($endDay->lessThan($startDay)) {
return ['start' => '', 'end' => ''];
}
@@ -169,13 +190,12 @@ final class SettlementPeriodOpenHintsService
return false;
}
/** @return list<string> 站点本地日历 `Y-m-d`(东八区,与后台开账日期选择一致) */
private function expandPeriodToUtcDays(string $periodStart, string $periodEnd): array
/** @return list<string> 站点本地日历 `Y-m-d` */
private function expandPeriodToLocalDays(string $periodStart, string $periodEnd, string $timezone): array
{
$dates = [];
$tz = 'Asia/Shanghai';
$cursor = Carbon::parse($periodStart)->timezone($tz)->startOfDay();
$end = Carbon::parse($periodEnd)->timezone($tz)->startOfDay();
$cursor = Carbon::parse($periodStart, 'UTC')->timezone($timezone)->startOfDay();
$end = Carbon::parse($periodEnd, 'UTC')->timezone($timezone)->startOfDay();
while ($cursor->lessThanOrEqualTo($end)) {
$dates[] = $cursor->format('Y-m-d');
@@ -185,14 +205,16 @@ final class SettlementPeriodOpenHintsService
return $dates;
}
/** @return array{suggested_start: string, suggested_end: string, occupied_period_dates: list<string>, pending_activity_dates: list<string>, unpaid_bill_dates: list<string>} */
/** @return array{suggested_start: string, suggested_end: string, settlement_timezone: string, occupied_period_dates: list<string>, pending_activity_dates: list<string>, orphan_activity_dates: list<string>, unpaid_bill_dates: list<string>} */
private function emptyHints(): array
{
return [
'suggested_start' => '',
'suggested_end' => '',
'settlement_timezone' => (string) config('lottery.settlement.default_timezone', 'UTC'),
'occupied_period_dates' => [],
'pending_activity_dates' => [],
'orphan_activity_dates' => [],
'unpaid_bill_dates' => [],
];
}