Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124

實作網址: https://twingo.polinwei.com/pages/ShortUrl

Sqids 是一個開源函式庫,可以從數字生成短的唯一識別碼。這些識別碼是 URL 安全的,可以編碼多個數字,並且不包含常見的髒話。
Sqids 的主要用途是單純形象化的。如果您想在應用中使用識別碼 ID代替數字,Sqids 可能是一個不錯的選擇。
composer require sqids/sqids<?php
return [
'alphabet' => env(
'SQIDS_ALPHABET',
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
),
'min_length' => env('SQIDS_MIN_LENGTH', 6),
];這一步很重要:避免未來再換編碼器時大改程式
<?php
namespace App\Services;
use Sqids\Sqids;
class SqidsService
{
protected Sqids $sqids;
public function __construct()
{
$this->sqids = new Sqids(
config('sqids.alphabet'),
config('sqids.min_length')
);
}
public function encode(int $id): string
{
return $this->sqids->encode([$id]);
}
public function decode(string $code): ?int
{
$numbers = $this->sqids->decode($code);
return $numbers[0] ?? null;
}
}不存 code、只存 id
use App\Services\SqidsService;
class ShortUrl extends Model
{
protected $fillable = [
'original_url',
'domain',
'expired_at',
];
protected $appends = [
'code',
'short_url',
];
public function getCodeAttribute(): string
{
return app(SqidsService::class)->encode($this->id);
}
public function getShortUrlAttribute(): string
{
$domain = $this->domain ?? config('app.url');
return rtrim($domain, '/') . '/' . $this->code;
}
}use App\Models\ShortUrl;
use App\Services\SqidsService;
use Illuminate\Support\Facades\Cache;
class RedirectController extends Controller
{
public function __invoke(
string $code,
SqidsService $sqids
) {
$id = $sqids->decode($code);
abort_if(!$id, 404);
$short = Cache::remember(
"short_url:{$id}",
3600,
fn () => ShortUrl::findOrFail($id)
);
if ($short->expired_at && now()->gt($short->expired_at)) {
abort(410);
}
Cache::increment("short_url:{$id}:clicks");
return redirect()->away($short->original_url);
}
}首先,需要建立處理邏輯的 Job。在終端機執行:建立 Job: SyncShortUrlClicksJob
php artisan make:job SyncShortUrlClicksJob這會在 app/Jobs/SyncShortUrlClicks.php 產生檔案。請在 handle 方法中撰寫程式邏輯,程式碼內容如下:
<?php
namespace App\Jobs;
use App\Models\Util\ShortUrl;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
// 同步短網址點擊次數的工作類別
class SyncShortUrlClicksJob implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
// 分批處理短網址,將快取中的點擊次數同步到資料庫
ShortUrl::chunk(100, function ($shortUrls) {
foreach ($shortUrls as $shortUrl) {
$cacheKey = "short_url:{$shortUrl->id}:clicks";
$cachedClicks = cache()->get($cacheKey, 0);
if ($cachedClicks > 0) {
$shortUrl->increment('clicks', $cachedClicks);
cache()->forget($cacheKey);
}
}
});
}
}在 Laravel 11 與 12 中,所有的排程任務都定義在 routes/console.php 檔案中,不再像舊版本需要去修改 app/Console/Kernel.php。打開 routes/console.php,加入程式碼:
use App\Jobs\SyncShortUrlClicksJob;
use Illuminate\Support\Facades\Schedule;
// 每分鐘執行一次 SyncShortUrlClicks Job
Schedule::job(new SyncShortUrlClicksJob)->everyMinute();或是
use App\Jobs\SyncShortUrlClicksJob;
use Illuminate\Support\Facades\Schedule;
use Illuminate\Support\Facades\Artisan;
// 1. 定義 Artisan 指令
Artisan::command('shorturl:sync-clicks', function () {
// 這裡可以直接派發 Job
SyncShortUrlClicksJob::dispatch();
$this->info('點擊數據同步 Job 已派發!');
})->purpose('同步短網址的點擊次數');
// 2. 設定排程:使用指令名稱呼叫
Schedule::command('shorturl:sync-clicks')->everyMinute(); 如果你希望平常也能手動輸入指令來同步數據(例如:php artisan shorturl:sync-clicks),那麼使用 方法一 是最佳選擇。它讓你的開發與維運(Debug)變得非常容易。
在正式環境中,需要設定伺服器的 Cron Job。但在開發階段,可以使用以下指令手動觸發排程器,觀察它是否正確執行:
php artisan schedule:work執行後,排程器會每分鐘自動呼叫一次該 Job。
為了讓排程在伺服器上自動運作,你需要在伺服器的 Crontab 中加入這一行:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1如果希望排程更具彈性,可以參考下表調整方法:
| 方法 | 說明 |
->everyFiveMinutes() | 每 5 分鐘執行一次 |
->hourly() | 每小時執行一次 |
->onOneServer() | 如果有多台伺服器,僅在其中一台執行(避免重複) |
->withoutOverlapping() | 確保前一個任務完成前,不會啟動下一個相同的任務 |
可以在
RedirectController同時支援:
- 舊的:
ShortUrl::where('code', $code)- 新的:
Sqids(由 id encode / decode)
這種做法通常用在:
新短網址 → Sqids
舊短網址 → code 欄位
條件假設
code 欄位id + SqidsSqidsService 或 traituse App\Http\Controllers\Controller;
use App\Models\Util\ShortUrl;
use App\Services\SqidsService;
use Illuminate\Support\Facades\Cache;
class ShortUrlRedirectController extends Controller
{
// 短網址重定向, 將物件 controller 當作為 function 使用
public function __invoke(string $code, SqidsService $sqids)
{
$shortUrl = null;
/**
* ① 先嘗試用 Sqids 解碼(新制)
*/
if ($id = $sqids->decode($code)) {
$shortUrl = Cache::remember(
"short_url:id:{$id}",
3600,
fn () => ShortUrl::find($id)
);
}
/**
* ② Sqids 失敗 → fallback 舊 code
*/
if (!$shortUrl) {
$shortUrl = Cache::remember(
"short_url:code:{$code}",
3600,
fn () => ShortUrl::where('code', $code)->first()
);
}
abort_if(!$shortUrl, 404);
/**
* ③ 過期檢查
*/
if ($shortUrl->expired_at && now()->gt($shortUrl->expired_at)) {
abort(410);
}
/**
* ④ Redis 點擊統計(統一 key)
*/
Cache::increment("short_url:{$shortUrl->id}:clicks");
return redirect()->away($shortUrl->original_url);
}
}firstOrFail()?ShortUrl::where('code', $code)->firstOrFail(); ❌因為:
abort 應該只做一次可以改成 trait 嗎?可以,完全沒問題。
但要注意一件事:
Sqids 需要設定(alphabet / min_length)
trait 沒有 constructor 注入,因此要自己處理初始化方式。
php artisan make:trait HasSqids.php產生 app/Traits/HasSqids.php 後, 撰寫程式內容如下
<?php
namespace App\Traits;
use Sqids\Sqids;
trait HasSqids
{
protected function sqids(): Sqids
{
static $sqids = null;
if ($sqids === null) {
$sqids = new Sqids(
config('sqids.alphabet'),
config('sqids.min_length')
);
}
return $sqids;
}
protected function encodeSqid(int $id): string
{
return $this->sqids()->encode([$id]);
}
protected function decodeSqid(string $code): ?int
{
$numbers = $this->sqids()->decode($code);
return $numbers[0] ?? null;
}
}App\Models\ShortUrl.php
use App\Traits\HasSqids;
class ShortUrl extends Model
{
use HasSqids;
protected $fillable = [
'original_url',
'domain',
'expired_at',
];
protected $appends = ['code', 'short_url'];
public function getCodeAttribute(): string
{
return $this->encodeSqid($this->id);
}
public function getShortUrlAttribute(): string
{
$domain = $this->domain ?? config('app.url');
return rtrim($domain, '/') . '/' . $this->code;
}
}RedirectController.php
use App\Traits\HasSqids;
use Illuminate\Support\Facades\Cache;
class RedirectController extends Controller
{
use HasSqids;
public function __invoke(string $code)
{
$id = $this->decodeSqid($code);
abort_if(!$id, 404);
$short = Cache::remember(
"short_url:{$id}",
3600,
fn () => ShortUrl::findOrFail($id)
);
if ($short->expired_at && now()->gt($short->expired_at)) {
abort(410);
}
Cache::increment("short_url:{$id}:clicks");
return redirect()->away($short->original_url);
}
}❌ 在 trait 裡用 constructor
public function __construct() ❌❌ 每次 encode 都 new Sqids
new Sqids(...) ❌❌ 把 trait 當成 DI
$this->sqidsService ❌以上就是過程,有需要討論再留言囉!!