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.

243 lines
9.1 KiB

1 month ago
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Api\Concerns\ResolvesParticipantApplication;
use App\Http\Controllers\Controller;
use App\Models\Application;
use App\Models\Competition;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
class ApplicationController extends Controller
{
use ResolvesParticipantApplication;
private function ensureParticipantMayEditSignup(Application $app): void
{
$app->assertMayEditSignup('status');
}
private function currentApplication(Request $request): Application
{
return $this->participantApplication($request);
}
/**
* @return list<string>
*/
private function enabledTrackCodes(Competition $competition): array
{
return $competition->tracks()
->where('is_enabled', true)
->pluck('track_code')
->all();
}
/** 报名表 schema 是否包含参赛承诺书勾选(与选手端 key 一致) */
private function signupSchemaRequiresCommitment(Competition $competition): bool
{
$competition->loadMissing('formSchema');
$rows = $competition->formSchema?->schema_json;
if (! is_array($rows) || count($rows) === 0) {
return true;
}
foreach ($rows as $row) {
if (is_array($row) && ($row['key'] ?? '') === 'commitment_accepted') {
return true;
}
}
return false;
}
public function show(Request $request): JsonResponse
{
$app = $this->currentApplication($request);
$app->load(['files']);
return response()->json($this->transform($app));
}
public function update(Request $request): JsonResponse
{
$competition = $this->resolvePublishedCompetitionFromRequest($request);
$app = $this->currentApplication($request);
$this->ensureParticipantMayEditSignup($app);
$trackCodes = $this->enabledTrackCodes($competition);
$degrees = config('contest.degrees', []);
$countries = config('contest.location_countries', []);
$trackRules = count($trackCodes)
? ['nullable', 'string', Rule::in($trackCodes)]
: ['nullable', 'string', 'max:100'];
$rules = [
'player_name' => ['nullable', 'string', 'max:120'],
'school' => ['nullable', 'string', 'max:200'],
'degree' => ['nullable', 'string', Rule::in($degrees)],
'contact_email' => ['nullable', 'email', 'max:255'],
'contact_mobile' => ['nullable', 'regex:/^1[3-9]\d{9}$/'],
'company_name' => ['nullable', 'string', 'max:255'],
'project_name' => ['nullable', 'string', 'max:255'],
'track' => $trackRules,
'location_country' => ['nullable', 'string', Rule::in($countries)],
'location_province' => ['nullable', 'string', 'max:100'],
'location_city' => ['nullable', 'string', 'max:100'],
'oversea_country' => ['nullable', 'string', 'max:100'],
'intro' => ['nullable', 'string', 'max:5000'],
];
if ($this->signupSchemaRequiresCommitment($competition)) {
$rules['commitment_accepted'] = ['sometimes', 'boolean'];
$rules['promise_signature'] = ['nullable', 'string', 'max:2097152'];
}
$data = $request->validate($rules);
$commitmentAccepted = $data['commitment_accepted'] ?? null;
$promiseSignature = $data['promise_signature'] ?? null;
unset($data['commitment_accepted'], $data['promise_signature']);
$app->fill($data);
if ($this->signupSchemaRequiresCommitment($competition)) {
if ($commitmentAccepted === true) {
if (! is_string($promiseSignature) || trim($promiseSignature) === '') {
throw ValidationException::withMessages([
'promise_signature' => ['请完成手写签名后再保存'],
]);
}
$app->promise_signed_at = now();
$app->promise_signature = $promiseSignature;
} elseif ($commitmentAccepted === false) {
$app->promise_signed_at = null;
$app->promise_signature = null;
}
}
if (! empty($data['player_name']) && empty($request->user()->name)) {
$request->user()->update(['name' => $data['player_name']]);
}
if (! empty($data['contact_email']) && empty($request->user()->email)) {
$request->user()->update(['email' => $data['contact_email']]);
}
$app->save();
$app->load(['files']);
return response()->json($this->transform($app));
}
public function submit(Request $request): JsonResponse
{
$competition = $this->resolvePublishedCompetitionFromRequest($request);
$app = $this->currentApplication($request);
$this->ensureParticipantMayEditSignup($app);
$trackCodes = $this->enabledTrackCodes($competition);
if (count($trackCodes) === 0) {
throw ValidationException::withMessages([
'track' => ['本场赛事尚未配置可用赛道,请联系管理员'],
]);
}
$degrees = config('contest.degrees', []);
$countries = config('contest.location_countries', []);
$rules = [
'player_name' => ['required', 'string', 'max:120'],
'school' => ['required', 'string', 'max:200'],
'degree' => ['required', 'string', Rule::in($degrees)],
'contact_email' => ['required', 'email', 'max:255'],
'contact_mobile' => ['required', 'regex:/^1[3-9]\d{9}$/'],
'company_name' => ['nullable', 'string', 'max:255'],
'project_name' => ['required', 'string', 'max:255'],
'track' => ['required', 'string', Rule::in($trackCodes)],
'location_country' => ['required', 'string', Rule::in($countries)],
'location_province' => ['nullable', 'string', 'max:100', 'required_if:location_country,中国'],
'location_city' => ['nullable', 'string', 'max:100', 'required_if:location_country,中国'],
'oversea_country' => ['nullable', 'string', 'max:100', 'required_if:location_country,海外'],
'intro' => ['nullable', 'string', 'max:5000'],
];
if ($this->signupSchemaRequiresCommitment($competition)) {
$rules['commitment_accepted'] = ['required', 'accepted'];
$rules['promise_signature'] = ['required', 'string', 'max:2097152'];
}
$data = $request->validate($rules);
$promiseSignature = $data['promise_signature'] ?? '';
unset($data['commitment_accepted'], $data['promise_signature']);
$planCount = $app->files()->where('kind', 'plan')->count();
if ($planCount < 1) {
throw ValidationException::withMessages([
'files' => ['请至少上传一份商业计划书'],
]);
}
$app->fill($data);
if ($this->signupSchemaRequiresCommitment($competition)) {
if (! is_string($promiseSignature) || trim($promiseSignature) === '') {
throw ValidationException::withMessages([
'promise_signature' => ['请完成参赛承诺书手写签名'],
]);
}
$app->promise_signature = $promiseSignature;
$app->promise_signed_at = now();
}
$app->status = 'submitted';
$app->submitted_at = now();
$app->save();
$request->user()->update([
'name' => $data['player_name'],
'email' => $data['contact_email'],
]);
$app->load(['files']);
return response()->json($this->transform($app));
}
private function transform(Application $app): array
{
return [
'id' => $app->id,
'competition_id' => $app->competition_id,
'status' => $app->status,
'player_name' => $app->player_name,
'school' => $app->school,
'degree' => $app->degree,
'contact_email' => $app->contact_email,
'contact_mobile' => $app->contact_mobile,
'company_name' => $app->company_name,
'project_name' => $app->project_name,
'track' => $app->track,
'location_country' => $app->location_country,
'location_province' => $app->location_province,
'location_city' => $app->location_city,
'oversea_country' => $app->oversea_country,
'intro' => $app->intro,
'promise_signed_at' => $app->promise_signed_at?->toIso8601String(),
'promise_signature' => $app->promise_signature,
'submitted_at' => $app->submitted_at?->toIso8601String(),
'participant_may_edit' => $app->participantMayEditSignup(),
'files' => $app->files->map(fn ($f) => [
'id' => $f->id,
'kind' => $f->kind,
'original_name' => $f->original_name,
'size' => $f->size,
'url' => $f->publicUrl(),
])->values(),
];
}
}