|
|
<?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 App\Support\SignupFormFileRules;
|
|
|
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;
|
|
|
}
|
|
|
|
|
|
/** 当前赛事报名表 schema 是否包含指定字段 key */
|
|
|
private function signupSchemaHasKey(Competition $competition, string $key): bool
|
|
|
{
|
|
|
$competition->loadMissing('formSchema');
|
|
|
$rows = $competition->formSchema?->schema_json;
|
|
|
if (! is_array($rows) || count($rows) === 0) {
|
|
|
// 与选手端 normalizeSignupSchema([]) 兜底 DEFAULT_SIGNUP_FORM_SCHEMA 一致(含 entry_group)
|
|
|
return $key === 'entry_group';
|
|
|
}
|
|
|
foreach ($rows as $row) {
|
|
|
if (is_array($row) && ($row['key'] ?? '') === $key) {
|
|
|
return true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @return list<string>
|
|
|
*/
|
|
|
private function entryGroupOptionValues(): array
|
|
|
{
|
|
|
$raw = config('contest.entry_groups', ['创新组', '创业组']);
|
|
|
if (! is_array($raw) || count($raw) === 0) {
|
|
|
return ['创新组', '创业组'];
|
|
|
}
|
|
|
|
|
|
return array_values(array_filter(array_map('strval', $raw), fn (string $v) => $v !== ''));
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 报名表 schema 中 entry_group 下拉允许的「值」(与选项 `value` 一致,如 entry_group1)。
|
|
|
*
|
|
|
* @return list<string>
|
|
|
*/
|
|
|
private function entryGroupSelectValuesFromSignupSchema(Competition $competition): array
|
|
|
{
|
|
|
$competition->loadMissing('formSchema');
|
|
|
$rows = $competition->formSchema?->schema_json;
|
|
|
if (! is_array($rows)) {
|
|
|
return [];
|
|
|
}
|
|
|
foreach ($rows as $row) {
|
|
|
if (! is_array($row) || ($row['key'] ?? '') !== 'entry_group') {
|
|
|
continue;
|
|
|
}
|
|
|
$opts = $row['options'] ?? [];
|
|
|
if (! is_array($opts)) {
|
|
|
return [];
|
|
|
}
|
|
|
$out = [];
|
|
|
foreach ($opts as $opt) {
|
|
|
if (is_array($opt) && array_key_exists('value', $opt)) {
|
|
|
$s = trim((string) $opt['value']);
|
|
|
if ($s !== '') {
|
|
|
$out[] = $s;
|
|
|
}
|
|
|
} elseif (is_string($opt)) {
|
|
|
$s = trim($opt);
|
|
|
if ($s !== '') {
|
|
|
$out[] = $s;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return array_values(array_unique($out));
|
|
|
}
|
|
|
|
|
|
return [];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 参赛组别取何值时企业名称必填:优先读 company_name 的 required_when,否则读 config。
|
|
|
*
|
|
|
* @return list<string>
|
|
|
*/
|
|
|
private function companyNameRequiredWhenEntryGroupValues(Competition $competition): array
|
|
|
{
|
|
|
$competition->loadMissing('formSchema');
|
|
|
$rows = $competition->formSchema?->schema_json;
|
|
|
if (! is_array($rows)) {
|
|
|
$rows = [];
|
|
|
}
|
|
|
foreach ($rows as $row) {
|
|
|
if (! is_array($row) || ($row['key'] ?? '') !== 'company_name') {
|
|
|
continue;
|
|
|
}
|
|
|
$rw = $row['required_when'] ?? null;
|
|
|
if (! is_array($rw) || ($rw['field'] ?? '') !== 'entry_group') {
|
|
|
break;
|
|
|
}
|
|
|
$vals = $rw['values'] ?? [];
|
|
|
if (! is_array($vals)) {
|
|
|
break;
|
|
|
}
|
|
|
$out = [];
|
|
|
foreach ($vals as $v) {
|
|
|
$s = trim((string) $v);
|
|
|
if ($s !== '') {
|
|
|
$out[] = $s;
|
|
|
}
|
|
|
}
|
|
|
if (count($out) > 0) {
|
|
|
return array_values(array_unique($out));
|
|
|
}
|
|
|
break;
|
|
|
}
|
|
|
|
|
|
$cv = trim((string) config('contest.entry_group_company_required_value', '创业组'));
|
|
|
|
|
|
return $cv !== '' ? [$cv] : [];
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @return list<string>
|
|
|
*/
|
|
|
private function allowedEntryGroupValues(Competition $competition): array
|
|
|
{
|
|
|
$fromSchema = $this->entryGroupSelectValuesFromSignupSchema($competition);
|
|
|
if (count($fromSchema) > 0) {
|
|
|
return $fromSchema;
|
|
|
}
|
|
|
|
|
|
return $this->entryGroupOptionValues();
|
|
|
}
|
|
|
|
|
|
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', []);
|
|
|
|
|
|
$companyRules = ['nullable', 'string', 'max:255'];
|
|
|
if ($this->signupSchemaHasKey($competition, 'entry_group')) {
|
|
|
$reqVals = $this->companyNameRequiredWhenEntryGroupValues($competition);
|
|
|
if (count($reqVals) > 0) {
|
|
|
$companyRules[] = Rule::requiredIf(function () use ($request, $reqVals): bool {
|
|
|
$eg = (string) $request->input('entry_group', '');
|
|
|
|
|
|
return in_array($eg, $reqVals, true);
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$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' => $companyRules,
|
|
|
'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->signupSchemaHasKey($competition, 'entry_group')) {
|
|
|
$rules['entry_group'] = ['required', 'string', Rule::in($this->allowedEntryGroupValues($competition))];
|
|
|
}
|
|
|
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', []);
|
|
|
|
|
|
$companyRules = ['nullable', 'string', 'max:255'];
|
|
|
if ($this->signupSchemaHasKey($competition, 'entry_group')) {
|
|
|
$reqVals = $this->companyNameRequiredWhenEntryGroupValues($competition);
|
|
|
if (count($reqVals) > 0) {
|
|
|
$companyRules[] = Rule::requiredIf(function () use ($request, $reqVals): bool {
|
|
|
$eg = (string) $request->input('entry_group', '');
|
|
|
|
|
|
return in_array($eg, $reqVals, true);
|
|
|
});
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$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' => $companyRules,
|
|
|
'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->signupSchemaHasKey($competition, 'entry_group')) {
|
|
|
$rules['entry_group'] = ['required', 'string', Rule::in($this->allowedEntryGroupValues($competition))];
|
|
|
}
|
|
|
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' => ['请至少上传一份商业计划书'],
|
|
|
]);
|
|
|
}
|
|
|
$planRow = SignupFormFileRules::fileFieldRow($competition, 'plan');
|
|
|
$maxPlanFiles = SignupFormFileRules::maxCount($planRow);
|
|
|
if ($maxPlanFiles !== null && $planCount > $maxPlanFiles) {
|
|
|
throw ValidationException::withMessages([
|
|
|
'files' => ['商业计划书最多可上传 '.$maxPlanFiles.' 个文件'],
|
|
|
]);
|
|
|
}
|
|
|
if ($this->signupSchemaHasKey($competition, 'supporting')) {
|
|
|
$supportingCount = $app->files()->where('kind', 'supporting')->count();
|
|
|
$supRow = SignupFormFileRules::fileFieldRow($competition, 'supporting');
|
|
|
$maxSup = SignupFormFileRules::maxCount($supRow);
|
|
|
if ($maxSup !== null && $supportingCount > $maxSup) {
|
|
|
throw ValidationException::withMessages([
|
|
|
'files' => ['佐证材料最多可上传 '.$maxSup.' 个文件'],
|
|
|
]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
$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,
|
|
|
'entry_group' => $app->entry_group,
|
|
|
'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->participantPreviewSignedUrl(),
|
|
|
])->values(),
|
|
|
];
|
|
|
}
|
|
|
}
|