master
lion 3 weeks ago
parent ba9266f8b0
commit 40b715ae53

@ -190,7 +190,7 @@ class CourseController extends CommonController
if ($message = $detail->participationError($this->getUserId())) return $this->success(['id' => $detail->id, 'access_error' => $message]);
$detail->schema_hash = SurveySchema::hash($detail->courseContentEvaluationAsks->toArray());
$reply = $detail->courseContentEvaluationForms()->where('user_id', $this->getUserId())->first();
$detail->requires_profile = empty(auth('mobile')->user()->mobile);
$detail->requires_profile = !\App\Services\Survey\SurveyIdentity::complete(auth('mobile')->user());
if ($reply && !empty($reply->snapshot['questions'])) $detail->schema_hash = SurveySchema::hash($reply->snapshot['questions']);
$detail->my_response = $reply ? [
'title' => $reply->snapshot['title'] ?? $detail->title,
@ -489,6 +489,7 @@ class CourseController extends CommonController
if ((int) $survey->status !== 1) throw new \InvalidArgumentException('问卷未发布');
if ($survey->start_time && strtotime($survey->start_time) > time()) throw new \InvalidArgumentException('评价时间未开始');
if (empty(auth('mobile')->user()->mobile)) throw new \InvalidArgumentException('请先绑定或注册');
if (!\App\Services\Survey\SurveyIdentity::complete(auth('mobile')->user())) throw new \InvalidArgumentException('请先完成绑定或注册资料');
$reply = $survey->courseContentEvaluationForms()->where('user_id', $this->getUserId())->first();
$questions = $survey->courseContentEvaluationAsks()->with('courseContent.teacher')->orderBy('sort')->orderBy('id')->get()->toArray();
if ($reply && !empty($reply->snapshot['questions'])) $questions = $reply->snapshot['questions'];

@ -166,7 +166,7 @@ class UserController extends CommonController
*/
public function updateUser()
{
$all = \request()->except(['id', 'mobile', 'openid']);
$all = \request()->except(['id', 'mobile', 'openid', 'survey_id']);
$model = User::find($this->getUserId());
$oldCompanyName = $model->company_name;
if (isset($all['password'])) {
@ -192,6 +192,7 @@ class UserController extends CommonController
}
}
$model->fill($all);
if (request()->filled('survey_id') && !\App\Services\Survey\SurveyIdentity::complete($model)) return $this->fail([ResponseCode::ERROR_BUSINESS, '请完整填写姓名、手机号、性别、出生日期、邮箱、公司和职务']);
$model->save();
// 手机验证发生在填写注册资料之前,资料保存后重新核对代发需求。
\App\Services\SupplyDemand\Handover::autoBind($model);
@ -476,7 +477,7 @@ class UserController extends CommonController
}
\App\Services\SupplyDemand\Handover::verified($hasMobile);
$token = $hasMobile->createToken("mobile-token")->plainTextToken;
return $this->success(compact('token'));
return $this->success(['token' => $token, 'requires_profile' => !\App\Services\Survey\SurveyIdentity::complete($hasMobile)]);
}
return $this->fail([ResponseCode::ERROR_BUSINESS, '校友库中还没有您的信息,请先注册']);
}

@ -4,6 +4,14 @@ namespace App\Models;
class CourseContentEvaluationForm extends SoftDeletesModel
{
protected $appends = ['respondent'];
public function getRespondentAttribute(): array
{
$user = $this->relationLoaded('user') ? $this->getRelation('user') : User::find($this->user_id);
return \App\Services\Survey\SurveyIdentity::respondent($user, $this->data, $this->snapshot);
}
protected $casts = ['data' => 'json', 'snapshot' => 'array'];
public function user()

@ -13,7 +13,6 @@ 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) ?: '未命名';
@ -30,47 +29,35 @@ class SurveyExport
try {
if ($zip->open($base.'.zip', ZipArchive::CREATE | ZipArchive::EXCL) !== true) throw new \RuntimeException('无法创建导出文件');
$opened = true;
$summary = $book->getActiveSheet()->setTitle('答卷列表');
$detail = $book->createSheet()->setTitle('问题详细表');
$detail = $book->getActiveSheet()->setTitle('问题详细表');
$attachments = $book->createSheet()->setTitle('附件清单');
self::row($summary, 1, ['答卷ID', '问卷名称', '答题人姓名', '学号', '公司', '职务', '首次提交时间', '最近提交时间']);
self::row($detail, 1, ['答卷ID', '答题人姓名', '章节', '题号', '题目', '子题/维度', '答案', '补充说明', '题型', '题目说明', '所有选项']);
self::row($attachments, 1, ['答卷ID', '答题人姓名', '题目', '原文件名', '压缩包内路径', '状态']);
$sr = $dr = $ar = 2;
$hasSections = $hasDimensions = false;
$replyRanges = $sectionRanges = $questionRanges = $plainRows = [];
$dr = 3;
$ar = 2;
$columns = [];
$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]);
$replyStart = $sectionStart = $dr;
$section = '';
$number = 0;
$name = $reply->respondent['name'];
if (!is_scalar($name) || trim((string) $name) === '') $name = '未填写姓名';
self::row($detail, $dr, [$reply->id, $name]);
foreach (SurveySchema::replyQuestions($current, $reply->data, $reply->snapshot) as $q) {
if ($q['edit_input'] === 'section') {
if ($section !== '' && $dr > $sectionStart) $sectionRanges[] = [$sectionStart, $dr - 1];
$sectionStart = $dr;
$section = $q['name'];
$hasSections = true;
continue;
}
$questionStart = $dr;
if ($q['edit_input'] === 'section') 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'] : '', $hidden ? '' : ($field['display_value'] ?? ''), $extra, self::TYPES[$field['edit_input']] ?? $field['edit_input'], $field['help'] ?? $q['help'] ?? '', implode("\n", $options)]);
foreach (self::questionFields($q) as [$key, $label, $field]) {
if (!isset($columns[$key])) $columns[$key] = ['column' => count($columns) + 3, 'label' => $label, 'title' => $q['name'], 'group' => (string) ($q['id'] ?? $q['field']), 'dimension' => $q['edit_input'] === 'multi_dimension' ? $field['name'] : null, 'type' => $field['edit_input'] ?? 'text'];
$answer = $hidden || SurveySchema::blank($field['value'] ?? null) ? '' : ($field['display_value'] ?? '');
if ($q['edit_input'] === 'multi_dimension') {
$value = $q['value'][$field['field']] ?? null;
$answer = $hidden || SurveySchema::blank($value) ? '' : ($field['display_value'] ?? '');
}
if ($extra !== '') $answer .= ($answer !== '' ? "\n" : '').'补充:'.$extra;
$detail->setCellValueExplicit(Coordinate::stringFromColumnIndex($columns[$key]['column']).$dr, $answer, DataType::TYPE_STRING);
}
if ($q['edit_input'] === 'multi_dimension' && $dr > $questionStart) {
$hasDimensions = true;
$questionRanges[] = [$questionStart, $dr - 1];
} elseif ($dr > $questionStart) $plainRows[] = $questionStart;
if ($hidden || $q['edit_input'] !== 'file') continue;
foreach ($q['value'] ?? [] as $item) {
$file = Upload::find($item['id'] ?? null);
@ -81,33 +68,28 @@ class SurveyExport
self::row($attachments, $ar++, [$reply->id, $name, $q['name'], $original, $valid ? $entry : '', $valid ? '已打包' : '文件缺失,未打包']);
}
}
if ($dr > $replyStart) $replyRanges[] = [$replyStart, $dr - 1];
if ($section !== '' && $dr > $sectionStart) $sectionRanges[] = [$sectionStart, $dr - 1];
}
if (!$hasDimensions) $detail->removeColumn('F');
if (!$hasSections) $detail->removeColumn('C');
$titleColumn = $hasSections ? 'E' : 'D';
$numberColumn = $hasSections ? 'D' : 'C';
$dimensionColumn = $hasSections ? 'F' : 'E';
foreach ($replyRanges as [$start, $end]) {
self::merge($detail, 'A', $start, $end);
self::merge($detail, 'B', $start, $end);
$dr++;
}
if ($hasSections) foreach ($sectionRanges as [$start, $end]) self::merge($detail, 'C', $start, $end);
foreach ($questionRanges as [$start, $end]) {
self::merge($detail, $titleColumn, $start, $end);
self::merge($detail, $numberColumn, $start, $end);
// An unanswered survey still exports its question headings.
if (!$columns && $dr === 3) foreach ($current as $q) {
if ($q['edit_input'] === 'section') continue;
foreach (self::questionFields($q) as [$key, $label, $field]) $columns[$key] = ['column' => count($columns) + 3, 'label' => $label, 'title' => $q['name'], 'group' => (string) ($q['id'] ?? $q['field']), 'dimension' => $q['edit_input'] === 'multi_dimension' ? $field['name'] : null, 'type' => $field['edit_input'] ?? 'text'];
}
// In mixed questionnaires a plain question spans the unused dimension cell.
if ($hasDimensions) foreach ($plainRows as $row) $detail->mergeCells($titleColumn.$row.':'.$dimensionColumn.$row);
self::headers($detail, $columns, $dr);
foreach ($book->getAllSheets() as $sheet) {
$sheet->freezePane('C2');
$sheet->setAutoFilter($sheet->calculateWorksheetDimension());
$sheet->getStyle('1:1')->getFont()->setBold(true);
$headerRows = $sheet === $detail ? 2 : 1;
if ($sheet === $detail) $sheet->freezePane('C3');
else {
$sheet->freezePane('C2');
$sheet->setAutoFilter($sheet->calculateWorksheetDimension());
foreach ([12, 18, 36, 42, 64, 26] as $index => $width) $sheet->getColumnDimensionByColumn($index + 1)->setWidth($width);
}
$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);
$headerRange = 'A1:'.$sheet->getHighestColumn().$headerRows;
$sheet->getStyle($headerRange)->getFont()->setBold(true);
$sheet->getStyle($headerRange)->getAlignment()->setHorizontal('center')->setVertical('center');
self::fitRows($sheet, $headerRows);
}
foreach ($detail->getMergeCells() as $range) $detail->getStyle($range)->getAlignment()->setVertical('center');
(new Xlsx($book))->save($base.'.xlsx');
$zip->addFile($base.'.xlsx', '问卷答卷明细.xlsx');
if (!$zip->close()) throw new \RuntimeException('导出压缩包生成失败');
@ -123,9 +105,82 @@ class SurveyExport
}
}
private static function merge($sheet, string $column, int $start, int $end): void
private static function headers($sheet, array $columns, int $nextRow): void
{
if ($end > $start) $sheet->mergeCells($column.$start.':'.$column.$end);
// Group by stable question ID; equal titles and snapshots with extra dimensions stay distinct.
$groups = [];
foreach ($columns as $column) $groups[$column['group']][] = $column;
$ordered = [];
foreach ($groups as $group) foreach ($group as $column) $ordered[] = $column;
for ($row = 3; $row < $nextRow; $row++) {
$values = [];
foreach ($ordered as $column) $values[] = $sheet->getCell(Coordinate::stringFromColumnIndex($column['column']).$row)->getValue();
foreach ($values as $index => $value) $sheet->setCellValueExplicit(Coordinate::stringFromColumnIndex($index + 3).$row, (string) $value, DataType::TYPE_STRING);
}
self::row($sheet, 1, ['答卷ID', '答题人姓名']);
$sheet->mergeCells('A1:A2');
$sheet->mergeCells('B1:B2');
$sheet->getColumnDimension('A')->setWidth(12);
$sheet->getColumnDimension('B')->setWidth(18);
$next = 3;
foreach ($groups as $group) {
$start = $next;
foreach ($group as $column) {
$letter = Coordinate::stringFromColumnIndex($next++);
$widths = ['number' => 18, 'date' => 20, 'datetime' => 25, 'file' => 42, 'checkbox' => 38, 'textarea' => 52];
$sheet->getColumnDimension($letter)->setWidth($widths[$column['type']] ?? 32);
if ($column['dimension'] === null) {
$sheet->setCellValueExplicit($letter.'1', $column['title'], DataType::TYPE_STRING);
$sheet->mergeCells($letter.'1:'.$letter.'2');
} else $sheet->setCellValueExplicit($letter.'2', $column['dimension'], DataType::TYPE_STRING);
}
if ($group[0]['dimension'] !== null) {
$first = Coordinate::stringFromColumnIndex($start);
$last = Coordinate::stringFromColumnIndex($next - 1);
$sheet->setCellValueExplicit($first.'1', $group[0]['title'], DataType::TYPE_STRING);
if ($next - $start > 1) $sheet->mergeCells($first.'1:'.$last.'1');
}
}
}
private static function fitRows($sheet, int $headerRows): void
{
$heights = [];
$merged = [];
foreach ($sheet->getMergeCells() as $range) {
[[$left, $top], [$right, $bottom]] = Coordinate::rangeBoundaries($range);
for ($row = $top; $row <= $bottom; $row++) for ($col = $left; $col <= $right; $col++) $merged[$row][$col] = [$left, $top, $right, $bottom];
}
$lastColumn = Coordinate::columnIndexFromString($sheet->getHighestColumn());
for ($row = 1; $row <= $sheet->getHighestRow(); $row++) {
$heights[$row] = max($heights[$row] ?? 0, $row <= $headerRows ? 34 : 30);
for ($col = 1; $col <= $lastColumn; $col++) {
$text = (string) $sheet->getCell(Coordinate::stringFromColumnIndex($col).$row)->getValue();
if ($text === '') continue;
[$left, $top, $right, $bottom] = $merged[$row][$col] ?? [$col, $row, $col, $row];
if ($col !== $left || $row !== $top) continue;
$width = 0;
for ($c = $left; $c <= $right; $c++) $width += $sheet->getColumnDimensionByColumn($c)->getWidth();
// Excel widths use roughly half-width characters; CJK characters count twice.
$charactersPerLine = max(1, $width - 3);
$lines = 0;
foreach (preg_split('/\r\n|\r|\n/', $text) as $line) $lines += max(1, (int) ceil(mb_strwidth($line, 'UTF-8') / $charactersPerLine));
$height = min(409, max(30, ceil(($lines * 16 + 12) / ($bottom - $top + 1))));
for ($r = $top; $r <= $bottom; $r++) $heights[$r] = max($heights[$r] ?? 0, $height);
}
}
foreach ($heights as $row => $height) $sheet->getRowDimension($row)->setRowHeight($height);
}
private static function questionFields(array $q): array
{
$key = (string) ($q['id'] ?? $q['field']);
if ($q['edit_input'] !== 'multi_dimension') return [[$key, $q['name'], $q]];
$fields = [];
foreach ($q['dimensions'] ?? [] as $dim) {
$fields[] = [$key.'/'.($dim['field'] ?? $dim['name']), $q['name'].' / '.$dim['name'], $dim];
}
return $fields;
}
private static function row($sheet, int $row, array $values): void

@ -0,0 +1,43 @@
<?php
namespace App\Services\Survey;
use App\Models\User;
class SurveyIdentity
{
public const REGISTRATION_FIELDS = ['mobile', 'name', 'sex', 'birthday', 'email', 'company_name', 'company_position'];
public const DISPLAY_FIELDS = ['name', 'no', 'mobile', 'company_name', 'company_position'];
public static function complete(?User $user): bool
{
if (!$user) return false;
foreach (self::REGISTRATION_FIELDS as $field) {
if (!is_scalar($user->$field) || trim((string) $user->$field) === '') return false;
}
return preg_match('/^1\d{10}$/', (string) $user->mobile)
&& filter_var(trim($user->email), FILTER_VALIDATE_EMAIL)
&& User::hasCompleteBirthday((string) $user->birthday);
}
public static function respondent(?User $user, $data, $snapshot): array
{
$result = [];
foreach (self::DISPLAY_FIELDS as $field) $result[$field] = $user->$field ?? null;
$map = SurveySchema::answerMap($data);
$snapshot = SurveySchema::arrayValue($snapshot);
$questions = SurveySchema::arrayValue($snapshot['questions'] ?? array_values($map));
$keyed = ($snapshot['version'] ?? 0) >= 2;
foreach ($questions as $q) {
$q['config'] = SurveySchema::arrayValue($q['config'] ?? []);
$field = $q['config']['user_field'] ?? null;
if (!in_array($field, self::DISPLAY_FIELDS, true)) continue;
if (is_scalar($result[$field]) && trim((string) $result[$field]) !== '') continue;
$answer = $map[(string) ($q['id'] ?? '')] ?? [];
if (!empty($answer['hidden']) || !SurveySchema::visible($q, $questions, $map, $keyed)) continue;
$value = $answer['value'] ?? null;
if (is_scalar($value) && trim((string) $value) !== '') $result[$field] = trim((string) $value);
}
return $result;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -27,7 +27,7 @@ class SurveyWorkflowTest extends TestCase
Schema::create($name, function (Blueprint $table) { $table->id(); $table->softDeletes(); });
}
$user = new User();
$user->forceFill(['id' => 200, 'mobile' => '13800000000']);
$user->forceFill(['id' => 200, 'mobile' => '13800000000', 'name' => '测试学员', 'sex' => '男', 'birthday' => '1990-01-01', 'email' => 'test@example.com', 'company_name' => '测试公司', 'company_position' => '经理']);
Auth::guard('mobile')->setUser($user);
$admin = new Admin();
$admin->forceFill(['id' => 1, 'department_id' => 1]);
@ -65,6 +65,64 @@ class SurveyWorkflowTest extends TestCase
return $response->json('id');
}
public function testIncompleteProfilesCannotSubmitUntilRegistrationIsComplete(): void
{
$id = $this->createQuestion();
$complete = clone Auth::guard('mobile')->user();
$pending = new User();
$pending->forceFill(['id' => 200, 'mobile' => '13800000000']);
Auth::guard('mobile')->setUser($pending);
$detail = '/api/mobile/course/evaluation-detail?course_content_evaluation_id=10';
$payload = ['course_content_evaluation_id' => 10, 'answer_version' => 2, 'data' => [['id' => $id, 'value' => ['count' => 0, 'follow' => 'yes']]]];
$this->getJson($detail)->assertJsonPath('requires_profile', true);
$this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('errmsg', '请先完成绑定或注册资料');
$this->assertSame(0, \App\Models\CourseContentEvaluationForm::count());
foreach (['name', 'sex', 'birthday', 'email', 'company_name', 'company_position'] as $field) {
$missing = clone $complete;
$missing->$field = null;
$this->assertFalse(\App\Services\Survey\SurveyIdentity::complete($missing));
}
$monthOnly = clone $complete;
$monthOnly->birthday = '1990-01';
$this->assertFalse(\App\Services\Survey\SurveyIdentity::complete($monthOnly));
Auth::guard('mobile')->setUser($complete);
$this->getJson($detail)->assertJsonPath('requires_profile', false);
$this->postJson('/api/mobile/course/course-content-form', $payload)->assertJsonPath('user_id', 200);
}
public function testRespondentFallbackIsSharedByListsDetailsAndExportWithoutUpdatingUsers(): void
{
Schema::table('users', function (Blueprint $table) {
foreach (['name', 'mobile', 'no', 'company_name', 'company_position'] as $field) $table->string($field)->nullable();
});
Schema::create('course_appointment_totals', function (Blueprint $table) { $table->id(); $table->integer('user_id'); $table->integer('total'); $table->dateTime('end_time'); $table->softDeletes(); });
DB::table('users')->insert(['id' => 200, 'mobile' => '13800000000']);
$questions = $answers = [];
$identity = ['name' => '问卷姓名', 'company_name' => '问卷公司', 'company_position' => '问卷职务', 'no' => 'S001'];
foreach ($identity as $field => $value) {
$id = count($questions) + 1;
$questions[] = ['id' => $id, 'field' => 'q_'.$id, 'name' => $field, 'edit_input' => 'text', 'config' => ['user_field' => $field]];
$answers[] = ['id' => $id, 'value' => $value];
}
$reply = \App\Models\CourseContentEvaluationForm::create(['course_content_evaluation_id' => 10, 'user_id' => 200, 'snapshot' => ['version' => 2, 'questions' => $questions], 'data' => $answers]);
$this->getJson('/api/admin/course-content-evaluation-form/index')->assertJsonPath('data.0.respondent.name', '问卷姓名')->assertJsonPath('data.0.respondent.company_name', '问卷公司')->assertJsonPath('data.0.user.name', null);
$this->getJson('/api/admin/course-content-evaluation-form/show?id='.$reply->id)->assertJsonPath('respondent.company_position', '问卷职务')->assertJsonPath('respondent.no', 'S001');
$this->assertNull(DB::table('users')->where('id', 200)->value('name'));
DB::table('users')->where('id', 200)->update(['company_name' => '个人资料公司']);
$this->getJson('/api/admin/course-content-evaluation-form/show?id='.$reply->id)->assertJsonPath('respondent.company_name', '个人资料公司')->assertJsonPath('respondent.name', '问卷姓名');
$path = \App\Services\Survey\SurveyExport::build(\App\Models\CourseContentEvaluation::find(10));
$zip = new \ZipArchive(); $zip->open($path);
file_put_contents($path.'.xlsx', $zip->getFromName('问卷答卷明细.xlsx'));
$book = \PhpOffice\PhpSpreadsheet\IOFactory::load($path.'.xlsx');
$this->assertSame('问卷姓名', $book->getSheetByName('问题详细表')->getCell('B3')->getValue());
$book->disconnectWorksheets(); $zip->close(); unlink($path.'.xlsx'); unlink($path);
$legacy = [];
foreach ($questions as $index => $question) $legacy[] = array_merge($question, $answers[$index]);
$this->assertSame('问卷姓名', \App\Services\Survey\SurveyIdentity::respondent(null, $legacy, null)['name']);
$this->postJson('/api/mobile/user/update-user', ['survey_id' => 10, 'username' => '未完整注册'])->assertJsonPath('errmsg', '请完整填写姓名、手机号、性别、出生日期、邮箱、公司和职务');
$this->assertNull(DB::table('users')->where('id', 200)->value('name'));
}
public function testConditionalRequiredAnswersAreEnforcedOnSubmissionAndResubmission(): void
{
$base = '/api/admin/course-content-evaluation-ask/save';
@ -104,7 +162,7 @@ class SurveyWorkflowTest extends TestCase
$this->getJson('/api/mobile/course/evaluation-detail?course_content_evaluation_id=10')->assertJsonPath('my_response', null);
}
public function testExportMergesChaptersAndKeepsReplyBoundaries(): void
public function testExportUsesQuestionHeadersAndOneRowPerReply(): void
{
$questions = [
['id' => 1, 'name' => '第一章', 'edit_input' => 'section'],
@ -127,10 +185,42 @@ class SurveyWorkflowTest extends TestCase
file_put_contents($path.'.xlsx', $zip->getFromName('问卷答卷明细.xlsx'));
$book = \PhpOffice\PhpSpreadsheet\IOFactory::load($path.'.xlsx');
$sheet = $book->getSheetByName('问题详细表');
$this->assertSame(['答卷ID', '答题人姓名', '章节', '题号', '题目', '子题/维度', '答案', '补充说明', '题型', '题目说明', '所有选项'], $sheet->rangeToArray('A1:K1')[0]);
foreach (['A2:A5', 'B2:B5', 'A6:A9', 'C2:C4', 'C6:C8', 'E2:E3', 'E4:F4'] as $range) $this->assertContains($range, $sheet->getMergeCells());
$this->assertSame('第二章', $sheet->getCell('C5')->getValue());
$this->assertSame('建议内容', $sheet->getCell('G4')->getValue());
$this->assertSame(['答卷ID', '答题人姓名', '组合题', null, '建议', '评价'], $sheet->rangeToArray('A1:F1')[0]);
foreach (['A1:A2', 'B1:B2', 'C1:D1', 'E1:E2', 'F1:F2'] as $range) $this->assertContains($range, $sheet->getMergeCells());
$this->assertSame(['数量A', '数量B'], $sheet->rangeToArray('C2:D2')[0]);
$this->assertSame(4, $sheet->getHighestRow());
$this->assertSame('0', $sheet->getCell('C3')->getValue());
$this->assertSame('2', $sheet->getCell('D3')->getValue());
$this->assertSame('建议内容', $sheet->getCell('E3')->getValue());
$this->assertSame('评价内容', $sheet->getCell('F4')->getValue());
$book->disconnectWorksheets();
$zip->close();
unlink($path.'.xlsx');
unlink($path);
}
public function testWideExportKeepsNotesHiddenAnswersAndDuplicateTitlesInTheirOwnColumns(): void
{
$questions = [
['id' => 1, 'name' => '同名题', 'field' => 'choice', 'edit_input' => 'checkbox', 'select_item' => [['key' => 'a', 'value' => '财税'], ['key' => 'b', 'value' => '法律']]],
['id' => 2, 'name' => '同名题', 'field' => 'note', 'edit_input' => 'text'],
['id' => 3, 'name' => '隐藏题', 'field' => 'hidden', 'edit_input' => 'text', 'config' => ['visible_when' => ['field' => 'choice', 'values' => ['b']]]],
];
$reply = \App\Models\CourseContentEvaluationForm::create(['course_content_evaluation_id' => 10, 'user_id' => 200, 'snapshot' => ['version' => 2, 'questions' => $questions], 'data' => [
['id' => 1, 'value' => ['a'], 'option_texts' => ['a' => '咨询税务'], 'other_text' => '其他意见'],
['id' => 2, 'value' => '=1+2'], ['id' => 3, 'value' => '不应导出的旧答案'],
]]);
$path = \App\Services\Survey\SurveyExport::build(\App\Models\CourseContentEvaluation::find(10));
$zip = new \ZipArchive();
$zip->open($path);
file_put_contents($path.'.xlsx', $zip->getFromName('问卷答卷明细.xlsx'));
$book = \PhpOffice\PhpSpreadsheet\IOFactory::load($path.'.xlsx');
$sheet = $book->getSheetByName('问题详细表');
$this->assertSame(['答卷ID', '答题人姓名', '同名题', '同名题', '隐藏题'], $sheet->rangeToArray('A1:E1')[0]);
$this->assertSame("财税\n补充:其他意见\n财税:咨询税务", $sheet->getCell('C3')->getValue());
$this->assertSame('=1+2', $sheet->getCell('D3')->getValue());
$this->assertSame('s', $sheet->getCell('D3')->getDataType());
$this->assertEmpty($sheet->getCell('E3')->getValue());
$book->disconnectWorksheets();
$zip->close();
unlink($path.'.xlsx');
@ -179,9 +269,9 @@ class SurveyWorkflowTest extends TestCase
$xlsxPath = $path.'.xlsx';
file_put_contents($xlsxPath, $zip->getFromName('问卷答卷明细.xlsx'));
$book = \PhpOffice\PhpSpreadsheet\IOFactory::load($xlsxPath);
$this->assertSame('=SUM(1,2)', $book->getSheetByName('答卷列表')->getCell('C2')->getValue());
$this->assertSame('s', $book->getSheetByName('答卷列表')->getCell('C2')->getDataType());
$this->assertSame('调研附件.pdf', $book->getSheetByName('问题详细表')->getCell('E2')->getValue());
$this->assertSame('=SUM(1,2)', $book->getSheetByName('问题详细表')->getCell('B3')->getValue());
$this->assertSame('s', $book->getSheetByName('问题详细表')->getCell('B3')->getDataType());
$this->assertSame('调研附件.pdf', $book->getSheetByName('问题详细表')->getCell('C3')->getValue());
$entry = $book->getSheetByName('附件清单')->getCell('E2')->getValue();
$this->assertSame('example attachment', $zip->getFromName($entry));
$book->disconnectWorksheets();
@ -284,15 +374,13 @@ class SurveyWorkflowTest extends TestCase
file_put_contents($path.'.xlsx', $zip->getFromName('问卷答卷明细.xlsx'));
$book = \PhpOffice\PhpSpreadsheet\IOFactory::load($path.'.xlsx');
$sheet = $book->getSheetByName('问题详细表');
$this->assertSame('融资对接', $sheet->getCell('D2')->getValue());
$this->assertSame('意向数量', $sheet->getCell('E2')->getValue());
$this->assertSame('0', $sheet->getCell('F2')->getValue());
$this->assertSame('否', $sheet->getCell('F3')->getValue());
$this->assertContains('A2:A3', $sheet->getMergeCells());
$this->assertContains('B2:B3', $sheet->getMergeCells());
$this->assertContains('D2:D3', $sheet->getMergeCells());
$this->assertSame('题号', $sheet->getCell('C1')->getValue());
$this->assertSame('补充说明', $sheet->getCell('G1')->getValue());
$this->assertSame('融资对接', $sheet->getCell('C1')->getValue());
$this->assertSame('意向数量', $sheet->getCell('C2')->getValue());
$this->assertSame('需要跟进', $sheet->getCell('D2')->getValue());
$this->assertSame('0', $sheet->getCell('C3')->getValue());
$this->assertSame('否', $sheet->getCell('D3')->getValue());
$this->assertSame(3, $sheet->getHighestRow());
$this->assertContains('C1:D1', $sheet->getMergeCells());
$book->disconnectWorksheets();
$zip->close();
unlink($path.'.xlsx');

Loading…
Cancel
Save