1. 事前準備
準備材料
| 服務 | 用途 | 費用 |
|---|---|---|
| GitHub | 存放程式碼 | 免費 |
| 自己的 Linux 伺服器 | 部署網站 | 月付 |
| Cloudflare | 網域 + HTTPS 隧道 | 免費 |
| LINE Developers | LINE Bot | 免費 |
| PayPal Developer | 金流 | 免費(正式收款需商業帳號) |
| AI模型 | AI 配對 + 計畫書生成 | 充值制 |
| Google Search Console | SEO | 免費 |
軟體
# Linux (Ubuntu/Debian)
sudo apt update && sudo apt install -y curl git nginx postgresql postgresql-client
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v # 確認 >= 18
npm -v # 確認 >= 9
2. 環境建置
安裝 PostgreSQL
sudo systemctl start postgresql
sudo systemctl enable postgresql
# 建立資料庫與使用者
sudo -u postgres psql
CREATE USER subsidy_user WITH PASSWORD '你的密碼';
CREATE DATABASE subsidy_hub OWNER subsidy_user;
初始化 Next.js 專案
mkdir subsidy-hub && cd subsidy-hub
npx create-next-app@latest . --typescript --tailwind --eslint --app --src-dir
npm install prisma @prisma/client @prisma/adapter-pg
npm install bcryptjs cheerio iconv-lite lucide-react nodemailer react-markdown remark-gfm xml2js zustand
環境變數(.env)
DATABASE_URL="postgresql://subsidy_user:***@127.0.0.1:5432/subsidy_hub?schema=public"
SITE_URL=https://你的網域
困難 1:DATABASE_URL 密碼中有特殊符號會噴錯
解法:用 URL encoding 取代特殊字元,例如 @ 寫成 %40,# 寫成 %23
3. 專案初始化
Prisma Schema — 8 個資料表
建立 prisma/schema.prisma,定義以下資料表:
| 資料表 | 用途 |
|---|---|
| Subsidy | 補助資料主表(25+ 欄位) |
| User | 會員(Email、OAuth、點數、等級) |
| Plan | AI 生成的計畫書 |
| Transaction | 金流交易紀錄 |
| SubsidyMatch | AI 配對結果 |
| UserFavorite | 使用者收藏 |
| UserSession | 登入 session |
| ApplicationStatus | 補助申請進度 |
npx prisma migrate dev --name init
npx prisma generate
4. 資料庫設計
最難的決定:補助資料怎麼來?
| 方式 | 優點 | 缺點 |
|---|---|---|
| 手動輸入 | 精準 | 累死 |
| 政府 Open Data API | 官方資料 | 格式不統一 |
| 網頁爬蟲 | 資料量大 | 容易被封鎖 |
| AI 生成 | 快速 | 可能幻覺 |
我們選的方案:網頁爬蟲為主 + AI 補缺欄位
AI 補缺欄位
爬蟲只能拿到基本資料,像 age_limit、target、documents 這些欄位常常是空的。用推論補齊後:age_limit 從 5% 提升到 60%,target 和 documents 從 50% 提升到 100%。
5. 核心功能開發
專案結構
src/
app/
page.tsx # 首頁
subsidies/ # 補助列表 + 詳情
ai-match/ # AI 配對
ai-assistant/ # 文件助手(計畫書生成)
login/register/ # 會員
admin/ # 管理後台
api/ # API 路由
layout.tsx # 全站佈局
sitemap.ts # SEO
components/ # 共用元件
lib/
db.ts # Prisma 客戶端
session.ts # 登入 session
ai/client.ts # AI 多 provider 支援
utils.ts # 工具函式
prisma/schema.prisma # 資料庫定義
會員系統(Email + Google OAuth)
使用 Token-based session:crypto.randomUUID() 產生 token,存在資料庫,HttpOnly Cookie 存前端。Google OAuth 透過 NextAuth 或手動授權流程完成。
AI 配對
不採用傳統關鍵字比對,而是讓AI分析使用者的年齡、地區、類別等條件,計算每個補助的匹配分數,回傳最相關的 10 筆。困難 2:Google 封鎖爬蟲
Google 搜尋爬蟲遇到 HTTP 429/403 封鎖。解法:改用 Bing 搜尋(www.bing.com/search),Bing 比較寬鬆。最終用 44 組關鍵字掃全台公部門網站。
6. 金流串接(PayPal)
先去 PayPal Developer Dashboard 建立應用程式,取得 Client ID 和 Secret。
最重要的一步: 把 https://你的網域 加到 PayPal 後台的 Return URLs,否則會跳 401。困難 3:PayPal 沙箱 vs 正式環境
一開始用沙箱測試,但正式上線忘了改回正式 API 網址。解法:用環境變數 PAYPAL_SANDBOX=false 控制,寫在 .env 裡一目瞭然。
7. LINE Bot 串接
申請 LINE Channel
- 去 LINE Developers Console → 新增 Provider → 新增 Messaging API Channel
- 取得:Channel Secret、Channel Access Token、Your User ID
Webhook 關鍵流程
1. request.text() 讀取 body
2. HMAC-SHA256 驗證簽章
3. JSON.parse() 取代 request.json()
4. 處理訊息(熱門/關鍵字/預設)
5. LINE API 回覆訊息
6. 一律回傳 200
困難 4:request.text() 跟 request.json() 的衝突
程式碼先呼叫 request.text() 做簽章驗證,但 request 的 body stream 只能讀一次。之後的 request.json() 會因為 stream 被消耗而失敗,回傳 400 Invalid JSON。
解法: 只讀一次 request.text(),然後用 JSON.parse() 取代 request.json()困難 5:公司網路封鎖 IPv6
環境沒有公網 IP,IPv6 被封住,導致連不上 LINE 的 API 伺服器。
解法: 在 HTTP 連線強制使用 IPv4:httpx.AsyncHTTPTransport(local_address="0.0.0.0")
8. AI 功能串接
5 套提示詞系統(依補助類型自動切換)
| 類型 | AI 角色 | 章節數 |
|---|---|---|
| 創業 / 研發 | 商業計畫書專家 | 9 章 |
| 住宅補貼 | 居住補助顧問 | 5 章 |
| 育兒津貼 | 育兒福利專家 | 6 章 |
| 社福 / 長照 | 社福補助顧問 | 6 章 |
| 職訓 / 進修 | 職訓教育專家 | 6 章 |
表單也要對應
第一次犯的錯: 所有表單共用同一個 state 物件,結果 form.companyName 在商業表單是「公司名稱」,在住宅表單卻是「每月租金」。
解法: 每種表單類型建立專屬 payload(buildPayload()),只送相關欄位給後端。
後端收到後依 formType 解析對應欄位,組不同的 userDataBlock 餵給 AI。
9. 讓網站上線(Cloudflare Tunnel)
為什麼選 Cloudflare Tunnel?
- 沒有公網 IP → 無法直接暴露 port
- Cloudflare Tunnel 建立加密通道,不用開防火牆
- 順便 CDN + DDoS 保護 + HTTPS 自動
# 安裝 cloudflared
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 \
-o /usr/local/bin/cloudflared
chmod +x /usr/local/bin/cloudflared
# 登入 + 建立隧道
cloudflared tunnel login
cloudflared tunnel create subsidy-hub
cloudflared tunnel route dns subsidy-hub 你的網域
# 設定檔 ~/.cloudflared/subsidy-hub.yml
tunnel: subsidy-hub
credentials-file: /home/你/.cloudflared/隧道ID.json
ingress:
- hostname: 你的網域
service: http://localhost:3000
- service: http_status:404
# 啟動
cloudflared tunnel run subsidy-hub
困難 6:Cloudflare 的 QUIC 被封
部分公司網路封鎖 UDP(QUIC 協定),導致 Tunnel 連不上。
解法: 強制使用 HTTP2:cloudflared tunnel --protocol http2 run subsidy-hub
10. 自動爬蟲與排程
使用 Hermes Cron Job
| 排程 | 時間 | 用途 |
|---|---|---|
| 補助自動爬蟲 | 每日 08:00 | 搜尋 44 組關鍵字,爬取最新補助 |
| 補助截止提醒 | 每日 09:00 | 檢查 7 天內截止的補助,推送到 Telegram |
11.遇到的困難與解法
| # | 問題 | 症狀 | 解法 |
|---|---|---|---|
| 1 | DATABASE_URL 特殊字元 | Prisma 連不上 | URL encoding(@ → %40) |
| 2 | Google 封鎖爬蟲 | HTTP 429/403 | 改用 Bing + 44 組關鍵字 |
| 3 | PayPal 沙箱忘了改 | 正式環境無法收款 | 用環境變數 PAYPAL_SANDBOX 控制 |
| 4 | request.stream 只能讀一次 | Webhook 回傳 400 | request.text() + JSON.parse() |
| 5 | 公司網路封鎖 IPv6 | HTTP 連線逾時 | 強制 local_address=’0.0.0.0’(IPv4) |
| 6 | Cloudflare QUIC 被封 | Tunnel 連不上 | –protocol http2 |
| 7 | 表單共用 state | companyName = 公司名或房租? | 依 formType 建專屬 payload |
| 8 | 扣點在 AI 之前 | AI 失敗也扣 200 點 | 先生成成功再扣點 |
| 9 | 提示詞共用 | 住宅補助也生出 9 章商業計畫書 | 5 套提示詞依類型切換 |
| 10 | title 雙重後綴 | 「補助名 | 台灣補助通 | 台灣補助通」 | layout 用 template,子頁只放 title |
發佈留言