belongsTo(Application::class); } public function publicUrl(): string { $u = Storage::disk($this->disk)->url($this->path); return preg_replace('#([^:])//+#', '$1/', $u) ?? $u; } /** * 浏览器下载/「另存为」显示名:优先用户上传时的原始文件名,必要时按存储路径补全扩展名(避免无后缀时落到随机 storage 名)。 */ public function clientDownloadName(): string { $raw = $this->original_name !== null && trim($this->original_name) !== '' ? basename(str_replace(['\\', '/'], '_', $this->original_name)) : ''; $storageExt = strtolower((string) pathinfo($this->path, PATHINFO_EXTENSION)); if ($raw !== '') { $baseExt = strtolower((string) pathinfo($raw, PATHINFO_EXTENSION)); if ($storageExt !== '' && $baseExt === '') { $raw .= '.'.$storageExt; } return $raw; } return $storageExt !== '' ? '附件.'.$storageExt : '附件'; } /** * PDF/图片在浏览器内联预览,其余类型用 attachment 以触发「保存为」时使用 clientDownloadName,避免地址栏/标题仍显示 storage 随机名。 */ public function preferredStreamDisposition(): string { $mime = strtolower((string) ($this->mime ?? '')); if ($mime !== '') { if (str_starts_with($mime, 'image/') || $mime === 'application/pdf') { return 'inline'; } return 'attachment'; } $ext = strtolower((string) pathinfo($this->path, PATHINFO_EXTENSION)); if ($ext === 'pdf') { return 'inline'; } return 'attachment'; } /** * 选手端「预览」用 URL:走 web 签名路由直出文件,不依赖 public/storage 符号链接(生产环境未 link 时 /storage/... 会 404)。 */ public function participantPreviewSignedUrl(int $ttlSeconds = 172800): string { $u = URL::temporarySignedRoute( 'participant.application-file.download', now()->addSeconds(max(60, $ttlSeconds)), ['file' => $this->id], ); return preg_replace('#([^:])//+#', '$1/', $u) ?? $u; } }