'datetime', 'submitted_at' => 'datetime', 'signup_channel_entered_at' => 'datetime', 'deleted_at' => 'datetime', ]; public static function hasDeletedAtColumn(): bool { return Schema::hasColumn((new static)->getTable(), 'deleted_at'); } protected static function booted(): void { static::addGlobalScope(self::EXCLUDE_SOFT_DELETED_SCOPE, function (Builder $builder): void { if (! static::hasDeletedAtColumn()) { return; } $builder->whereNull($builder->getModel()->qualifyColumn('deleted_at')); }); static::created(function (Application $application): void { if (! Schema::hasColumn($application->getTable(), 'project_code')) { return; } if (trim((string) ($application->project_code ?? '')) === '') { $application->forceFill([ 'project_code' => ProjectCode::forApplication($application), ])->saveQuietly(); } }); } public static function firstOrCreateDraft(int $userId, int $competitionId): self { if (static::hasDeletedAtColumn()) { $trashed = static::withoutGlobalScope(self::EXCLUDE_SOFT_DELETED_SCOPE) ->where('user_id', $userId) ->where('competition_id', $competitionId) ->whereNotNull('deleted_at') ->first(); if ($trashed !== null) { $trashed->forceFill([ 'deleted_at' => null, 'status' => 'draft', ])->save(); return $trashed->fresh(); } } return static::query()->firstOrCreate( [ 'user_id' => $userId, 'competition_id' => $competitionId, ], ['status' => 'draft'] ); } public function user(): BelongsTo { return $this->belongsTo(User::class); } public function competition(): BelongsTo { return $this->belongsTo(Competition::class); } public function signupChannel(): BelongsTo { return $this->belongsTo(SignupChannel::class); } public function files(): HasMany { return $this->hasMany(ApplicationFile::class); } public function reviewRecords(): HasMany { return $this->hasMany(ApplicationReviewRecord::class); } public function reviewScores(): HasMany { return $this->hasMany(ApplicationReviewScore::class); } public function isSubmitted(): bool { return $this->status === 'submitted'; } /** 评委已产生评审记录或打分后,选手端不可再修改报名/附件 */ public function isSignupLockedByReview(): bool { return $this->reviewRecords()->exists() || $this->reviewScores()->exists(); } /** 已超过赛事设置的报名截止时间(未设置截止时间则不据此拦截) */ public function isPastSignupCloseForCompetition(): bool { $this->loadMissing('competition'); $close = $this->competition?->signup_close_at; return $close !== null && now()->isAfter($close); } /** * 选手是否仍可编辑报名表:报名截止时间内且评委尚未评审;草稿与已提交均可继续改报直至截止或评审落库。 */ public function participantMayEditSignup(): bool { if ($this->isSignupLockedByReview()) { return false; } return ! $this->isPastSignupCloseForCompetition(); } /** * @param 'status'|'file' $field 422 时 errors 字段名(报名表用 status,附件接口用 file) */ public function assertMayEditSignup(string $field = 'status'): void { if ($this->isSignupLockedByReview()) { $msg = $field === 'file' ? '已有评委评审或打分记录,不可再上传或删除附件' : '已有评委评审或打分记录,报名内容不可再修改'; throw ValidationException::withMessages([$field => [$msg]]); } if ($this->isPastSignupCloseForCompetition()) { $msg = $field === 'file' ? '报名已截止,不可再上传或删除附件' : '报名已截止,无法再修改或提交报名'; throw ValidationException::withMessages([$field => [$msg]]); } } }