master
lion 1 month ago
parent df37b3a9b6
commit 795f34c0fc

@ -5,6 +5,7 @@ namespace App\Services;
use App\Models\Application;
use App\Models\ApplicationFile;
use App\Models\Competition;
use App\Models\ReviewerScope;
use App\Support\ProjectCode;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
@ -329,16 +330,22 @@ class AdminApplicationExportService
'项目简介',
'提交时间',
'附件数量',
'进程',
'评分详情',
]));
$reviewersByTrack = $this->reviewersByTrackForCompetition($competition);
$chunkSize = max(20, (int) config('contest.export.chunk_size', 20));
$this->buildFilteredQuery($competition, $filters)
->with(['signupChannel'])
->with(['signupChannel', 'reviewScores.reviewer'])
->withCount('files')
->orderBy('id')
->chunkById($chunkSize, function ($applications) use ($writer, $trackTitles): void {
->chunkById($chunkSize, function ($applications) use ($writer, $trackTitles, $reviewersByTrack): void {
foreach ($applications as $app) {
$writer->addRow(Row::fromValues($this->applicationExportRow($app, $trackTitles)));
$writer->addRow(Row::fromValues(
$this->applicationExportRow($app, $trackTitles, $reviewersByTrack)
));
}
}, 'id');
@ -372,16 +379,73 @@ class AdminApplicationExportService
}
}
/**
* 按赛道预加载评委名单(含姓名),用于导出进程与评分详情。
*
* @return Collection<string, list<array{id: int, name: string}>>
*/
private function reviewersByTrackForCompetition(Competition $competition): Collection
{
$grouped = [];
ReviewerScope::query()
->with('reviewer')
->where('competition_id', $competition->id)
->orderBy('id')
->get()
->each(function (ReviewerScope $scope) use (&$grouped): void {
$track = (string) ($scope->track_code ?? '');
if ($track === '') {
return;
}
$reviewerId = (int) $scope->reviewer_id;
$name = trim((string) ($scope->reviewer?->name ?? ''));
if ($name === '') {
$name = '评委#'.$reviewerId;
}
$grouped[$track] ??= [];
$grouped[$track][$reviewerId] = [
'id' => $reviewerId,
'name' => $name,
];
});
return collect($grouped)->map(fn (array $rows) => array_values($rows));
}
/**
* @param Collection<string, string> $trackTitles
* @param Collection<string, list<array{id: int, name: string}>> $reviewersByTrack
* @return list<scalar|null>
*/
private function applicationExportRow(Application $app, Collection $trackTitles): array
private function applicationExportRow(Application $app, Collection $trackTitles, Collection $reviewersByTrack): array
{
$channel = $app->signupChannel
? $app->signupChannel->channel_name.'('.$app->signupChannel->channel_code.')'
: ($app->signup_channel_code ?: '');
$trackReviewers = $reviewersByTrack->get((string) ($app->track ?? '')) ?? [];
$required = count($trackReviewers);
$scoresByReviewerId = [];
if ($app->relationLoaded('reviewScores')) {
foreach ($app->reviewScores as $score) {
$scoresByReviewerId[(int) $score->reviewer_id] = $score;
}
}
$completed = count($scoresByReviewerId);
$progress = $required > 0 ? ($completed.'/'.$required) : '-';
$scoreDetails = collect($trackReviewers)
->map(function (array $reviewer) use ($scoresByReviewerId): string {
$name = $reviewer['name'];
$score = $scoresByReviewerId[$reviewer['id']] ?? null;
if ($score === null) {
return $name.' 未评审';
}
return $name.' '.$this->formatExportScore($score->line_total);
})
->implode(';');
return [
ProjectCode::resolve($app),
$app->status === 'submitted' ? '已提交' : '草稿',
@ -400,9 +464,23 @@ class AdminApplicationExportService
$app->intro ?? '',
$app->submitted_at?->format('Y-m-d H:i:s') ?? '',
(int) ($app->files_count ?? $app->files->count()),
$progress,
$scoreDetails,
];
}
private function formatExportScore(mixed $value): string
{
if ($value === null || $value === '') {
return '—';
}
if (! is_numeric($value)) {
return (string) $value;
}
return rtrim(rtrim(number_format((float) $value, 2, '.', ''), '0'), '.') ?: '0';
}
/**
* @param array<string, mixed> $filters
*/

@ -4,11 +4,16 @@ namespace Tests\Feature;
use App\Models\AdminUser;
use App\Models\Application;
use App\Models\ApplicationReviewScore;
use App\Models\Competition;
use App\Models\CompetitionTrack;
use App\Models\Reviewer;
use App\Models\ReviewerScope;
use App\Models\User;
use App\Services\AdminApplicationExportService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Laravel\Sanctum\Sanctum;
use OpenSpout\Reader\XLSX\Reader;
use Tests\TestCase;
class AdminApplicationExportTest extends TestCase
@ -79,4 +84,98 @@ class AdminApplicationExportTest extends TestCase
->assertStatus(422)
->assertJsonValidationErrors(['application_ids']);
}
public function test_xlsx_export_includes_progress_and_score_details(): void
{
$admin = AdminUser::query()->create([
'username' => 'admin_export3',
'password_hash' => 'unused',
'name' => '管理员3',
'status' => 'active',
]);
$competition = Competition::query()->create([
'slug' => 'xxf-export-score',
'name' => '导出评分赛',
'published' => true,
]);
CompetitionTrack::query()->create([
'competition_id' => $competition->id,
'track_code' => 'track_a',
'title' => '赛道A',
'sort' => 1,
'is_enabled' => true,
]);
$reviewer1 = Reviewer::query()->create([
'mobile' => '13900139101',
'username' => 'rev_export_1',
'name' => '评委甲',
'status' => 'active',
'password_hash' => 'unused',
]);
$reviewer2 = Reviewer::query()->create([
'mobile' => '13900139102',
'username' => 'rev_export_2',
'name' => '评委乙',
'status' => 'active',
'password_hash' => 'unused',
]);
ReviewerScope::query()->create([
'competition_id' => $competition->id,
'reviewer_id' => $reviewer1->id,
'track_code' => 'track_a',
]);
ReviewerScope::query()->create([
'competition_id' => $competition->id,
'reviewer_id' => $reviewer2->id,
'track_code' => 'track_a',
]);
$application = Application::query()->create([
'user_id' => User::query()->create(['mobile' => '13800138111'])->id,
'competition_id' => $competition->id,
'status' => 'submitted',
'project_name' => '导出评分项目',
'track' => 'track_a',
]);
ApplicationReviewScore::query()->create([
'application_id' => $application->id,
'reviewer_id' => $reviewer1->id,
'payload_json' => ['line_total' => 88.5],
'line_total' => 88.5,
]);
Sanctum::actingAs($admin);
$service = app(AdminApplicationExportService::class);
$xlsxPath = $service->buildXlsxExportPath($competition, ['status' => 'submitted']);
$this->assertFileExists($xlsxPath);
$reader = new Reader;
$reader->open($xlsxPath);
$rows = [];
foreach ($reader->getSheetIterator() as $sheet) {
foreach ($sheet->getRowIterator() as $row) {
$rows[] = $row->toArray();
}
break;
}
$reader->close();
@unlink($xlsxPath);
$this->assertGreaterThanOrEqual(2, count($rows));
$header = $rows[0];
$this->assertContains('进程', $header);
$this->assertContains('评分详情', $header);
$progressIdx = array_search('进程', $header, true);
$detailIdx = array_search('评分详情', $header, true);
$this->assertNotFalse($progressIdx);
$this->assertNotFalse($detailIdx);
$data = $rows[1];
$this->assertSame('1/2', (string) $data[$progressIdx]);
$this->assertStringContainsString('评委甲 88.5', (string) $data[$detailIdx]);
$this->assertStringContainsString('评委乙 未评审', (string) $data[$detailIdx]);
}
}

Loading…
Cancel
Save