You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

317 lines
14 KiB

3 weeks ago
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
1 week ago
use App\Models\Activity;
3 weeks ago
use App\Models\Blacklist;
1 week ago
use App\Models\Reservation;
use App\Models\TicketGrabEvent;
use App\Models\TicketGrabEventVenue;
3 weeks ago
use App\Models\Venue;
1 week ago
use App\Services\DashboardTicketGrabStatsService;
3 weeks ago
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
1 week ago
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
3 weeks ago
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();
}
1 week ago
$datesBlank = ! $request->filled('start_date') || ! $request->filled('end_date');
$startDate = $datesBlank ? null : trim((string) $request->input('start_date'));
$endDate = $datesBlank ? null : trim((string) $request->input('end_date'));
$activityId = null;
if (! $datesBlank && $request->filled('activity_id')) {
$activityId = (int) $request->input('activity_id');
$allowed = Activity::query()
->whereKey($activityId)
->whereNull('deleted_at')
->whereIn('venue_id', $venueIds)
->exists();
if (! $allowed) {
return response()->json(['message' => '活动不存在或无权查看'], 422);
}
}
$dateOnReservation = 'COALESCE(activity_days.activity_date, DATE(reservations.created_at))';
/** 顶部五项:账号可见场馆范围内的全量累计,不受日期与「筛选场馆」影响 */
$scopeVenueIds = $allowedVenueIds;
$activeVenueCount = (int) DB::table('reservations')
->whereIn('venue_id', $scopeVenueIds)
->distinct('venue_id')
->count('venue_id');
$userCount = (int) DB::table('reservations')
->whereIn('venue_id', $scopeVenueIds)
->whereNotNull('wechat_user_id')
->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();
3 weeks ago
$blacklistedUniqueQuery = Blacklist::query()
1 week ago
->whereIn('venue_id', $scopeVenueIds)
3 weeks ago
->whereNotNull('visitor_phone')
->whereRaw("TRIM(visitor_phone) <> ''")
1 week ago
->whereExists(function ($q) use ($scopeVenueIds, $user) {
3 weeks ago
$q->selectRaw('1')
->from('reservations')
->whereColumn('reservations.visitor_phone', 'blacklists.visitor_phone')
1 week ago
->whereIn('reservations.venue_id', $scopeVenueIds);
3 weeks ago
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');
1 week ago
$publishedCount = 0;
$activitySums = null;
$arTotal = 0;
$arVerified = 0;
$arCancelled = 0;
$arPending = 0;
$arExpired = 0;
$arVerifyRate = 0.0;
$activityStatsByVenue = [];
if (! $datesBlank) {
$activityOverlap = function ($q) use ($startDate, $endDate) {
3 weeks ago
$q->where(function ($w) use ($startDate, $endDate) {
$w->whereNotNull('start_at')
->whereNotNull('end_at')
->whereDate('start_at', '<=', $endDate)
->whereDate('end_at', '>=', $startDate);
})->orWhere(function ($w) use ($startDate, $endDate) {
$w->whereNull('start_at')
->whereNull('end_at')
->whereDate('created_at', '>=', $startDate)
->whereDate('created_at', '<=', $endDate);
});
1 week ago
};
$statsActivityIdsQuery = Activity::query()
->whereIn('venue_id', $venueIds)
->whereNull('deleted_at');
if ($activityId !== null) {
$statsActivityIdsQuery->whereKey($activityId);
} else {
$statsActivityIdsQuery->where($activityOverlap);
}
$statsActivityIds = $statsActivityIdsQuery->pluck('id');
$publishedCount = $statsActivityIds->isEmpty()
? 0
: (int) Activity::query()
->whereIn('id', $statsActivityIds)
->whereDate('created_at', '>=', $startDate)
->whereDate('created_at', '<=', $endDate)
->count();
$activitySums = $statsActivityIds->isEmpty()
? null
: Activity::query()
->whereIn('id', $statsActivityIds)
->selectRaw('COALESCE(SUM(view_count),0) as v')
->selectRaw('COALESCE(SUM(external_link_click_count),0) as l')
->first();
if ($statsActivityIds->isNotEmpty()) {
$ar = DB::table('reservations')
->leftJoin('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id')
->whereIn('reservations.venue_id', $venueIds)
->where('reservations.reservation_kind', Reservation::KIND_ACTIVITY)
->whereIn('reservations.activity_id', $statsActivityIds)
->whereRaw("{$dateOnReservation} >= ?", [$startDate])
->whereRaw("{$dateOnReservation} <= ?", [$endDate])
->selectRaw('COUNT(*) as total_count')
->selectRaw("SUM(CASE WHEN reservations.status = 'verified' THEN 1 ELSE 0 END) as verified_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'pending' THEN 1 ELSE 0 END) as pending_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'expired' THEN 1 ELSE 0 END) as expired_count")
->first();
$arTotal = (int) ($ar->total_count ?? 0);
$arVerified = (int) ($ar->verified_count ?? 0);
$arCancelled = (int) ($ar->cancelled_count ?? 0);
$arPending = (int) ($ar->pending_count ?? 0);
$arExpired = (int) ($ar->expired_count ?? 0);
$effective = max(0, $arTotal - $arCancelled);
$arVerifyRate = $effective > 0 ? round($arVerified / $effective, 4) : 0;
}
if ($statsActivityIds->isNotEmpty()) {
$actByVenueRows = Activity::query()
->whereIn('id', $statsActivityIds)
->selectRaw('venue_id')
->selectRaw('COALESCE(SUM(view_count), 0) as total_view_count')
->selectRaw('COALESCE(SUM(external_link_click_count), 0) as total_external_link_click_count')
->selectRaw('SUM(CASE WHEN DATE(created_at) >= ? AND DATE(created_at) <= ? THEN 1 ELSE 0 END) as published_count', [$startDate, $endDate])
->groupBy('venue_id')
->get();
$actByVenue = [];
foreach ($actByVenueRows as $row) {
$actByVenue[(int) $row->venue_id] = $row;
}
$resByVenueRows = DB::table('reservations')
->leftJoin('activity_days', 'activity_days.id', '=', 'reservations.activity_day_id')
->whereIn('reservations.venue_id', $venueIds)
->where('reservations.reservation_kind', Reservation::KIND_ACTIVITY)
->whereIn('reservations.activity_id', $statsActivityIds)
->whereRaw("{$dateOnReservation} >= ?", [$startDate])
->whereRaw("{$dateOnReservation} <= ?", [$endDate])
->selectRaw('reservations.venue_id as venue_id')
->selectRaw('COUNT(*) as total_count')
->selectRaw("SUM(CASE WHEN reservations.status = 'verified' THEN 1 ELSE 0 END) as verified_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'cancelled' THEN 1 ELSE 0 END) as cancelled_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'pending' THEN 1 ELSE 0 END) as pending_count")
->selectRaw("SUM(CASE WHEN reservations.status = 'expired' THEN 1 ELSE 0 END) as expired_count")
->groupBy('reservations.venue_id')
->get();
$resByVenue = [];
foreach ($resByVenueRows as $row) {
$resByVenue[(int) $row->venue_id] = $row;
}
$allVenueIds = collect(array_keys($actByVenue))->merge(array_keys($resByVenue))->unique()->sort()->values();
$names = Venue::query()->whereIn('id', $allVenueIds)->pluck('name', 'id');
foreach ($allVenueIds as $vid) {
$vid = (int) $vid;
$a = $actByVenue[$vid] ?? null;
$r = $resByVenue[$vid] ?? null;
$total = (int) ($r->total_count ?? 0);
$verified = (int) ($r->verified_count ?? 0);
$cancelled = (int) ($r->cancelled_count ?? 0);
$pending = (int) ($r->pending_count ?? 0);
$expired = (int) ($r->expired_count ?? 0);
$effective = max(0, $total - $cancelled);
$rate = $effective > 0 ? round($verified / $effective, 4) : 0.0;
$activityStatsByVenue[] = [
'venue_id' => (int) $vid,
'venue_name' => (string) ($names[$vid] ?? ('#'.$vid)),
'published_count' => (int) ($a->published_count ?? 0),
'total_view_count' => (int) ($a->total_view_count ?? 0),
'total_external_link_click_count' => (int) ($a->total_external_link_click_count ?? 0),
'total_count' => $total,
'verified_count' => $verified,
'cancelled_count' => $cancelled,
'pending_count' => $pending,
'expired_count' => $expired,
'verify_rate' => $rate,
];
}
3 weeks ago
1 week ago
usort($activityStatsByVenue, fn (array $x, array $y) => strcmp($x['venue_name'], $y['venue_name']));
}
3 weeks ago
}
return response()->json([
'scope' => [
'role' => $user->role,
'venue_id' => $selectedVenueId,
'start_date' => $startDate,
'end_date' => $endDate,
1 week ago
'activity_id' => $activityId,
'dates_applied' => ! $datesBlank,
3 weeks ago
],
'summary' => [
'active_venue_count' => (int) $activeVenueCount,
1 week ago
'activity_sessions' => (int) $activitySessions,
'ticket_grab_sessions' => (int) $ticketGrabSessions,
'user_count' => (int) $userCount,
'blacklisted_unique' => (int) $blacklistedUnique,
],
'activity_stats' => [
'published_count' => $publishedCount,
'total_view_count' => (int) ($activitySums->v ?? 0),
'total_external_link_click_count' => (int) ($activitySums->l ?? 0),
'total_count' => $arTotal,
'verified_count' => $arVerified,
'cancelled_count' => $arCancelled,
'pending_count' => $arPending,
'expired_count' => $arExpired,
'verify_rate' => $arVerifyRate,
3 weeks ago
],
1 week ago
'activity_stats_venues' => $activityStatsByVenue,
3 weeks ago
]);
}
1 week ago
public function ticketGrabStats(Request $request, DashboardTicketGrabStatsService $ticketGrabStats): JsonResponse
{
$validated = $request->validate([
'ticket_grab_event_id' => ['required', 'integer', 'exists:ticket_grab_events,id'],
'date' => ['required', '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 = $validated['date'];
$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);
}
3 weeks ago
}