這是這系列最長的一篇,也是把前面 28 天知識串起來的實戰演練:先幫 blog-app 加一個能回答讀者問題的 LINE 聊天機器人(串接 OpenAI),接著走一次 Git 版控流程,最後真正把專案部署上線。三件事合起來剛好是「開發 → 版控 → 上線」的完整故事線。
到 LINE Developers 建立一個 Provider 跟 Messaging API Channel,取得兩個關鍵憑證:
LINE_BOT_CHANNEL_ACCESS_TOKEN=xxx
LINE_BOT_CHANNEL_SECRET=xxx

安裝官方 SDK:
composer require linecorp/line-bot-sdk
// routes/api.php
Route::post('/line/webhook', LineWebhookController::class);
// app/Http/Controllers/LineWebhookController.php
use LINE\Clients\MessagingApi\Api\MessagingApiApi;
use LINE\Clients\MessagingApi\Configuration;
use LINE\Parser\EventRequestParser;
use LINE\Webhook\Model\MessageEvent;
use LINE\Webhook\Model\TextMessageContent;
class LineWebhookController extends Controller
{
public function __construct(
protected ChatbotService $chatbot,
) {}
public function __invoke(Request $request)
{
$parsedEvents = EventRequestParser::parseEventRequest(
$request->getContent(),
config('services.line.channel_secret'),
$request->header('x-line-signature'),
);
foreach ($parsedEvents->getEvents() as $event) {
if ($event instanceof MessageEvent && $event->getMessage() instanceof TextMessageContent) {
ReplyToLineMessage::dispatch(
replyToken: $event->getReplyToken(),
userMessage: $event->getMessage()->getText(),
);
}
}
return response()->noContent();
}
}
EventRequestParser::parseEventRequest() 會用 Channel Secret 驗證這個請求真的來自 LINE(防止偽造請求),驗簽失敗會直接拋出例外,這正好呼應 Day 16 學過的請求偽造防護精神,只是這裡是應用層級的簽章驗證,不是框架內建的 CSRF 機制。
注意上面的程式碼把實際回覆邏輯派送成一個 Job(ReplyToLineMessage::dispatch()),而不是像早期草稿那樣直接同步呼叫 $this->chatbot->reply()。這是刻意的設計,理由值得說清楚:LINE 平台對 Webhook 的回應時間有嚴格限制(通常是幾秒鐘內),如果 Controller 直接同步呼叫 OpenAI API(Day 26 的 HTTP Client,網路延遲加上模型生成時間,經常需要幾秒到十幾秒),很可能在 OpenAI 回應之前就已經超過 LINE 的逾時限制,導致 LINE 判定這次 Webhook 呼叫失敗、觸發重試機制——重試又會再次觸發同一段耗時邏輯,容易演變成同一則訊息被處理好幾次。
把實際處理邏輯丟進 Day 19 學過的 Queue,Controller 只需要「確認收到、快速回應 LINE」跟「把工作排進佇列」兩件事,符合 Day 19 那句「不需要讓使用者等待的工作」精神——只是這裡「使用者」換成了「LINE 平台的 Webhook 呼叫方」:
// app/Jobs/ReplyToLineMessage.php
class ReplyToLineMessage implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(
public string $replyToken,
public string $userMessage,
) {}
public function handle(ChatbotService $chatbot): void
{
$chatbot->reply($this->replyToken, $this->userMessage);
}
}
handle() 方法直接型別提示 ChatbotService $chatbot——這是 Day 19 提過的、Job 的 handle() 方法本身也享有 Service Container 的方法注入能力,不需要在建構子裡塞進這個依賴。

composer require openai-php/laravel
php artisan openai:install
OPENAI_API_KEY=xxx
// app/Services/ChatbotService.php
use OpenAI\Laravel\Facades\OpenAI;
use LINE\Clients\MessagingApi\Api\MessagingApiApi;
use LINE\Clients\MessagingApi\Model\ReplyMessageRequest;
use LINE\Clients\MessagingApi\Model\TextMessage;
class ChatbotService
{
public function __construct(
protected MessagingApiApi $lineApi,
) {}
public function reply(string $replyToken, string $userMessage): void
{
try {
$result = OpenAI::chat()->create([
'model' => 'gpt-4o-mini',
'messages' => [
['role' => 'system', 'content' => '你是 blog-app 部落格的客服機器人,用繁體中文簡短回答讀者問題。'],
['role' => 'user', 'content' => $userMessage],
],
]);
$replyText = $result->choices[0]->message->content;
} catch (\Throwable $e) {
Log::error('OpenAI 呼叫失敗', ['error' => $e->getMessage()]);
$replyText = '抱歉,我現在有點忙不過來,請稍後再試一次。';
}
$this->lineApi->replyMessage(new ReplyMessageRequest([
'replyToken' => $replyToken,
'messages' => [new TextMessage(['text' => $replyText])],
]));
}
}
ChatbotService 依賴 MessagingApiApi(LINE 回覆 API 的客戶端),實際綁定可以在 AppServiceProvider 裡用 Day 23 學過的方式組裝:
$this->app->singleton(MessagingApiApi::class, function () {
return new MessagingApiApi(
config: (new Configuration())->setAccessToken(config('services.line.channel_access_token')),
);
});
try/catch 這段是刻意補上的——呼應 Day 26 提過的「不能假設外部 API 永遠成功」,OpenAI 服務偶爾會逾時或回傳錯誤,如果沒有妥善處理,使用者會完全收不到任何回應(而不是至少看到一句「抱歉現在有點忙」),這種「優雅降級」的處理方式在串接任何外部 AI 服務時都值得養成習慣。
這整段串接,本質上是 Day 26 HTTP Client 概念的延伸(openai-php/laravel、linecorp/line-bot-sdk 底層都是包裝好的 HTTP 呼叫)、Day 19 Queue 背景處理、與 Day 23 依賴注入實戰的組合應用——原理都是前面天數教過的,今天只是把它們組合成一個真正能用的功能。
blog-app 開發到這裡,如果還沒建立版本控制,先確認 .gitignore 排除該排除的內容(vendor/、node_modules/、.env——.env 含機密資訊絕對不能進版控,Day 6 提過的加密機制是給真的需要分享 .env 內容時的另一條路,日常開發直接排除在外最單純):
git init
git add .
git commit -m "Initial commit: blog-app with LINE bot integration"
到 GitHub 建立一個新的 repository,推送上去:
git remote add origin https://github.com/your-username/blog-app.git
git branch -M main
git push -u origin main
實務上不會直接在 main 上開發,常見的 Git Flow 精神:
git checkout -b develop
git checkout -b feature/line-chatbot develop
# 開發完成,提交變更
git add .
git commit -m "feat: 新增 LINE 聊天機器人串接 OpenAI"
git push -u origin feature/line-chatbot
接著到 GitHub 開一個 Pull Request,從 feature/line-chatbot 合併回 develop,讓團隊成員 Code Review 後再合併,最後 develop 穩定後才合併進 main 準備部署——這套流程的核心精神是「main 永遠是可以部署的穩定狀態」,功能開發都在獨立分支進行,不直接衝擊主線。

上面的 commit message 用了 feat: 前綴,這是社群廣泛採用的 Conventional Commits 慣例,幾個常見前綴:
| 前綴 | 用途 |
|---|---|
feat: |
新功能 |
fix: |
修正錯誤 |
refactor: |
重構,不改變外部行為 |
test: |
新增或調整測試 |
docs: |
文件調整 |
chore: |
建置流程、套件更新等雜項 |
這套慣例的實際價值不只是「看起來整齊」——如果團隊一致遵守,配合工具(例如自動產生 CHANGELOG 的套件)能直接從 commit 歷史自動整理出版本更新紀錄,不需要每次發版都手動回想「這個版本到底改了什麼」。Day 27 提過的 CI 流程,也常會設定規則檢查 commit message 是否符合這套格式,確保長期累積下來的版控歷史保持一致、可讀。
Render 是一個對 Laravel 專案友善的雲端平台,用 Docker 容器部署。
FROM richarvey/nginx-php-fpm:latest
COPY . .
ENV SKIP_COMPOSER=1
ENV WEBROOT=/var/www/html/public
ENV PHP_ERRORS_STDERR=1
ENV RUN_SCRIPTS=1
ENV REAL_IP_HEADER=1
RUN composer install --no-dev --optimize-autoloader
CMD ["/start.sh"]
這裡有個容易踩雷的細節:網路上的 Laravel + Render 教學(包含我自己以前寫的)常會直接給一個綁死的映像檔標籤,例如 richarvey/nginx-php-fpm:2.0.0 或 3.1.6。這種版本號會過期——部署 blog-app(Laravel 13、PHP 8.3+)時,請直接查 Docker Hub 上的最新標籤,確認內建 PHP 版本滿足 8.3+,不要照抄。如果找不到明確對應的標籤,也可以改用 Laravel 官方維護的 Sail production 映像檔當基礎。
deploy 分支(或直接用 main,視你的部署策略而定)。APP_KEY 用 php artisan key:generate --show 產生後貼上、DB_* 系列、LINE_BOT_*、OPENAI_API_KEY 等)。git push 到指定分支就會自動觸發重新部署。
正式環境每次部署新版本時,如果有新的 Migration(Day 9 一路建立的資料表結構變更),需要在新版本程式碼上線的同時執行 php artisan migrate。Render 的 Web Service 設定裡可以指定一個「Release Command」,在每次部署、應用程式真正對外服務之前執行:
php artisan migrate --force
--force 是必要的——migrate 指令在正式環境(APP_DEBUG=false)預設會詢問確認,避免有人不小心在正式環境誤跑 migration;--force 明確表示「我知道這是正式環境,請直接執行」,這是自動化部署流程裡標準的做法。同時記得部署流程也該包含 Day 6/7/12 提過的快取指令:
php artisan config:cache
php artisan route:cache
php artisan view:cache
這幾個指令在正式環境上線流程裡幾乎是標準配備,能明顯提升回應速度,Day 6/7/12 各自介紹過它們個別的效果,部署時建議一次全部執行。
Day 19 提過 Queue Worker 是一個獨立的常駐行程,Render 這類平台通常需要額外建立一個「Background Worker」服務(區別於處理 HTTP 請求的 Web Service),執行:
php artisan queue:work --tries=3
這是很容易在第一次部署時漏掉的一步——如果只部署了 Web Service,blog-app 的 HTTP 請求能正常運作,但 Day 19 派送的 Job(包括今天的 LINE 回覆邏輯)會一直堆在佇列裡,永遠不會被實際執行,因為根本沒有 Worker 行程在監聽處理。

// app/Providers/AppServiceProvider.php
public function boot(): void
{
if ($this->app->environment('production')) {
URL::forceScheme('https');
}
}
部署完成不代表工作結束,這裡提供一份簡短的上線後檢查清單,串起這系列學過的工具:
/up),確認應用程式基本存活。php artisan schedule:run)。.env 不小心被 git add . 加進版控:務必在第一次 commit 之前就確認 .gitignore 正確排除 .env,如果已經不小心提交過,光是之後補加 .gitignore 沒用,還需要用 git rm --cached .env 把它從版控歷史移除(更徹底的做法需要改寫歷史,這裡不展開,重點是養成一開始就排除的習慣)。APP_KEY:沒有這個值,應用程式會直接啟動失敗(加密機制需要這把金鑰),部署前務必確認核心環境變數都已經正確設定。channel_secret 跟 channel_access_token 是兩個不同的憑證,驗簽用的是 channel_secret,如果搞混會導致所有 Webhook 請求都被判定為驗簽失敗。今天把 blog-app 從「一個能在本機跑的專案」推進到「一個真正上線、能被外部使用者互動的服務」:串接 LINE Bot + OpenAI 打造聊天機器人功能(並學到透過 Queue 背景處理避免 Webhook 逾時的實務考量)、用 Git Flow 走一次團隊協作的版控流程、部署到 Render 並確保 HTTPS、Migration 自動執行、Worker 部署,最後補上一份上線後的檢查清單。部署映像檔的版本號,是這篇少數需要你根據部署當下的實際情況自行查證的地方。
千里之堤,潰於蟻穴 — 《韓非子・喻老》
Day 30,這個系列的最後一天:回顧整趟 30 天的旅程,並展望 Laravel 13 裡幾個很值得關注的新能力。