|
|
|
|
<?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)
|
|
|
|
|
: [],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|