$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; } } }