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.
83 lines
4.7 KiB
83 lines
4.7 KiB
|
1 week ago
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services\Course;
|
||
|
|
|
||
|
|
use App\Exports\CourseRegistrationExport;
|
||
|
|
use App\Models\Upload;
|
||
|
|
use Illuminate\Support\Facades\Storage;
|
||
|
|
use Maatwebsite\Excel\Facades\Excel;
|
||
|
|
use Maatwebsite\Excel\Excel as Format;
|
||
|
|
use ZipArchive;
|
||
|
|
|
||
|
|
class CourseRegistrationArchive
|
||
|
|
{
|
||
|
|
public static function attachments(array $rows): array
|
||
|
|
{
|
||
|
|
$items = [];
|
||
|
|
foreach ($rows as $row) foreach ($row['data'] ?? [] as $field) {
|
||
|
|
if (!is_array($field) || !empty($field['hidden']) || ($field['edit_input'] ?? '') !== 'files') continue;
|
||
|
|
$files = $field['fileList'] ?? [];
|
||
|
|
if (!$files) $files = $field['fileListArr'] ?? [];
|
||
|
|
if (!$files) $files = array_filter(explode(',', (string) ($field['value'] ?? '')));
|
||
|
|
foreach ($files as $file) $items[] = ['row' => $row, 'field' => $field, 'file' => is_array($file) ? ($file['response'] ?? $file) : ['url' => $file]];
|
||
|
|
}
|
||
|
|
return $items;
|
||
|
|
}
|
||
|
|
|
||
|
|
private static function safeName($name): string
|
||
|
|
{
|
||
|
|
return mb_substr(trim(preg_replace('/[\\\\\\/<>:"|?*\x00-\x1f]/u', '_', (string) $name), '. '), 0, 80) ?: '未命名';
|
||
|
|
}
|
||
|
|
|
||
|
|
public static function build(CourseRegistrationExport $export, array $items): string
|
||
|
|
{
|
||
|
|
$dir = storage_path('app/course-registration-exports');
|
||
|
|
if (!is_dir($dir)) mkdir($dir, 0700, true);
|
||
|
|
$path = $dir.'/'.bin2hex(random_bytes(16)).'.zip';
|
||
|
|
$zip = new ZipArchive();
|
||
|
|
$opened = false;
|
||
|
|
try {
|
||
|
|
if ($zip->open($path, ZipArchive::CREATE | ZipArchive::EXCL) !== true) throw new \RuntimeException('无法创建报名导出文件');
|
||
|
|
$opened = true;
|
||
|
|
if (!$zip->addFromString('报名信息.xlsx', Excel::raw($export, Format::XLSX))) throw new \RuntimeException('报名表打包失败');
|
||
|
|
$manifest = [["报名ID", "报名人", "字段", "原文件名", "压缩包内路径", "状态"]];
|
||
|
|
foreach ($items as $index => $item) {
|
||
|
|
$row = $item['row']; $field = $item['field']; $file = $item['file'];
|
||
|
|
$upload = !empty($file['id']) ? Upload::find($file['id']) : null;
|
||
|
|
// Historical answers may contain only URLs. Resolve through upload records;
|
||
|
|
// never download a submitted URL or use its path to read arbitrary files.
|
||
|
|
if (!$upload && empty($file['id'])) {
|
||
|
|
$urlPath = parse_url((string) ($file['url'] ?? ''), PHP_URL_PATH);
|
||
|
|
if (is_string($urlPath) && preg_match('#(?:^|/)storage/files/([^/]+)$#', $urlPath, $match)) {
|
||
|
|
$upload = Upload::where('folder', 'storage/files')->where('name', $match[1])->first();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
$name = data_get($row, 'user.name') ?: '未填写姓名';
|
||
|
|
foreach ($row['data'] ?? [] as $answer) if (($answer['field'] ?? '') === 'name' && !empty($answer['value']) && is_scalar($answer['value'])) { $name = $answer['value']; break; }
|
||
|
|
$original = $upload->original_name ?? $file['original_name'] ?? $file['name'] ?? basename((string) ($file['url'] ?? '附件'));
|
||
|
|
$entry = '附件/'.$row['id'].'_'.self::safeName($name).'/'.self::safeName($field['field']).'_'.self::safeName($field['name'] ?? '').'/'.($index + 1).'_'.self::safeName($original);
|
||
|
|
$valid = $upload && $upload->folder === 'storage/files' && basename($upload->name) === $upload->name;
|
||
|
|
$source = $valid ? realpath(Storage::disk('public')->path('files/'.$upload->name)) : false;
|
||
|
|
$root = realpath(Storage::disk('public')->path('files'));
|
||
|
|
$valid = $source && $root && strpos($source, $root.DIRECTORY_SEPARATOR) === 0 && is_file($source);
|
||
|
|
if ($valid && !$zip->addFile($source, $entry)) throw new \RuntimeException('报名附件打包失败');
|
||
|
|
$manifest[] = [$row['id'], $name, $field['name'] ?? $field['field'], $original, $valid ? $entry : '', $valid ? '已打包' : '文件缺失,未打包'];
|
||
|
|
}
|
||
|
|
$stream = fopen('php://temp', 'r+');
|
||
|
|
fwrite($stream, "\xEF\xBB\xBF");
|
||
|
|
foreach ($manifest as $line) fputcsv($stream, array_map(function ($value) {
|
||
|
|
$text = (string) $value;
|
||
|
|
return preg_match('/^[\s]*[=+@-]/u', $text) ? "'".$text : $text;
|
||
|
|
}, $line));
|
||
|
|
rewind($stream); $csv = stream_get_contents($stream); fclose($stream);
|
||
|
|
if (!$zip->addFromString('附件清单.csv', $csv) || !$zip->close()) throw new \RuntimeException('报名导出文件生成失败');
|
||
|
|
$opened = false;
|
||
|
|
return $path;
|
||
|
|
} catch (\Throwable $e) {
|
||
|
|
if ($opened) $zip->close();
|
||
|
|
@unlink($path);
|
||
|
|
throw $e;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|