validatedFilters($request, $competition); $query = $this->buildQuery($competition, $filters); $perPage = min((int) ($filters['per_page'] ?? 15), 100); $paginator = $query->paginate($perPage); $paginator->getCollection()->transform(fn (AudienceRegistration $row) => $this->toArray($row, $competition)); $payload = $paginator->toArray(); $competition->loadMissing('audienceFormSchema'); $payload['schema_list_fields'] = SignupSchemaLayout::adminListFields($competition, 'audience'); $payload['schema_filter_fields'] = SignupSchemaLayout::adminFilterFields($competition, 'audience'); return response()->json($payload); } public function export(Request $request, Competition $competition): BinaryFileResponse { $filters = $this->validatedFilters($request, $competition); $rows = $this->buildQuery($competition, $filters)->get(); $tmp = tempnam(sys_get_temp_dir(), 'audience_export_'); if ($tmp === false) { abort(Response::HTTP_INTERNAL_SERVER_ERROR, '无法创建导出文件'); } $xlsxPath = $tmp.'.xlsx'; @unlink($tmp); $writer = new Writer; $writer->openToFile($xlsxPath); $competition->loadMissing('audienceFormSchema'); $known = ['name', 'phone', 'company', 'job', 'purpose']; $extraFields = []; $extraHeaders = []; foreach (SignupSchemaLayout::adminListFields($competition, 'audience') as $field) { if (in_array($field['key'], $known, true)) { continue; } $extraHeaders[] = $field['label']; if ($field['type'] === 'select') { $extraHeaders[] = $field['label'].'编码'; } $extraFields[] = $field; } $writer->addRow(Row::fromValues(array_merge([ '场次', '姓名', '电话', '票号', '单位', '职务', '观赛目的', '填报语言', '报名时间', '核销状态', '到场时间', ], $extraHeaders))); foreach ($rows as $row) { $locale = SignupDisplay::audienceLocale((int) $row->user_id, (int) $row->competition_id); $cells = [ $row->session ? SignupDisplay::sessionName($row->session, $locale) : '', $row->name, $row->phone, $row->ticket_code ?? '', $row->company ?? '', $row->job ?? '', SignupDisplay::purposeLabel((string) $row->purpose, $locale), SignupDisplay::localeTag($locale), $row->registered_at ? $row->registered_at->copy()->timezone(BizDateTime::TIMEZONE)->format('Y-m-d H:i') : '', $row->checked_in_at ? '已到场' : '未到场', $row->checked_in_at ? $row->checked_in_at->copy()->timezone(BizDateTime::TIMEZONE)->format('Y-m-d H:i') : '', ]; $display = SignupSchemaLayout::audienceDisplayMap($competition, $row); foreach ($extraFields as $field) { $cells[] = $display[$field['key']] ?? ''; if ($field['type'] === 'select') { $raw = SignupDisplay::audienceFieldValue($row, $field['key']); $cells[] = is_scalar($raw) ? (string) $raw : ''; } } $writer->addRow(Row::fromValues($cells)); } $writer->close(); $filename = '观众报名_'.$competition->slug.'_'.now()->timezone(BizDateTime::TIMEZONE)->format('Ymd_His').'.xlsx'; return response()->download($xlsxPath, $filename, [ 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', ])->deleteFileAfterSend(true); } /** * @return array */ private function validatedFilters(Request $request, Competition $competition): array { return array_merge($request->validate([ 'page' => ['sometimes', 'integer', 'min:1'], 'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'], 'audience_session_id' => ['sometimes', 'nullable', 'integer', 'min:1'], 'phone' => ['sometimes', 'nullable', 'string', 'max:20'], 'purpose' => ['sometimes', 'nullable', 'string', 'max:64'], 'signup_locale' => ['sometimes', 'nullable', 'string', Rule::in(['', PortalLocale::ZH, PortalLocale::EN])], 'keyword' => ['sometimes', 'nullable', 'string', 'max:64'], 'checkin_status' => ['sometimes', 'nullable', 'string', Rule::in(['checked_in', 'not_checked_in', ''])], ]), SignupSchemaLayout::extractFilterValues($request, $competition, 'audience')); } /** * @param array $filters * @return Builder */ private function buildQuery(Competition $competition, array $filters): Builder { $query = AudienceRegistration::query() ->with('session') ->where('competition_id', $competition->id) ->orderByDesc('registered_at') ->orderByDesc('id'); $sessionId = (int) ($filters['audience_session_id'] ?? 0); if ($sessionId > 0) { $query->where('audience_session_id', $sessionId); } $phone = trim((string) ($filters['phone'] ?? '')); if ($phone !== '') { $query->where('phone', 'like', '%'.$phone.'%'); } $purpose = trim((string) ($filters['purpose'] ?? '')); if ($purpose !== '') { $normalized = SignupOptionCatalog::normalize('purpose', $purpose) ?: $purpose; $query->where('purpose', $normalized); } $signupLocale = trim((string) ($filters['signup_locale'] ?? '')); if ($signupLocale !== '') { $userIds = CompetitionPortalLocale::query() ->where('competition_id', $competition->id) ->where('portal', PortalLocale::PORTAL_AUDIENCE) ->where('locale', $signupLocale) ->pluck('user_id'); $query->whereIn('user_id', $userIds); } $keyword = trim((string) ($filters['keyword'] ?? '')); if ($keyword !== '') { $query->where(function (Builder $q) use ($keyword) { $q->where('name', 'like', '%'.$keyword.'%') ->orWhere('phone', 'like', '%'.$keyword.'%') ->orWhere('company', 'like', '%'.$keyword.'%') ->orWhere('ticket_code', 'like', '%'.$keyword.'%'); }); } $checkinStatus = trim((string) ($filters['checkin_status'] ?? '')); if ($checkinStatus === 'checked_in') { $query->whereNotNull('checked_in_at'); } elseif ($checkinStatus === 'not_checked_in') { $query->whereNull('checked_in_at'); } SignupSchemaLayout::applyFilters($query, $competition, $filters, 'audience'); return $query; } /** * @return array */ private function toArray(AudienceRegistration $row, ?Competition $competition = null): array { $locale = SignupDisplay::audienceLocale((int) $row->user_id, (int) $row->competition_id); $competition ??= $row->competition; return [ 'id' => $row->id, 'competition_id' => $row->competition_id, 'audience_session_id' => $row->audience_session_id, 'session_name' => $row->session ? SignupDisplay::sessionName($row->session, $locale) : $row->session?->name, 'session_code' => $row->session?->code, 'name' => $row->name, 'phone' => $row->phone, 'company' => $row->company, 'job' => $row->job, 'purpose' => $row->purpose, 'purpose_label' => SignupDisplay::purposeLabel((string) $row->purpose, $locale), 'signup_locale' => $locale, 'signup_locale_label' => SignupDisplay::localeTag($locale), 'ticket_code' => $row->ticket_code, 'registered_at' => BizDateTime::toIso8601($row->registered_at), 'checked_in_at' => BizDateTime::toIso8601($row->checked_in_at), 'checked_in' => $row->checked_in_at !== null, 'schema_display' => $competition ? SignupSchemaLayout::audienceDisplayMap($competition, $row) : [], ]; } }