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.

267 lines
8.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Services;
use App\Models\Application;
use App\Models\ApplicationFile;
use App\Models\Competition;
use App\Support\ProjectCode;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use ZipArchive;
class AdminApplicationExportService
{
/**
* @param array<string, mixed> $filters
*/
public function buildZipPath(Competition $competition, array $filters): string
{
$applications = $this->filteredApplications($competition, $filters);
$trackTitles = $competition->tracks()->pluck('title', 'track_code');
$xlsxPath = $this->buildXlsxPath($applications, $trackTitles);
$tmp = tempnam(sys_get_temp_dir(), 'app_export_');
if ($tmp === false) {
@unlink($xlsxPath);
throw new \RuntimeException('无法创建临时文件');
}
$zipPath = $tmp.'.zip';
@unlink($tmp);
$zip = new ZipArchive;
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
@unlink($xlsxPath);
throw new \RuntimeException('无法创建 ZIP 文件');
}
$zip->addFile($xlsxPath, '报名信息.xlsx');
$usedFolderNames = [];
foreach ($applications as $application) {
$folder = $this->uniqueFolderName($application, $usedFolderNames);
$usedFolderNames[$folder] = true;
$zip->addEmptyDir($folder);
foreach ($application->files as $file) {
$absolutePath = $this->resolveFileAbsolutePath($file);
if ($absolutePath === null) {
continue;
}
$entryName = $this->uniqueFileNameInFolder($folder, $file, $zip);
$zip->addFile($absolutePath, $entryName);
}
}
$zip->close();
@unlink($xlsxPath);
return $zipPath;
}
/**
* @param Collection<int, Application> $applications
* @param Collection<string, string> $trackTitles
*/
private function buildXlsxPath(Collection $applications, Collection $trackTitles): string
{
$headers = [
'项目编号',
'状态',
'项目名称',
'负责人',
'学校',
'学历',
'手机号',
'邮箱',
'组别',
'企业名称',
'赛道',
'所在地',
'报名渠道',
'项目简介',
'提交时间',
'附件数量',
];
$rows = [$headers];
foreach ($applications as $app) {
$location = $this->formatLocation($app);
$channel = $app->signupChannel
? $app->signupChannel->channel_name.'('.$app->signupChannel->channel_code.')'
: ($app->signup_channel_code ?: '');
$rows[] = [
ProjectCode::resolve($app),
$app->status === 'submitted' ? '已提交' : '草稿',
$app->project_name ?? '',
$app->player_name ?? '',
$app->school ?? '',
$app->degree ?? '',
$app->contact_mobile ?? '',
$app->contact_email ?? '',
$app->entry_group ?? '',
$app->company_name ?? '',
$trackTitles->get($app->track ?? '') ?? ($app->track ?? ''),
$location,
$channel,
$app->intro ?? '',
$app->submitted_at?->format('Y-m-d H:i:s') ?? '',
$app->files->count(),
];
}
$spreadsheet = new Spreadsheet;
$sheet = $spreadsheet->getActiveSheet();
$sheet->fromArray($rows, null, 'A1');
$tmp = tempnam(sys_get_temp_dir(), 'app_export_xlsx_');
if ($tmp === false) {
throw new \RuntimeException('无法创建 Excel 临时文件');
}
$xlsxPath = $tmp.'.xlsx';
@unlink($tmp);
(new Xlsx($spreadsheet))->save($xlsxPath);
$spreadsheet->disconnectWorksheets();
return $xlsxPath;
}
/**
* @param array<string, mixed> $filters
* @return Collection<int, Application>
*/
private function filteredApplications(Competition $competition, array $filters): Collection
{
return $this->buildFilteredQuery($competition, $filters)
->with(['files', 'signupChannel'])
->orderByDesc('submitted_at')
->orderByDesc('id')
->get();
}
/**
* @param array<string, mixed> $filters
*/
public function buildFilteredQuery(Competition $competition, array $filters): Builder
{
$query = Application::query()->where('competition_id', $competition->id);
$status = trim((string) ($filters['status'] ?? ''));
if ($status !== '') {
$query->where('status', $status);
}
$track = trim((string) ($filters['track'] ?? ''));
if ($track !== '') {
$query->where('track', $track);
}
$channelId = $filters['signup_channel_id'] ?? null;
if ($channelId !== null && $channelId !== '') {
$query->where('signup_channel_id', (int) $channelId);
}
$keyword = trim((string) ($filters['keyword'] ?? ''));
if ($keyword !== '') {
$like = '%'.str_replace(['%', '_'], ['\\%', '\\_'], $keyword).'%';
$query->where(function ($q) use ($like): void {
$q->where('player_name', 'like', $like)
->orWhere('project_name', 'like', $like)
->orWhere('school', 'like', $like)
->orWhere('contact_mobile', 'like', $like)
->orWhere('company_name', 'like', $like)
->orWhere('project_code', 'like', $like);
});
}
return $query;
}
private function resolveFileAbsolutePath(ApplicationFile $file): ?string
{
/** @var FilesystemAdapter $disk */
$disk = Storage::disk($file->disk);
if (! $disk->exists($file->path)) {
return null;
}
$absolutePath = $disk->path($file->path);
if (! is_file($absolutePath)) {
return null;
}
return $absolutePath;
}
private function formatLocation(Application $app): string
{
$country = trim((string) ($app->location_country ?? ''));
if ($country === '海外' && ($app->oversea_country ?? '') !== '') {
return '海外 / '.$app->oversea_country;
}
return implode(' / ', array_values(array_filter([
$app->location_country,
$app->location_province,
$app->location_city,
], fn ($v) => $v !== null && trim((string) $v) !== '')));
}
/**
* @param array<string, true> $usedFolderNames
*/
private function uniqueFolderName(Application $application, array &$usedFolderNames): string
{
$base = $this->sanitizePathSegment(
ProjectCode::resolve($application).($application->player_name ?? '未知')
);
$folder = $base;
$suffix = 2;
while (isset($usedFolderNames[$folder])) {
$folder = $base.'_'.$suffix;
$suffix++;
}
return $folder;
}
private function uniqueFileNameInFolder(string $folder, ApplicationFile $file, ZipArchive $zip): string
{
$name = $file->clientDownloadName();
$candidate = $folder.'/'.$name;
if ($zip->locateName($candidate) === false) {
return $candidate;
}
$ext = pathinfo($name, PATHINFO_EXTENSION);
$base = pathinfo($name, PATHINFO_FILENAME);
$suffix = 2;
while (true) {
$next = $folder.'/'.($ext !== '' ? $base.'_'.$suffix.'.'.$ext : $base.'_'.$suffix);
if ($zip->locateName($next) === false) {
return $next;
}
$suffix++;
}
}
private function sanitizePathSegment(string $value): string
{
$value = trim(str_replace(['\\', '/'], '_', $value));
$value = preg_replace('/[<>:"|?*]/u', '_', $value) ?? $value;
$value = preg_replace('/[\x00-\x1F\x7F]/u', '', $value) ?? $value;
return $value !== '' ? $value : '未命名';
}
}