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.

247 lines
6.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Competition extends Model
{
use HasFactory;
public const STATUSES = [
'draft',
'published',
'signup_open',
'signup_closed',
'reviewing',
'ended',
];
protected $fillable = [
'slug',
'name',
'description',
'pledge_title',
'pledge_title_en',
'pledge_subtitle',
'pledge_subtitle_en',
'pledge_content_html',
'pledge_content_html_en',
'status',
'published',
'signup_open_at',
'signup_close_at',
'form_schema_id',
'review_form_schema_id',
'audience_form_schema_id',
'branding_json',
'settings',
'scoring_rules_json',
];
/**
* @var array<string, string>
*/
protected $casts = [
'published' => 'boolean',
'signup_open_at' => 'datetime',
'signup_close_at' => 'datetime',
'branding_json' => 'array',
'settings' => 'array',
'scoring_rules_json' => 'array',
];
public function tracks(): HasMany
{
return $this->hasMany(CompetitionTrack::class)->orderBy('sort');
}
public function signupChannels(): HasMany
{
return $this->hasMany(SignupChannel::class)->orderByDesc('id');
}
public function audienceSessions(): HasMany
{
return $this->hasMany(AudienceSession::class)->orderBy('sort')->orderBy('id');
}
public function competitionAdmins(): HasMany
{
return $this->hasMany(CompetitionAdmin::class);
}
/** 嵌套路由 competitions.admins 的绑定关系名 */
public function admins(): HasMany
{
return $this->competitionAdmins();
}
public function audienceRegistrations(): HasMany
{
return $this->hasMany(AudienceRegistration::class);
}
public function formSchemaDefinitions(): HasMany
{
return $this->hasMany(FormSchemaDefinition::class, 'competition_id');
}
/**
* 嵌套路由参数 `{form_schema}` 的隐式绑定约定关系名为 formSchemas见 Model::childRouteBindingRelationshipName
*/
public function formSchemas(): HasMany
{
return $this->formSchemaDefinitions();
}
public function formSchema(): BelongsTo
{
return $this->belongsTo(FormSchemaDefinition::class, 'form_schema_id');
}
public function reviewFormSchema(): BelongsTo
{
return $this->belongsTo(FormSchemaDefinition::class, 'review_form_schema_id');
}
public function audienceFormSchema(): BelongsTo
{
return $this->belongsTo(FormSchemaDefinition::class, 'audience_form_schema_id');
}
/**
* 评审端是否开放登录/访问。settings.review_portal.enabled 缺省或非 false 时视为开启。
*/
public function isReviewPortalEnabled(): bool
{
$portal = $this->reviewPortalSettings();
if ($portal === null || ! array_key_exists('enabled', $portal)) {
return true;
}
return $portal['enabled'] !== false
&& $portal['enabled'] !== 0
&& $portal['enabled'] !== '0'
&& $portal['enabled'] !== 'false';
}
/**
* 关闭评审端时展示给评审员的说明(登录弹窗 / 接口提示)。
*/
public function reviewPortalClosedMessage(): string
{
$portal = $this->reviewPortalSettings();
$message = is_array($portal) ? trim((string) ($portal['message'] ?? '')) : '';
if ($message === '') {
return '评审通道暂未开放,请稍后再试';
}
return mb_substr($message, 0, 2000);
}
/**
* 公开接口用的评审端开关摘要。
*
* @return array{enabled: bool, message: string}
*/
public function reviewPortalPublicPayload(): array
{
$enabled = $this->isReviewPortalEnabled();
$portal = $this->reviewPortalSettings();
$stored = is_array($portal) ? trim((string) ($portal['message'] ?? '')) : '';
return [
'enabled' => $enabled,
'message' => $enabled
? mb_substr($stored, 0, 2000)
: $this->reviewPortalClosedMessage(),
];
}
/**
* @return array<string, mixed>|null
*/
private function reviewPortalSettings(): ?array
{
$settings = is_array($this->settings) ? $this->settings : [];
$portal = $settings['review_portal'] ?? null;
return is_array($portal) ? $portal : null;
}
/**
* 报名截止后是否开启「指定用户仅可补传/删除附件」。
*/
public function isPostCloseFileEditEnabled(): bool
{
$cfg = $this->postCloseFileEditSettings();
if ($cfg === null || ! array_key_exists('enabled', $cfg)) {
return false;
}
return $cfg['enabled'] === true
|| $cfg['enabled'] === 1
|| $cfg['enabled'] === '1'
|| $cfg['enabled'] === 'true';
}
/**
* @return list<string> 规范化后的登录手机号白名单
*/
public function postCloseFileEditMobiles(): array
{
$cfg = $this->postCloseFileEditSettings();
$raw = is_array($cfg) ? ($cfg['user_mobiles'] ?? null) : null;
if (! is_array($raw)) {
return [];
}
$out = [];
foreach ($raw as $item) {
$mobile = self::normalizeAllowlistMobile($item);
// 勿用手机号做关联数组 keyPHP 会把纯数字字符串键转成 int导致严格比较失败
if ($mobile !== '' && ! in_array($mobile, $out, true)) {
$out[] = $mobile;
}
}
return $out;
}
public function allowsPostCloseFileEditForMobile(?string $mobile): bool
{
if (! $this->isPostCloseFileEditEnabled()) {
return false;
}
$normalized = self::normalizeAllowlistMobile($mobile);
if ($normalized === '') {
return false;
}
return in_array($normalized, $this->postCloseFileEditMobiles(), true);
}
public static function normalizeAllowlistMobile(mixed $mobile): string
{
$s = preg_replace('/\s+/', '', trim((string) $mobile)) ?? '';
return $s;
}
/**
* @return array<string, mixed>|null
*/
private function postCloseFileEditSettings(): ?array
{
$settings = is_array($this->settings) ? $this->settings : [];
$cfg = $settings['post_close_file_edit'] ?? null;
return is_array($cfg) ? $cfg : null;
}
}