|
|
<?php
|
|
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
use App\Models\Activity;
|
|
|
use App\Models\Blacklist;
|
|
|
use App\Models\Reservation;
|
|
|
use App\Models\TicketGrabEvent;
|
|
|
use App\Models\TicketGrabEventVenue;
|
|
|
use App\Models\Venue;
|
|
|
use App\Services\DashboardTicketGrabStatsService;
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
use Illuminate\Http\Request;
|
|
|
use Illuminate\Support\Collection;
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
use Illuminate\Support\Facades\Http;
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
|
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
|
|
class DashboardController extends Controller
|
|
|
{
|
|
|
public function stats(Request $request): JsonResponse
|
|
|
{
|
|
|
$user = $request->user();
|
|
|
$allowedVenueIds = $user->isSuperAdmin()
|
|
|
? Venue::query()->pluck('id')
|
|
|
: $user->venues()->pluck('venues.id');
|
|
|
|
|
|
$selectedVenueId = $request->filled('venue_id') ? (int) $request->input('venue_id') : null;
|
|
|
$venueIds = $allowedVenueIds;
|
|
|
if ($selectedVenueId) {
|
|
|
$venueIds = $venueIds->filter(fn ($id) => (int) $id === $selectedVenueId)->values();
|
|
|
}
|
|
|
|
|
|
$activityId = $request->filled('activity_id') ? (int) $request->input('activity_id') : null;
|
|
|
if ($activityId) {
|
|
|
$allowed = Activity::query()
|
|
|
->whereKey($activityId)
|
|
|
->whereNull('deleted_at')
|
|
|
->whereIn('venue_id', $venueIds)
|
|
|
->exists();
|
|
|
if (! $allowed) {
|
|
|
return response()->json(['message' => '活动不存在或无权查看'], 422);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 顶部指标:账号可见场馆范围内的全量累计(不受「筛选场馆」影响) */
|
|
|
$scopeVenueIds = $allowedVenueIds;
|
|
|
|
|
|
$userCountBase = DB::table('reservations')
|
|
|
->whereIn('venue_id', $scopeVenueIds)
|
|
|
->whereNotNull('wechat_user_id')
|
|
|
->whereNull('deleted_at');
|
|
|
/** 场馆管理员:仅统计预约了本场馆「活动」的用户(不含抢票等) */
|
|
|
if ($user->role === 'venue_admin') {
|
|
|
$userCountBase->where(function ($q) {
|
|
|
$q->whereNull('reservation_kind')
|
|
|
->orWhere('reservation_kind', Reservation::KIND_ACTIVITY);
|
|
|
});
|
|
|
}
|
|
|
$userCount = (int) $userCountBase->selectRaw('COUNT(DISTINCT wechat_user_id) as c')->value('c');
|
|
|
|
|
|
$activitySessions = Activity::query()
|
|
|
->whereIn('venue_id', $scopeVenueIds)
|
|
|
->whereNull('deleted_at')
|
|
|
->count();
|
|
|
|
|
|
$ticketGrabSessions = TicketGrabEvent::query()
|
|
|
->whereHas('venues', fn ($vq) => $vq->whereIn('venues.id', $scopeVenueIds))
|
|
|
->count();
|
|
|
|
|
|
$blacklistedUniqueQuery = Blacklist::query()
|
|
|
->active()
|
|
|
->whereIn('venue_id', $scopeVenueIds)
|
|
|
->whereNotNull('visitor_phone')
|
|
|
->whereRaw("TRIM(visitor_phone) <> ''")
|
|
|
->whereExists(function ($q) use ($scopeVenueIds, $user) {
|
|
|
$q->selectRaw('1')
|
|
|
->from('reservations')
|
|
|
->whereNull('reservations.deleted_at')
|
|
|
->whereColumn('reservations.visitor_phone', 'blacklists.visitor_phone')
|
|
|
->whereIn('reservations.venue_id', $scopeVenueIds);
|
|
|
|
|
|
if ($user->isSuperAdmin() && Schema::hasTable('wechat_users')) {
|
|
|
$q->whereNotNull('reservations.wechat_user_id')
|
|
|
->whereExists(function ($wq) {
|
|
|
$wq->selectRaw('1')
|
|
|
->from('wechat_users')
|
|
|
->whereColumn('wechat_users.id', 'reservations.wechat_user_id');
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
$blacklistedUnique = $blacklistedUniqueQuery
|
|
|
->distinct('visitor_phone')
|
|
|
->count('visitor_phone');
|
|
|
|
|
|
$venuesCount = $scopeVenueIds->isEmpty()
|
|
|
? 0
|
|
|
: (int) Venue::query()->whereIn('id', $scopeVenueIds)->count();
|
|
|
|
|
|
/** 与核心指标同范围:账号可见全部场馆(不受当前筛选场馆影响) */
|
|
|
$activityScheduleBase = Activity::query()
|
|
|
->whereIn('venue_id', $scopeVenueIds)
|
|
|
->whereNull('deleted_at');
|
|
|
$activityScheduleCounts = [
|
|
|
'total' => (clone $activityScheduleBase)->count(),
|
|
|
'not_started' => (clone $activityScheduleBase)->whereComputedScheduleStatus('not_started')->count(),
|
|
|
'ongoing' => (clone $activityScheduleBase)->whereComputedScheduleStatus('ongoing')->count(),
|
|
|
'ended' => (clone $activityScheduleBase)->whereComputedScheduleStatus('ended')->count(),
|
|
|
];
|
|
|
|
|
|
$ticketGrabScheduleBase = TicketGrabEvent::query()
|
|
|
->whereHas('venues', fn ($vq) => $vq->whereIn('venues.id', $scopeVenueIds));
|
|
|
$ticketGrabEventIdsForScope = (clone $ticketGrabScheduleBase)->pluck('id');
|
|
|
$verifiedPeople = 0;
|
|
|
$bookedPeople = 0;
|
|
|
if ($ticketGrabEventIdsForScope->isNotEmpty()) {
|
|
|
$tgReservations = Reservation::query()
|
|
|
->where('reservation_kind', Reservation::KIND_TICKET_GRAB)
|
|
|
->whereIn('ticket_grab_event_id', $ticketGrabEventIdsForScope)
|
|
|
->where('status', '!=', 'cancelled')
|
|
|
->whereNull('deleted_at')
|
|
|
->get(['ticket_count', 'verified_at']);
|
|
|
$bookedPeople = (int) $tgReservations->sum(fn (Reservation $r) => max(1, (int) ($r->ticket_count ?? 1)));
|
|
|
$verifiedPeople = (int) $tgReservations
|
|
|
->filter(fn (Reservation $r) => $r->verified_at !== null)
|
|
|
->sum(fn (Reservation $r) => max(1, (int) ($r->ticket_count ?? 1)));
|
|
|
}
|
|
|
$verifyRatePct = $bookedPeople > 0 ? round(100 * $verifiedPeople / $bookedPeople, 1) : null;
|
|
|
|
|
|
$ticketGrabScheduleCounts = [
|
|
|
'total' => (clone $ticketGrabScheduleBase)->count(),
|
|
|
'not_started' => (clone $ticketGrabScheduleBase)->whereComputedScheduleStatus('not_started')->count(),
|
|
|
'ongoing' => (clone $ticketGrabScheduleBase)->whereComputedScheduleStatus('ongoing')->count(),
|
|
|
'ended' => (clone $ticketGrabScheduleBase)->whereComputedScheduleStatus('ended')->count(),
|
|
|
'verify_rate_pct' => $verifyRatePct,
|
|
|
'verified_people' => $verifiedPeople,
|
|
|
'booked_people' => $bookedPeople,
|
|
|
];
|
|
|
|
|
|
$baseActivityQuery = Activity::query()
|
|
|
->whereIn('venue_id', $venueIds)
|
|
|
->whereNull('deleted_at');
|
|
|
if ($activityId !== null) {
|
|
|
$baseActivityQuery->whereKey($activityId);
|
|
|
}
|
|
|
|
|
|
$totalViews = (int) (clone $baseActivityQuery)->sum('view_count');
|
|
|
|
|
|
$page = max(1, (int) $request->input('activity_stats_page', 1));
|
|
|
$pageSize = max(1, min(2000, (int) $request->input('activity_stats_page_size', 500)));
|
|
|
|
|
|
$totalActivities = (clone $baseActivityQuery)->count();
|
|
|
$activityRows = (clone $baseActivityQuery)
|
|
|
->orderByDesc('view_count')
|
|
|
->orderByDesc('id')
|
|
|
->forPage($page, $pageSize)
|
|
|
->get(['id', 'title', 'venue_id', 'start_at', 'end_at', 'view_count']);
|
|
|
|
|
|
$venueIdsForNames = $activityRows->pluck('venue_id')->unique()->filter()->values()->all();
|
|
|
$venueNameMap = $venueIdsForNames === []
|
|
|
? collect()
|
|
|
: Venue::query()->whereIn('id', $venueIdsForNames)->pluck('name', 'id');
|
|
|
|
|
|
$activityStatsActivities = $activityRows->map(function ($a) use ($venueNameMap) {
|
|
|
$vid = (int) $a->venue_id;
|
|
|
|
|
|
return [
|
|
|
'id' => (int) $a->id,
|
|
|
'title' => (string) $a->title,
|
|
|
'venue_id' => $vid,
|
|
|
'venue_name' => (string) ($venueNameMap[$vid] ?? ('#'.$vid)),
|
|
|
'view_count' => (int) ($a->view_count ?? 0),
|
|
|
'start_at' => $a->start_at,
|
|
|
'end_at' => $a->end_at,
|
|
|
];
|
|
|
})->values()->all();
|
|
|
|
|
|
$pendingAudits = null;
|
|
|
if ($user->isSuperAdmin()) {
|
|
|
$activityPendingCount = (int) Activity::query()
|
|
|
->whereNull('deleted_at')
|
|
|
->where('audit_status', Activity::AUDIT_PENDING)
|
|
|
->count();
|
|
|
|
|
|
$activityItems = Activity::query()
|
|
|
->whereNull('deleted_at')
|
|
|
->where('audit_status', Activity::AUDIT_PENDING)
|
|
|
->with(['venue:id,name'])
|
|
|
->orderByDesc('updated_at')
|
|
|
->limit(8)
|
|
|
->get(['id', 'title', 'venue_id', 'updated_at'])
|
|
|
->map(function (Activity $a) {
|
|
|
return [
|
|
|
'id' => $a->id,
|
|
|
'title' => $a->title,
|
|
|
'venue_name' => (string) ($a->venue?->name ?? ''),
|
|
|
'updated_at' => $a->updated_at,
|
|
|
];
|
|
|
})
|
|
|
->values()
|
|
|
->all();
|
|
|
|
|
|
$pendingAudits = [
|
|
|
'activities' => [
|
|
|
'count' => $activityPendingCount,
|
|
|
'items' => $activityItems,
|
|
|
],
|
|
|
];
|
|
|
} elseif ($user->role === 'venue_admin') {
|
|
|
/** 待办:本账号场馆下、审核已退回的活动(供前台修改后重新提交) */
|
|
|
if ($scopeVenueIds->isEmpty()) {
|
|
|
$pendingAudits = [
|
|
|
'activities' => [
|
|
|
'count' => 0,
|
|
|
'items' => [],
|
|
|
],
|
|
|
];
|
|
|
} else {
|
|
|
$rejectedCount = (int) Activity::query()
|
|
|
->whereIn('venue_id', $scopeVenueIds)
|
|
|
->whereNull('deleted_at')
|
|
|
->where('audit_status', Activity::AUDIT_REJECTED)
|
|
|
->count();
|
|
|
|
|
|
$rejectedItems = Activity::query()
|
|
|
->whereIn('venue_id', $scopeVenueIds)
|
|
|
->whereNull('deleted_at')
|
|
|
->where('audit_status', Activity::AUDIT_REJECTED)
|
|
|
->with(['venue:id,name'])
|
|
|
->orderByDesc('updated_at')
|
|
|
->limit(8)
|
|
|
->get(['id', 'title', 'venue_id', 'updated_at'])
|
|
|
->map(function (Activity $a) {
|
|
|
return [
|
|
|
'id' => $a->id,
|
|
|
'title' => $a->title,
|
|
|
'venue_name' => (string) ($a->venue?->name ?? ''),
|
|
|
'updated_at' => $a->updated_at,
|
|
|
];
|
|
|
})
|
|
|
->values()
|
|
|
->all();
|
|
|
|
|
|
$pendingAudits = [
|
|
|
'activities' => [
|
|
|
'count' => $rejectedCount,
|
|
|
'items' => $rejectedItems,
|
|
|
],
|
|
|
];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** 场馆管理员的排行榜口径与超级管理员一致(全场馆)。 */
|
|
|
$rankingScopeVenueIds = $user->role === 'venue_admin'
|
|
|
? Venue::query()->pluck('id')
|
|
|
: $scopeVenueIds;
|
|
|
|
|
|
$activityPublishRanking = $this->buildActivityPublishRanking($rankingScopeVenueIds);
|
|
|
$livePeoplePayload = $this->fetchLivePeopleMapWithStatus();
|
|
|
$livePeopleRanking = $this->buildLivePeopleRanking($rankingScopeVenueIds, $livePeoplePayload['map']);
|
|
|
|
|
|
return response()->json([
|
|
|
'scope' => [
|
|
|
'role' => $user->role,
|
|
|
'venue_id' => $selectedVenueId,
|
|
|
'activity_id' => $activityId,
|
|
|
],
|
|
|
'summary' => [
|
|
|
'activity_sessions' => (int) $activitySessions,
|
|
|
'venues_count' => $venuesCount,
|
|
|
'ticket_grab_sessions' => (int) $ticketGrabSessions,
|
|
|
'user_count' => (int) $userCount,
|
|
|
'blacklisted_unique' => (int) $blacklistedUnique,
|
|
|
],
|
|
|
'pending_audits' => $pendingAudits,
|
|
|
'activity_schedule_counts' => $activityScheduleCounts,
|
|
|
'ticket_grab_schedule_counts' => $ticketGrabScheduleCounts,
|
|
|
'activity_stats' => [
|
|
|
'total_view_count' => $totalViews,
|
|
|
],
|
|
|
'activity_stats_activities' => [
|
|
|
'data' => $activityStatsActivities,
|
|
|
'total' => $totalActivities,
|
|
|
'page' => $page,
|
|
|
'page_size' => $pageSize,
|
|
|
],
|
|
|
'activity_publish_ranking' => $activityPublishRanking,
|
|
|
'live_people_ranking' => $livePeopleRanking,
|
|
|
'live_people_counting_ok' => $livePeoplePayload['ok'],
|
|
|
'live_people_counting_debug' => $livePeoplePayload['debug'],
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param Collection<int, mixed> $scopeVenueIds
|
|
|
* @return list<array{venue_id: int, venue_name: string, published_count: int}>
|
|
|
*/
|
|
|
private function buildActivityPublishRanking(Collection $scopeVenueIds): array
|
|
|
{
|
|
|
if ($scopeVenueIds->isEmpty()) {
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
return Venue::query()
|
|
|
->whereIn('id', $scopeVenueIds)
|
|
|
->withCount(['activities as published_count'])
|
|
|
->having('published_count', '>', 0)
|
|
|
->orderByDesc('published_count')
|
|
|
->orderBy('id')
|
|
|
->limit(50)
|
|
|
->get(['id', 'name'])
|
|
|
->map(fn (Venue $v) => [
|
|
|
'venue_id' => (int) $v->id,
|
|
|
'venue_name' => (string) $v->name,
|
|
|
'published_count' => (int) $v->published_count,
|
|
|
])
|
|
|
->values()
|
|
|
->all();
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @return array{ok: bool, map: array<int, int>, debug: array<string, mixed>|null}
|
|
|
*/
|
|
|
private function fetchLivePeopleMapWithStatus(): array
|
|
|
{
|
|
|
$url = config('services.people_counting.url');
|
|
|
if (! is_string($url) || trim($url) === '') {
|
|
|
return [
|
|
|
'ok' => false,
|
|
|
'map' => [],
|
|
|
'debug' => [
|
|
|
'stage' => 'config',
|
|
|
'message' => '未配置 PEOPLE_COUNTING_URL(services.people_counting.url)',
|
|
|
],
|
|
|
];
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
$res = Http::timeout(12)->acceptJson()->get($url);
|
|
|
$status = $res->status();
|
|
|
$body = $res->body();
|
|
|
$json = $res->json();
|
|
|
|
|
|
$map = $this->parsePeopleCountingVenueLiveMap(is_array($json) ? $json : null);
|
|
|
$ok = $res->successful() && is_array($json) && (int) ($json['code'] ?? 0) === 200;
|
|
|
|
|
|
if ($ok) {
|
|
|
return ['ok' => true, 'map' => $map, 'debug' => null];
|
|
|
}
|
|
|
|
|
|
return [
|
|
|
'ok' => false,
|
|
|
'map' => $map,
|
|
|
'debug' => [
|
|
|
'stage' => 'remote_response',
|
|
|
'request_url' => $url,
|
|
|
'http_status' => $status,
|
|
|
'http_ok' => $res->successful(),
|
|
|
'parsed_code' => is_array($json) ? ($json['code'] ?? null) : null,
|
|
|
'parsed_message' => is_array($json) ? ($json['message'] ?? null) : null,
|
|
|
'response_json' => is_array($json) ? $json : null,
|
|
|
'response_body_preview' => mb_substr($body, 0, 6000),
|
|
|
],
|
|
|
];
|
|
|
} catch (\Throwable $e) {
|
|
|
return [
|
|
|
'ok' => false,
|
|
|
'map' => [],
|
|
|
'debug' => [
|
|
|
'stage' => 'exception',
|
|
|
'request_url' => is_string($url) ? $url : null,
|
|
|
'exception_class' => $e::class,
|
|
|
'message' => $e->getMessage(),
|
|
|
],
|
|
|
];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @param array<string, mixed>|null $json
|
|
|
* @return array<int, int> venueId => max(0, enter - exit)
|
|
|
*/
|
|
|
private function parsePeopleCountingVenueLiveMap(?array $json): array
|
|
|
{
|
|
|
if (! is_array($json) || ! is_array($json['venues'] ?? null)) {
|
|
|
return [];
|
|
|
}
|
|
|
$out = [];
|
|
|
foreach ($json['venues'] as $row) {
|
|
|
if (! is_array($row)) {
|
|
|
continue;
|
|
|
}
|
|
|
$id = (int) trim((string) ($row['venueId'] ?? ''));
|
|
|
if ($id <= 0) {
|
|
|
continue;
|
|
|
}
|
|
|
$enter = (int) ($row['enter'] ?? 0);
|
|
|
$exit = (int) ($row['exit'] ?? 0);
|
|
|
$out[$id] = max(0, $enter - $exit);
|
|
|
}
|
|
|
|
|
|
return $out;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 与 H5 /pages/stats/index 一致:仅「纳入人数统计」的场馆;仅返回在馆人数大于 0 的条目,按人数降序.
|
|
|
*
|
|
|
* @param Collection<int, mixed> $scopeVenueIds
|
|
|
* @param array<int, int> $liveByVenueId
|
|
|
* @return list<array{venue_id: int, venue_name: string, live_count: int}>
|
|
|
*/
|
|
|
private function buildLivePeopleRanking(Collection $scopeVenueIds, array $liveByVenueId): array
|
|
|
{
|
|
|
if ($scopeVenueIds->isEmpty()) {
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
$venues = Venue::query()
|
|
|
->whereIn('id', $scopeVenueIds)
|
|
|
->where('is_included_in_stats', true)
|
|
|
->orderBy('sort')
|
|
|
->orderByDesc('id')
|
|
|
->get(['id', 'name']);
|
|
|
|
|
|
$rows = [];
|
|
|
foreach ($venues as $v) {
|
|
|
$id = (int) $v->id;
|
|
|
if (! array_key_exists($id, $liveByVenueId)) {
|
|
|
continue;
|
|
|
}
|
|
|
$live = (int) $liveByVenueId[$id];
|
|
|
if ($live <= 0) {
|
|
|
continue;
|
|
|
}
|
|
|
$rows[] = [
|
|
|
'venue_id' => $id,
|
|
|
'venue_name' => (string) $v->name,
|
|
|
'live_count' => $live,
|
|
|
];
|
|
|
}
|
|
|
|
|
|
usort($rows, function (array $a, array $b): int {
|
|
|
if ($b['live_count'] !== $a['live_count']) {
|
|
|
return $b['live_count'] <=> $a['live_count'];
|
|
|
}
|
|
|
|
|
|
return $a['venue_id'] <=> $b['venue_id'];
|
|
|
});
|
|
|
|
|
|
return array_values($rows);
|
|
|
}
|
|
|
|
|
|
public function ticketGrabStats(Request $request, DashboardTicketGrabStatsService $ticketGrabStats): JsonResponse
|
|
|
{
|
|
|
$validated = $request->validate([
|
|
|
'ticket_grab_event_id' => ['required', 'integer', 'exists:ticket_grab_events,id'],
|
|
|
'date' => ['nullable', 'date_format:Y-m-d'],
|
|
|
]);
|
|
|
|
|
|
$user = $request->user();
|
|
|
$allowedVenueIds = $user->isSuperAdmin()
|
|
|
? Venue::query()->pluck('id')
|
|
|
: $user->venues()->pluck('venues.id');
|
|
|
|
|
|
$eventId = (int) $validated['ticket_grab_event_id'];
|
|
|
$date = isset($validated['date']) && $validated['date'] !== ''
|
|
|
? (string) $validated['date']
|
|
|
: now()->format('Y-m-d');
|
|
|
|
|
|
$pivotVenues = TicketGrabEventVenue::query()
|
|
|
->where('ticket_grab_event_id', $eventId)
|
|
|
->pluck('venue_id');
|
|
|
if ($pivotVenues->isNotEmpty() && ! $user->isSuperAdmin()) {
|
|
|
$allow = $allowedVenueIds->map(fn ($id) => (int) $id);
|
|
|
$ok = false;
|
|
|
foreach ($pivotVenues as $vid) {
|
|
|
if ($allow->contains((int) $vid)) {
|
|
|
$ok = true;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
if (! $ok) {
|
|
|
return response()->json(['message' => '无权查看该抢票活动统计'], 403);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$scoped = DashboardTicketGrabStatsService::scopedVenueIdsForEvent($eventId, $allowedVenueIds);
|
|
|
if ($scoped->isEmpty()) {
|
|
|
return response()->json(['message' => '当前账号下无可统计的场馆或未配置参与场馆'], 403);
|
|
|
}
|
|
|
|
|
|
$payload = $ticketGrabStats->build($eventId, $date, $scoped);
|
|
|
if (isset($payload['error'])) {
|
|
|
return response()->json(['message' => $payload['error']], 404);
|
|
|
}
|
|
|
|
|
|
$payload['data_updated_at'] = $date;
|
|
|
|
|
|
return response()->json($payload);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 抢票每日核销统计 Excel(行=场馆,列=活动期内各日 + 合计占比)
|
|
|
*/
|
|
|
public function ticketGrabDailyVerifyExport(Request $request, DashboardTicketGrabStatsService $ticketGrabStats): StreamedResponse
|
|
|
{
|
|
|
$validated = $request->validate([
|
|
|
'ticket_grab_event_id' => ['required', 'integer', 'exists:ticket_grab_events,id'],
|
|
|
]);
|
|
|
|
|
|
$user = $request->user();
|
|
|
$allowedVenueIds = $user->isSuperAdmin()
|
|
|
? Venue::query()->pluck('id')
|
|
|
: $user->venues()->pluck('venues.id');
|
|
|
|
|
|
$eventId = (int) $validated['ticket_grab_event_id'];
|
|
|
|
|
|
$pivotVenues = TicketGrabEventVenue::query()
|
|
|
->where('ticket_grab_event_id', $eventId)
|
|
|
->pluck('venue_id');
|
|
|
if ($pivotVenues->isNotEmpty() && ! $user->isSuperAdmin()) {
|
|
|
$allow = $allowedVenueIds->map(fn ($id) => (int) $id);
|
|
|
$ok = false;
|
|
|
foreach ($pivotVenues as $vid) {
|
|
|
if ($allow->contains((int) $vid)) {
|
|
|
$ok = true;
|
|
|
break;
|
|
|
}
|
|
|
}
|
|
|
if (! $ok) {
|
|
|
abort(403, '无权查看该抢票活动统计');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$scoped = DashboardTicketGrabStatsService::scopedVenueIdsForEvent($eventId, $allowedVenueIds);
|
|
|
if ($scoped->isEmpty()) {
|
|
|
abort(403, '当前账号下无可统计的场馆或未配置参与场馆');
|
|
|
}
|
|
|
|
|
|
$matrix = $ticketGrabStats->buildDailyVerifyMatrixPublic($eventId, $scoped);
|
|
|
if (isset($matrix['error'])) {
|
|
|
abort(404, $matrix['error']);
|
|
|
}
|
|
|
|
|
|
$labels = $matrix['date_labels'] ?? [];
|
|
|
$rows = $matrix['rows'] ?? [];
|
|
|
|
|
|
$table = [array_merge(['场馆'], $labels, ['总人数和核销比'])];
|
|
|
foreach ($rows as $r) {
|
|
|
$line = [$r['venue_name'] ?? ''];
|
|
|
foreach (($r['cells'] ?? []) as $c) {
|
|
|
$line[] = $c['display'] ?? '';
|
|
|
}
|
|
|
$line[] = ($r['total_cell'] ?? [])['display'] ?? '';
|
|
|
$table[] = $line;
|
|
|
}
|
|
|
|
|
|
$spreadsheet = new Spreadsheet;
|
|
|
$sheet = $spreadsheet->getActiveSheet();
|
|
|
$sheet->setTitle('每日核销统计');
|
|
|
$sheet->fromArray($table, null, 'A1');
|
|
|
|
|
|
$writer = new Xlsx($spreadsheet);
|
|
|
$filename = 'ticket-grab-daily-verify-'.$eventId.'-'.now()->format('Ymd-His').'.xlsx';
|
|
|
|
|
|
return response()->streamDownload(function () use ($writer) {
|
|
|
$writer->save('php://output');
|
|
|
}, $filename, [
|
|
|
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
|
]);
|
|
|
}
|
|
|
}
|