liutk 2026-02-22 11:58:01 +08:00
parent 9de912f7e5
commit 1c228d21e1
19 changed files with 403 additions and 6 deletions

View File

@ -40,6 +40,7 @@ class FriendLinkController extends AdminController
amis()->TableColumn('link', __('admin.friend_links.link')),
amis()->TableColumn('cover_url', __('admin.friend_links.cover'))->type('image')->height('50px')->width('250px')->enlargeAble(true),
// amis()->TableColumn('description', __('admin.project_cates.description')),
amis()->TableColumn('is_enable', __('admin.friend_links.is_enable'))->type('switch'),
amis()->TableColumn('created_at', __('admin.created_at'))->set('type', 'datetime')->sortable()->width('150px'),
amis()->Operation()->label(__('admin.actions'))->buttons([
$this->rowEditTypeButton('drawer', 'md'),
@ -60,6 +61,7 @@ class FriendLinkController extends AdminController
Components::make()->cropImageControl('cover', __('admin.friend_links.cover'))->required(true),
amis()->TextareaControl('description', __('admin.friend_links.description')),
Components::make()->sortControl('sort', __('admin.friend_links.sort')),
amis()->SwitchControl('is_enable', __('admin.friend_links.is_enable'))->value(false),
])
]),
]);

View File

@ -39,6 +39,7 @@ class ProjectCateController extends AdminController
amis()->TableColumn('title', __('admin.project_cates.title')),
amis()->TableColumn('cover_url', __('admin.project_cates.cover'))->type('image')->height('50px')->width('250px')->enlargeAble(true),
// amis()->TableColumn('description', __('admin.project_cates.description')),
amis()->TableColumn('is_enable', __('admin.project_cates.is_enable'))->type('switch'),
amis()->TableColumn('created_at', __('admin.created_at'))->set('type', 'datetime')->sortable()->width('150px'),
amis()->Operation()->label(__('admin.actions'))->buttons([
$this->rowEditTypeButton('drawer', 'md'),
@ -58,6 +59,7 @@ class ProjectCateController extends AdminController
Components::make()->cropImageControl('cover', __('admin.project_cates.cover'), 1.68)->required(true),
amis()->TextareaControl('description', __('admin.project_cates.description')),
Components::make()->sortControl('sort', __('admin.project_cates.sort')),
amis()->SwitchControl('is_enable', __('admin.project_cates.is_enable'))->value(false),
])
]),
]);

View File

@ -0,0 +1,42 @@
<?php
namespace App\Exceptions;
use Exception;
class BizException extends Exception
{
public function __construct(string $message, int $code = 400)
{
parent::__construct($message, $code);
}
/**
* 用于响应的 HTTP 状态代码
*
* @var int
*/
public $status = 200;
/**
* 设置用于响应的 HTTP 状态代码
*
* @param int $status
* @return $this
*/
public function status(int $status)
{
$this->status = $status;
return $this;
}
/**
* 报告异常
*
* @return mixed
*/
public function report()
{
}
}

View File

@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\CaptchaService;
use Gregwar\Captcha\CaptchaBuilder;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class CaptchaController extends Controller
{
/**
* 创建图形验证码
*/
public function store(Request $request, CaptchaService $captchaService)
{
$request->validate([
'key' => ['bail', 'filled', 'string', 'max:32'],
'w' => ['bail', 'int'],
'h' => ['bail', 'int'],
]);
$builder = $this->builder();
$builder->build($request->input('w', 150), $request->input('h', 40));
$captchaService->put(
$key = $request->input('key', Str::random(16)),
$builder->getPhrase()
);
return response()->json([
'key' => $key,
'img' => $builder->inline(),
]);
}
/**
* 查看图形验证码
*
* @param string $key
* @param \Illuminate\Http\Request $request
* @param \App\Services\CaptchaService $captchaService
* @return \Illuminate\Http\Response
*/
public function show($key, Request $request, CaptchaService $captchaService)
{
$builder = $this->builder();
$builder->build(
(int) $request->query('w', 150),
(int) $request->query('h', 40),
);
if (strlen($key) <= 32) {
$captchaService->put($key, $builder->getPhrase());
}
return response($builder->get())
->header('Content-Type', 'image/jpeg')
->header('Cache-Control', 'no-cache');
}
/**
* 图形验证码生成器
*
* @return \Gregwar\Captcha\CaptchaBuilder
*/
protected function builder(): CaptchaBuilder
{
return new CaptchaBuilder(Str::random(5));
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Traits\PaginatorTrait;
use App\Models\Filters\ProjectArticleFilter;
use App\Http\Resources\ProjectChildrenResource;
use App\Models\ProjectArticle;
class ProjectChildrenController extends Controller
{
use PaginatorTrait;
public function index(Request $request){
$query = ProjectArticle::filter($request->all(), ProjectArticleFilter::class)->sort();
$list = $query->childrens()->show()->sort()->paginate($this->resolvePerPage('per_page', 20));
return $this->json(ProjectChildrenResource::collection($list));
}
public function show(ProjectArticle $children){
request()->merge(['include_content' => true]);
return $this->json(ProjectChildrenResource::make($children));
}
}

View File

@ -0,0 +1,23 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use App\Traits\PaginatorTrait;
use App\Models\ProjectCate;
use App\Models\Filters\ProjectCateFilter;
use App\Http\Resources\ProjectResource;
use App\Models\ProjectArticle;
class ProjectController extends Controller
{
use PaginatorTrait;
public function index(Request $request){
$query = ProjectCate::filter($request->all(), ProjectCateFilter::class)->sort();
$list = $query->show()->sort()->paginate($this->resolvePerPage('per_page', 20));
return $this->json(ProjectResource::collection($list));
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProjectChildrenResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'cover' => $this->cover,
'description' => $this->description,
'content' => $this->when($request->boolean('include_content'), $this->content),
];
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProjectResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array|\Illuminate\Contracts\Support\Arrayable|\JsonSerializable
*/
public function toArray($request)
{
return [
'id' => $this->id,
'title' => $this->title,
'cover' => $this->cover,
];
}
}

View File

@ -32,4 +32,14 @@ class FriendLink extends Model
get: fn($value) => $this->cover ? (Str::startsWith($this->cover, ['http://', 'https://']) ? $this->cover : Storage::url($this->cover)) : null,
);
}
public function scopeShow($q){
$q->where('is_enable', true);
}
public function scopeSort($q)
{
$q->orderBy('sort', 'asc')
->orderBy('created_at', 'desc');
}
}

View File

@ -73,6 +73,11 @@ class ProjectArticle extends Model
->orderBy('created_at', 'desc');
}
public function scopeChildrens($q)
{
$q->where('type', self::TYPE_ARTICLE);
}
protected function tags():Attribute
{
return Attribute::make(

View File

@ -32,4 +32,14 @@ class ProjectCate extends Model
get: fn($value) => $this->cover ? (Str::startsWith($this->cover, ['http://', 'https://']) ? $this->cover : Storage::url($this->cover)) : null,
);
}
public function scopeShow($q){
$q->where('is_enable', true);
}
public function scopeSort($q)
{
$q->orderBy('sort', 'asc')
->orderBy('created_at', 'desc');
}
}

View File

@ -0,0 +1,82 @@
<?php
namespace App\Services;
use App\Exceptions\BizException;
use Illuminate\Contracts\Cache\Repository as Cache;
class CaptchaService
{
/**
* @var string
*/
protected $cachePrefix = 'captchas_';
/**
* @param \Illuminate\Contracts\Cache\Repository $cache
*/
public function __construct(
protected Cache $cache,
) {
}
/**
* 将验证码存入缓存中
*
* @param string $key
* @param string $phrase
* @param integer $ttl
* @return void
*/
public function put(string $key, string $phrase, int $ttl = 300): void
{
$this->cache->put($this->cacheKey($key), $phrase, $ttl);
}
/**
* 校验验证码是否正确
*
* @param string $key
* @param string $phrase
* @return bool
*/
public function testPhrase(string $key, string $phrase): bool
{
if ($phrase === '') {
return false;
}
$value = (string) $this->cache->pull(
$this->cacheKey($key)
);
return strtolower($value) === strtolower($phrase);
}
/**
* 校验验证码是否正确
*
* @param string $key
* @param string $phrase
* @return void
*
* @throws \App\Exceptions\BizException
*/
public function validatePhrase(string $key, string $phrase): void
{
if (! $this->testPhrase($key, $phrase)) {
throw new BizException(__('Invalid captcha'));
}
}
/**
* 生成验证码缓存的 key
*
* @param string $key
* @return string
*/
protected function cacheKey(string $key): string
{
return $this->cachePrefix.$key;
}
}

View File

@ -96,7 +96,11 @@ trait UploadTrait
switch($file['state']){
case 'init':
if(strpos($file['value'], 'temporary') !== false){
$filePath = $path.'/'.($file['name'] ?? $file['id']);
if(!isset($file['name'])){
$filePathInfo = pathinfo($file['value']);
$file['name'] = $filePathInfo['basename'];
}
$filePath = $path.'/'.$file['name'];
Storage::disk(Admin::config('admin.upload.disk'))->move($file['value'], $filePath);
$fileArr[] = Storage::disk(Admin::config('admin.upload.disk'))->url($filePath);
}else{

View File

@ -7,6 +7,7 @@
"require": {
"php": "^8.1",
"alphasnow/aliyun-oss-laravel": "^4.7",
"gregwar/captcha": "^1.3",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.3",

62
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6ce90738fadc40a447dfead6b131ec62",
"content-hash": "097769e6f2cd7e3bab775e47a9cce358",
"packages": [
{
"name": "aliyuncs/oss-sdk-php",
@ -599,6 +599,64 @@
],
"time": "2023-11-12T22:16:48+00:00"
},
{
"name": "gregwar/captcha",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/Gregwar/Captcha.git",
"reference": "4edbcd09fde4353b94ce550f43460eba73baf2cc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Gregwar/Captcha/zipball/4edbcd09fde4353b94ce550f43460eba73baf2cc",
"reference": "4edbcd09fde4353b94ce550f43460eba73baf2cc",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-mbstring": "*",
"php": ">=5.3.0",
"symfony/finder": "*"
},
"require-dev": {
"phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6.4 || ^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Gregwar\\": "src/Gregwar"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Grégoire Passault",
"email": "g.passault@gmail.com",
"homepage": "http://www.gregwar.com/"
},
{
"name": "Jeremy Livingston",
"email": "jeremy.j.livingston@gmail.com"
}
],
"description": "Captcha generator",
"homepage": "https://github.com/Gregwar/Captcha",
"keywords": [
"bot",
"captcha",
"spam"
],
"support": {
"issues": "https://github.com/Gregwar/Captcha/issues",
"source": "https://github.com/Gregwar/Captcha/tree/v1.3.0"
},
"time": "2025-06-23T12:25:54+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.8.1",
@ -6575,5 +6633,5 @@
"php": "^8.1"
},
"platform-dev": [],
"plugin-api-version": "2.3.0"
"plugin-api-version": "2.6.0"
}

View File

@ -18,6 +18,7 @@ return new class extends Migration
$table->string('cover')->nullable()->comment('封面');
$table->unsignedInteger('sort')->default(0)->comment('排序');
$table->text('content')->nullable()->comment('内容');
$table->unsignedTinyInteger('is_enable')->default(1)->comment('显示开关');
$table->timestamps();
});
}

View File

@ -17,7 +17,8 @@ return new class extends Migration
$table->text('description')->nullable()->comment('简介');
$table->string('cover')->nullable()->comment('封面');
$table->unsignedInteger('sort')->default(0)->comment('排序');
$table->string('link')->nullable()->comment('链接地址');
$table->string('link')->nullable()->comment('链接地址');
$table->unsignedTinyInteger('is_enable')->default(1)->comment('显示开关');
$table->timestamps();
});
}

View File

@ -324,7 +324,8 @@ return [
'description'=> '简介',
'content' => '内容',
'cover' =>'封面',
'sort' => '排序'
'sort' => '排序',
'is_enable' => '显示',
],
'project_articles' => [
'id' => '主键ID',
@ -350,6 +351,7 @@ return [
'content' => '内容',
'cover' =>'封面',
'sort' => '排序',
'is_enable' => '显示',
'link'=>'链接地址'
]
];

View File

@ -1,5 +1,7 @@
<?php
namespace App\Http\Controllers\Api;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
@ -19,5 +21,12 @@ Route::middleware('auth:sanctum')->get('/user', function (Request $request) {
});
Route::middleware('api')->group(function () {
Route::get('/ads', [App\Http\Controllers\Api\AdController::class, 'index']);
Route::post('captchas', [CaptchaController::class, 'store']);
Route::get('captchas/{captcha}', [CaptchaController::class, 'show']);
Route::get('/ads', [AdController::class, 'index']);
Route::get('/project_cates', [ProjectController::class, 'index']);
Route::get('/project_childrens', [ProjectChildrenController::class, 'index']);
Route::get('/project_childrens/{children}', [ProjectChildrenController::class, 'show']);
});