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.

101 lines
6.3 KiB

2 days ago
<?php
namespace App\Services\Survey;
use App\Models\CourseContentEvaluation;
use App\Models\Upload;
use Illuminate\Support\Facades\Storage;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use ZipArchive;
class SurveyExport
{
private const TYPES = ['radio' => '单选/评分', 'checkbox' => '多选', 'text' => '短文本', 'textarea' => '长文本', 'number' => '数字', 'date' => '日期', 'datetime' => '日期时间', 'file' => '文件上传'];
private static function safeName($name): string
{
return mb_substr(trim(preg_replace('/[\\\\\\/<>:"|?*\x00-\x1f]/u', '_', (string) $name), '. '), 0, 80) ?: '未命名';
}
public static function build(CourseContentEvaluation $survey): string
{
$dir = storage_path('app/survey-exports');
if (!is_dir($dir)) mkdir($dir, 0700, true);
$base = $dir.'/'.bin2hex(random_bytes(16));
$book = new Spreadsheet();
$zip = new ZipArchive();
$opened = false;
try {
if ($zip->open($base.'.zip', ZipArchive::CREATE | ZipArchive::EXCL) !== true) throw new \RuntimeException('无法创建导出文件');
$opened = true;
$summary = $book->getActiveSheet()->setTitle('答卷列表');
$detail = $book->createSheet()->setTitle('问题详细表');
$attachments = $book->createSheet()->setTitle('附件清单');
self::row($summary, 1, ['答卷ID', '问卷名称', '答题人姓名', '学号', '公司', '职务', '首次提交时间', '最近提交时间']);
self::row($detail, 1, ['答卷ID', '答题人姓名', '章节', '题号', '题目', '子题/维度', '题型', '题目说明', '所有选项', '答案', '补充说明', '状态']);
self::row($attachments, 1, ['答卷ID', '答题人姓名', '题目', '原文件名', '压缩包内路径', '状态']);
$sr = $dr = $ar = 2;
$current = $survey->courseContentEvaluationAsks()->orderBy('sort')->orderBy('id')->get()->toArray();
foreach ($survey->courseContentEvaluationForms()->with('user')->lazyById(100) as $reply) {
$name = $reply->user->name ?? '未填写姓名';
self::row($summary, $sr++, [$reply->id, $reply->snapshot['title'] ?? $survey->title, $name, $reply->user->no ?? '', $reply->user->company_name ?? '', $reply->user->company_position ?? '', $reply->created_at, $reply->updated_at]);
$section = '';
$number = 0;
foreach (SurveySchema::replyQuestions($current, $reply->data, $reply->snapshot) as $q) {
if ($q['edit_input'] === 'section') { $section = $q['name']; continue; }
$hidden = !empty($q['hidden']);
$position = $hidden ? '' : ++$number;
$extra = $hidden ? '' : ($q['other_text'] ?? '');
foreach ($q['select_item'] ?? [] as $opt) {
$note = $q['option_texts'][$opt['key']] ?? '';
if (!$hidden && $note !== '') $extra .= ($extra ? "\n" : '').$opt['value'].':'.$note;
}
$fields = $q['edit_input'] === 'multi_dimension' ? ($q['dimensions'] ?? []) : [$q];
foreach ($fields as $field) {
$options = array_map(fn ($o) => $o['value'].(isset($o['score']) ? '('.$o['score'].'分)' : ''), $field['select_item'] ?? []);
self::row($detail, $dr++, [$reply->id, $name, $section, $position, $q['name'], $q['edit_input'] === 'multi_dimension' ? $field['name'] : '', self::TYPES[$field['edit_input']] ?? $field['edit_input'], $field['help'] ?? $q['help'] ?? '', implode("\n", $options), $hidden ? '' : ($field['display_value'] ?? ''), $extra, $hidden ? '条件未满足,未显示' : '已显示']);
}
if ($hidden || $q['edit_input'] !== 'file') continue;
foreach ($q['value'] ?? [] as $item) {
$file = Upload::find($item['id'] ?? null);
$original = $file->original_name ?? $item['original_name'] ?? '附件';
$entry = '附件/'.$reply->id.'_'.self::safeName($name).'/'.$q['id'].'_'.self::safeName($q['name']).'/'.($item['id'] ?? '0').'_'.self::safeName($original);
$valid = $file && $file->folder === 'storage/files' && basename($file->name) === $file->name && Storage::disk('public')->exists('files/'.$file->name);
if ($valid && !$zip->addFile(Storage::disk('public')->path('files/'.$file->name), $entry)) throw new \RuntimeException('附件打包失败');
self::row($attachments, $ar++, [$reply->id, $name, $q['name'], $original, $valid ? $entry : '', $valid ? '已打包' : '文件缺失,未打包']);
}
}
}
foreach ($book->getAllSheets() as $sheet) {
$sheet->freezePane('C2');
$sheet->setAutoFilter($sheet->calculateWorksheetDimension());
$sheet->getStyle('1:1')->getFont()->setBold(true);
$sheet->getStyle($sheet->calculateWorksheetDimension())->getAlignment()->setWrapText(true)->setVertical('top');
foreach (range(1, Coordinate::columnIndexFromString($sheet->getHighestColumn())) as $col) $sheet->getColumnDimensionByColumn($col)->setWidth($col < 3 ? 18 : 32);
}
(new Xlsx($book))->save($base.'.xlsx');
$zip->addFile($base.'.xlsx', '问卷答卷明细.xlsx');
if (!$zip->close()) throw new \RuntimeException('导出压缩包生成失败');
$opened = false;
return $base.'.zip';
} catch (\Throwable $e) {
if ($opened) $zip->close();
@unlink($base.'.zip');
throw $e;
} finally {
@unlink($base.'.xlsx');
$book->disconnectWorksheets();
}
}
private static function row($sheet, int $row, array $values): void
{
foreach ($values as $index => $value) {
// User input is always text, never an Excel formula.
$sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($index + 1).$row, (string) $value, DataType::TYPE_STRING);
}
}
}