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.

229 lines
9.3 KiB

<?php
namespace App\Http\Controllers\Api\Admin;
use App\Http\Controllers\Controller;
use App\Models\AudienceRegistration;
use App\Models\Competition;
use App\Models\CompetitionPortalLocale;
use App\Support\BizDateTime;
use App\Support\PortalLocale;
use App\Support\SignupDisplay;
use App\Support\SignupOptionCatalog;
use App\Support\SignupSchemaLayout;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use OpenSpout\Common\Entity\Row;
use OpenSpout\Writer\XLSX\Writer;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Symfony\Component\HttpFoundation\Response;
class AudienceRegistrationController extends Controller
{
public function index(Request $request, Competition $competition): JsonResponse
{
$filters = $this->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<string, mixed>
*/
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<string, mixed> $filters
* @return Builder<AudienceRegistration>
*/
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<string, mixed>
*/
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)
: [],
];
}
}