diff --git a/app/Http/Api.php b/app/Http/Api.php index 07a57f38..fe748a52 100644 --- a/app/Http/Api.php +++ b/app/Http/Api.php @@ -2,26 +2,27 @@ namespace App\Http; +use Illuminate\Http\Response; use JetBrains\PhpStorm\Pure; trait Api { #[Pure] - public function success(string $message = 'success', bool $status = true, array $data = []): array + public function success(string $message = 'success', array $data = []): Response { - return $this->response($status, $message, $data); + return $this->response(true, $message, $data); } #[Pure] - public function error(string $message = 'error', bool $status = false, array $data = []): array + public function error(string $message = 'error', array $data = []): Response { - return $this->response($status, $message, $data); + return $this->response(false, $message, $data); } #[Pure] - public function response(bool $status, string $message = '', array $data = []): array + public function response(bool $status, string $message = '', array $data = []): Response { $data = $data ?: new \stdClass; - return compact('status', 'message', 'data'); + return response(compact('status', 'message', 'data')); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 4719c977..032bf405 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -10,6 +10,7 @@ use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Http\Request; +use Illuminate\Http\Response; use Illuminate\Routing\Controller as BaseController; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; @@ -19,27 +20,20 @@ class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests, Api; - public function upload(Request $request, UploadService $service): array + public function upload(Request $request, UploadService $service): Response { try { /** @var User $user */ $user = Auth::user(); - $service->store($request, $user); + $image = $service->store($request, $user); } catch (UploadException $e) { return $this->error($e->getMessage()); } catch (\Throwable $e) { Log::error("Web 上传文件时发生异常,", ['message' => $e->getMessage(), 'trace' => $e->getTraceAsString()]); - return $this->error('上传失败,请稍后再试'); + return $this->error('服务异常,请稍后再试'); } - $data = [ - 'url' => 'https://pic.iqy.ink/2021/12/12/e8cfd03eb787f.png', - 'html' => '<img src="https://pic.iqy.ink/2021/12/12/e8cfd03eb787f.png" alt="e212bc43771ad6d391952732a1713e31.png" title="e212bc43771ad6d391952732a1713e31.png" />', - 'bbcode' => '[img]https://pic.iqy.ink/2021/12/12/e8cfd03eb787f.png[/img]', - 'markdown' => '![e212bc43771ad6d391952732a1713e31.png](https://pic.iqy.ink/2021/12/12/e8cfd03eb787f.png)', - 'markdown_with_link' => '[![e212bc43771ad6d391952732a1713e31.png](https://pic.iqy.ink/2021/12/12/e8cfd03eb787f.png)](https://pic.iqy.ink/2021/12/12/e8cfd03eb787f.png)', - ]; - $status = true; - $message = '上传成功'; - return compact('status', 'data', 'message'); + return $this->success('上传成功', $image->setAppends(['url', 'pathname', 'links'])->only( + 'id', 'url', 'pathname', 'origin_name', 'size', 'mimetype', 'md5', 'sha1', 'links' + )); } } diff --git a/app/Models/Album.php b/app/Models/Album.php index 1373a278..316229fd 100644 --- a/app/Models/Album.php +++ b/app/Models/Album.php @@ -2,6 +2,7 @@ namespace App\Models; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -14,7 +15,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; * @property string $intro * @property int $image_num * @property-read User $user - * @property-read Image[] $images + * @property-read Collection $images */ class Album extends Model { diff --git a/app/Models/Group.php b/app/Models/Group.php index fb17b013..e338384e 100644 --- a/app/Models/Group.php +++ b/app/Models/Group.php @@ -16,8 +16,8 @@ use Illuminate\Support\Collection; * @property Collection $configs * @property Carbon $updated_at * @property Carbon $created_at - * @property-read User[] $users - * @property-read Strategy[] $strategies + * @property-read \Illuminate\Database\Eloquent\Collection $users + * @property-read \Illuminate\Database\Eloquent\Collection $strategies */ class Group extends Model { diff --git a/app/Models/Image.php b/app/Models/Image.php index 524f884e..4efa6257 100644 --- a/app/Models/Image.php +++ b/app/Models/Image.php @@ -6,6 +6,8 @@ use Carbon\Carbon; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; +use Illuminate\Support\Collection; +use Illuminate\Support\Str; /** * @property int $id @@ -14,12 +16,15 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; * @property int $strategy_id * @property string $path * @property string $name + * @property string $pathname * @property string $origin_name * @property string $alias_name * @property float $size * @property string $mimetype * @property string $md5 * @property string $sha1 + * @property string $url + * @property Collection $links * @property int $permission * @property boolean $is_unhealthy * @property string $uploaded_ip @@ -34,9 +39,6 @@ class Image extends Model use HasFactory; protected $fillable = [ - 'user_id', - 'album_id', - 'strategy_id', 'path', 'name', 'origin_name', @@ -64,6 +66,33 @@ class Image extends Model 'is_unhealthy' => 'bool', ]; + protected $with = ['strategy']; + + public function getPathnameAttribute(): string + { + return "{$this->path}/{$this->name}"; + } + + public function getUrlAttribute(): string + { + if (! $this->strategy) { + return asset($this->pathname); + } + $domain = Str::replaceFirst('/', '', $this->strategy->configs->get('domain')); + return $domain.'/'.$this->pathname; + } + + public function getLinksAttribute(): Collection + { + return collect([ + 'url' => $this->url, + 'html' => "<img src=\"{$this->url}\" alt=\"{$this->origin_name}\" title=\"{$this->origin_name}\" />", + 'bbcode' => "[img]{$this->url}[/img]", + 'markdown' => "![{$this->origin_name}]({$this->url})", + 'markdown_with_link' => "[![{$this->origin_name}]({$this->url})]({$this->url})", + ]); + } + public function user(): BelongsTo { return $this->belongsTo(User::class, 'user_id', 'id'); diff --git a/app/Models/Strategy.php b/app/Models/Strategy.php index 2a63e4ad..aeb73b12 100644 --- a/app/Models/Strategy.php +++ b/app/Models/Strategy.php @@ -3,11 +3,11 @@ namespace App\Models; use Carbon\Carbon; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Support\Collection; /** * @property int $id @@ -15,11 +15,11 @@ use Illuminate\Support\Collection; * @property string $key * @property string $name * @property string $intro - * @property Collection $configs + * @property \Illuminate\Support\Collection $configs * @property Carbon $updated_at * @property Carbon $created_at * @property-read Group $group - * @property-read Image[] $images + * @property-read Collection $images */ class Strategy extends Model { diff --git a/app/Models/User.php b/app/Models/User.php index 7fa86127..2c705313 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -25,7 +25,7 @@ use Laravel\Sanctum\HasApiTokens; * @property int $image_num * @property int $album_num * @property string $registered_ip - * @property string $status + * @property int $status * @property Carbon $email_verified_at * @property Carbon $updated_at * @property Carbon $created_at @@ -93,7 +93,7 @@ class User extends Authenticatable return $this->belongsTo(Group::class, 'group_id', 'id'); } - public function album(): HasMany + public function albums(): HasMany { return $this->hasMany(Album::class, 'user_id', 'id'); } diff --git a/app/Service/UploadService.php b/app/Service/UploadService.php index b71b128a..936be990 100644 --- a/app/Service/UploadService.php +++ b/app/Service/UploadService.php @@ -4,10 +4,12 @@ namespace App\Service; use App\Enums\ConfigKey; use App\Enums\GroupConfigKey; +use App\Enums\ImagePermission; use App\Enums\Strategy\KodoOption; use App\Enums\Strategy\LocalOption; use App\Enums\StrategyKey; use App\Enums\UserConfigKey; +use App\Enums\UserStatus; use App\Exceptions\UploadException; use App\Models\Group; use App\Models\Image; @@ -15,8 +17,10 @@ use App\Models\Strategy; use App\Models\User; use App\Utils; use Illuminate\Contracts\Auth\Authenticatable; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Http\Request; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use League\Flysystem\Adapter\Local; use League\Flysystem\AdapterInterface; use League\Flysystem\Filesystem; @@ -27,10 +31,10 @@ class UploadService /** * @param Request $request * @param User|null $user - * @return void + * @return Image * @throws UploadException */ - public function store(Request $request, ?User $user = null)//: Image + public function store(Request $request, ?User $user = null): Image { $file = $request->file('file'); @@ -38,15 +42,20 @@ class UploadService throw new UploadException('管理员关闭了游客上传'); } + $image = new Image(); // 组配置 $configs = Group::getDefaultConfig(); // 默认使用本地储存策略 $disk = collect([ 'driver' => StrategyKey::Local, - 'configs' => [LocalOption::Root => config('filesystems.disks.local.root')], + 'configs' => collect([LocalOption::Root => config('filesystems.disks.local.root')]), ]); if (! is_null($user)) { + if ($user->status !== UserStatus::Normal) { + throw new UploadException('账号状态异常'); + } + // 如果该用户有角色组,覆盖默认组、上传策略配置 if ($user->group) { $configs = $user->group->configs; @@ -57,13 +66,23 @@ class UploadService $defaultStrategyId = $user->configs->get(UserConfigKey::DefaultStrategy, 0); /** @var Strategy $strategy $disk */ $strategy = $strategies->find($defaultStrategyId, $strategies->first()); - $disk = collect(['driver' => $strategy->key])->merge(['configs' => $strategy->configs]); + $disk = collect(['driver' => $strategy->key, 'configs' => $strategy->configs]); + $image->strategy_id = $strategy->id; } } if ($file->getSize() / 1024 + $user->images()->sum('size') > $user->capacity) { throw new UploadException('储存空间不足'); } + + $image->user_id = $user->id; + + // 图片保存至默认相册(若有) + if ($albumId = $user->configs->get(UserConfigKey::DefaultAlbum)) { + if ($user->albums()->where('id', $albumId)->exists()) { + $image->album_id = $albumId; + } + } } if (! in_array($file->extension(), $configs->get(GroupConfigKey::AcceptedFileSuffixes))) { @@ -74,19 +93,48 @@ class UploadService throw new UploadException("图片大小超出限制"); } - // TODO 是否超出组限制 - $pathname = $this->replacePathname( $configs->get(GroupConfigKey::PathNamingRule).'/'.$configs->get(GroupConfigKey::FileNamingRule) ).".{$file->extension()}"; + $image->fill([ + 'md5' => md5_file($file->getRealPath()), + 'sha1' => sha1_file($file->getRealPath()), + 'path' => dirname($pathname), + 'name' => basename($pathname), + 'origin_name' => $file->getClientOriginalName(), + 'size' => $file->getSize() / 1024, + 'mimetype' => $file->getMimeType(), + 'permission' => ImagePermission::Private, + 'is_unhealthy' => false, // TODO 接入鉴黄? + 'uploaded_ip' => $request->ip(), + ]); + $filesystem = new Filesystem($this->getAdapter($disk->get('driver'), $disk->get('configs'))); - if (! $filesystem->putStream($pathname, fopen($file, 'r'))) { - throw new UploadException('上传失败'); + // 检测该策略是否存在该图片,有则只创建记录不保存文件 + /** @var Image $existing */ + $existing = Image::query()->when($image->strategy_id, function (Builder $builder, $id) { + $builder->where('strategy_id', $id); + })->where('md5', $image->md5)->where('sha1', $image->sha1)->first(); + if (is_null($existing)) { + $handle = fopen($file, 'r'); + if (! $filesystem->putStream($pathname, $handle) || ! fclose($handle)) { + throw new UploadException('图片上传失败'); + } + } else { + $image->fill($existing->only('path', 'name')); } - // TODO 检测是否存在该图片,有则只创建记录不保存文件 - // TODO 图片保存至默认相册(若有) + DB::transaction(function () use ($image, $user, $filesystem, $existing) { + if (! $image->save()) { + // 删除文件 + if (is_null($existing)) $filesystem->delete($image->pathname); + throw new UploadException('图片保存失败'); + } + $user->increment('image_num'); + }, 3); + + return $image; } protected function getAdapter(int $disk, Collection $configs): AdapterInterface diff --git a/resources/views/components/upload.blade.php b/resources/views/components/upload.blade.php index 5d9f71ef..a1692218 100644 --- a/resources/views/components/upload.blade.php +++ b/resources/views/components/upload.blade.php @@ -71,7 +71,7 @@