*/ public function closePeriod(int $periodId): array { AgentSettlementProductionGuard::assertProductionCloseAllowed(); $period = DB::table('settlement_periods')->where('id', $periodId)->first(); if ($period === null) { throw ValidationException::withMessages([ 'period' => ['period_not_found'], ]); } if ((string) $period->status === 'closed' || (string) $period->status === 'completed') { throw ValidationException::withMessages([ 'period' => ['period_already_closed'], ]); } $adminSiteId = (int) $period->admin_site_id; [$periodStart, $periodEnd] = AgentSettlementPeriodWindow::boundStrings( (string) $period->period_start, (string) $period->period_end, ); try { $aggregate = $this->aggregator->aggregate($adminSiteId, $periodStart, $periodEnd); } catch (\InvalidArgumentException $e) { if (str_starts_with($e->getMessage(), 'share_snapshot_missing')) { throw ValidationException::withMessages([ 'period' => ['share_snapshot_missing'], ]); } throw $e; } $billIds = $this->billGenerator->generate($periodId, $adminSiteId, $aggregate); $roundingDiff = $this->platformRounding->apply($periodId, $aggregate); $rebateStats = $this->periodCloseRebate->dispatchAndAllocate($periodId, $periodStart, $periodEnd); $unsettled = $this->unsettledWarning->countForSite($adminSiteId, $periodStart, $periodEnd); DB::table('settlement_periods')->where('id', $periodId)->update([ 'status' => 'closed', 'updated_at' => now(), ]); $siteCode = (string) DB::table('admin_sites')->where('id', $adminSiteId)->value('code'); DB::table('share_ledger') ->whereIn('id', function ($query) use ($siteCode, $periodStart, $periodEnd): void { $query->select('sl.id') ->from('share_ledger as sl') ->join('players as p', 'p.id', '=', 'sl.player_id') ->where('p.site_code', $siteCode) ->whereNull('sl.settlement_period_id') ->whereBetween('sl.settled_at', [$periodStart, $periodEnd]); }) ->update(['settlement_period_id' => $periodId]); $this->reconcileAllocatedCreditForSite($adminSiteId); return [ 'period_id' => $periodId, 'bill_ids' => $billIds, 'player_count' => count($aggregate['players']), 'agent_edges' => $aggregate['agent_edges'], 'rebate_dispatched' => $rebateStats['dispatched'], 'rebate_allocations' => $rebateStats['allocations'], 'unsettled_ticket_count' => $unsettled['count'], 'unsettled_ticket_sample' => $unsettled['ticket_item_ids'], 'platform_rounding_adjustment' => $roundingDiff, ]; } /** 关账后按真理源重算各代理「已下发额度」,避免与直属玩家/下级代理授信脱节。 */ private function reconcileAllocatedCreditForSite(int $adminSiteId): void { $nodes = AgentNode::query()->where('admin_site_id', $adminSiteId)->get(); foreach ($nodes as $node) { $this->allocatedSync->syncForAgent($node); } } }