iT邦幫忙

0

# Day 19 建立個人私房口袋名單 擴充 SQLite 實現加入收藏與歷史最愛回顧

n8n
  • 分享至 

  • xImage
  •  

在昨天的文章中,我們為美食 Agent 加入了趣味滿分的美食轉盤與 LINE Quick Reply 快速按鈕,拯救了無數選擇困難的使用者。

當系統推薦出一間評分極高、看起來非常誘人的餐廳時,使用者的直覺反應往往是:

「這家看起來好讚,先存起來,下次跟朋友去吃!」

如果每次推薦完就隨風而逝,下次想再找這家店就得翻遍落落長的聊天紀錄。因此,今天 Day 19 我們要為 Agent 打造個人化專屬收藏夾:利用 LINE 的 Postback Action 機制,讓使用者只要在推薦卡片上點擊「❤️ 加入收藏」,後端 SQLite 就會將店家自動歸檔至使用者的個人私房名單,並支援隨時隨地用口語指令調閱查看!


一、LINE Postback Action 的運作機制

在 LINE Flex Message 中,按鈕主要有兩種動作形式:

  1. URI Action:純前端跳轉,例如開啟 Google 地圖網址(後端完全不知道使用者按了按鈕)。
  2. Postback Action:使用者點擊按鈕後,LINE 會在背景默默發送一個帶有隱藏資料(Data Payload)的 Webhook 事件到後端,聊天室畫面不會顯示任何公開文字,非常適合用來做「悄悄收藏」、「後台記錄」或「非同步狀態更新」。

Postback 事件封包長相:

{
  "type": "postback",
  "replyToken": "nHuyWiB7yP5Zw52FIkcQobQuGDXCTA",
  "postback": {
    "data": "action=favorite&shop_name=恬淡日常法式甜點&cuisine=甜點/下午茶&location=板橋區&rating=4.7"
  }
}

二、擴充 SQLite 資料庫:新增收藏夾實體表 (app/models/memory.py)

打開 app/models/memory.py,新增一個專門存放使用者最愛的 FavoriteModel 資料表:

from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Float
from sqlalchemy.orm import relationship
from datetime import datetime
from app.database import Base

class FavoriteModel(Base):
    __tablename__ = "favorites"

    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    shop_name = Column(String(100), nullable=False)
    cuisine = Column(String(50), nullable=False)
    location = Column(String(100), nullable=False)
    rating = Column(Float, default=4.0)
    saved_at = Column(DateTime, default=datetime.utcnow)

    # 關聯使用者
    user = relationship("UserModel", back_populates="favorites")

# 記得在 UserModel 中補上雙向關聯:
# UserModel.favorites = relationship("FavoriteModel", back_populates="user", cascade="all, delete-orphan")

三、實作收藏夾管理 API 路由 (app/routers/favorite.py)

在 app/routers/ 目錄下新增 favorite.py,負責收藏的「寫入」與「清單查詢」。

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from pydantic import BaseModel, Field
from typing import List
from datetime import datetime
from app.database import get_db
from app.models.memory import UserModel, FavoriteModel

router = APIRouter(
    prefix="/api/v1/favorites",
    tags=["Favorites"]
)

class AddFavoriteRequest(BaseModel):
    user_id: str = Field(..., description="LINE 使用者 ID")
    shop_name: str
    cuisine: str
    location: str
    rating: float

class FavoriteItemResponse(BaseModel):
    id: int
    shop_name: str
    cuisine: str
    location: str
    rating: float
    saved_at: str

@router.post("/add")
async def add_to_favorites(payload: AddFavoriteRequest, db: Session = Depends(get_db)):
    """將餐廳加入使用者的收藏清單"""
    user = db.query(UserModel).filter(UserModel.line_user_id == payload.user_id).first()
    if not user:
        user = UserModel(line_user_id=payload.user_id)
        db.add(user)
        db.commit()
        db.refresh(user)

    # 檢查是否已收藏過同名店家
    exists = db.query(FavoriteModel).filter(
        FavoriteModel.user_id == user.id,
        FavoriteModel.shop_name == payload.shop_name
    ).first()
    
    if exists:
        return {"status": "already_exists", "message": f"【{payload.shop_name}】已經在您的私房清單中囉!"}

    # 寫入收藏
    new_fav = FavoriteModel(
        user_id=user.id,
        shop_name=payload.shop_name,
        cuisine=payload.cuisine,
        location=payload.location,
        rating=payload.rating,
        saved_at=datetime.utcnow()
    )
    db.add(new_fav)
    db.commit()

    return {"status": "success", "message": f"🎉 成功將【{payload.shop_name}】收錄到個人名單!"}

@router.get("/list/{user_id}")
async def list_favorites(user_id: str, db: Session = Depends(get_db)):
    """查詢使用者的歷史收藏清單"""
    user = db.query(UserModel).filter(UserModel.line_user_id == user_id).first()
    if not user:
        return {"favorites": []}

    items = db.query(FavoriteModel).filter(FavoriteModel.user_id == user.id).order_by(FavoriteModel.saved_at.desc()).all()
    
    results = [
        {
            "id": item.id,
            "shop_name": item.shop_name,
            "cuisine": item.cuisine,
            "location": item.location,
            "rating": item.rating,
            "saved_at": item.saved_at.strftime("%Y-%m-%d")
        }
        for item in items
    ]
    return {"favorites": results}

記得在 app/main.py 中掛載這個新路由:

from app.routers import favorite
app.include_router(favorite.router)

四、更新 Flex Message 卡片:加入「❤️ 收藏」按鈕

回顧 Day 11 的 Flex Message 產生程式碼,我們在每張餐廳 Bubble 的 footer 中多加入一顆按鈕,類型設定為 postback:

// 在每張卡片的 footer.contents 陣列中新增第二顆按鈕:
footer: {
  type: "box",
  layout: "horizontal",
  spacing: "sm",
  contents: [
    {
      type: "button",
      style: "primary",
      color: "#06C755",
      height: "sm",
      action: {
        type: "uri",
        label: "導航",
        uri: `[https://www.google.com/maps/search/?api=1&query=$](https://www.google.com/maps/search/?api=1&query=$){encodeURIComponent(shop.location + ' ' + shop.name)}`
      }
    },
    {
      type: "button",
      style: "secondary",
      height: "sm",
      action: {
        type: "postback",
        label: "❤️ 收藏",
        data: `action=fav&name=${encodeURIComponent(shop.name)}&cuisine=${encodeURIComponent(shop.cuisine)}&loc=${encodeURIComponent(shop.location)}&rate=${shop.rating}`,
        displayText: `把【${shop.name}】加入我的收藏!` // 使用者點擊時聊天室會自動帶出的對話泡泡
      }
    }
  ]
}

五、在 n8n 中處理 Postback 事件與名單回調

Step 1:更新 Extract LINE Event 節點

將原本只收 message 事件的判斷邏輯放寬,讓它也能接納 postback 事件:

const event = $input.first().json.body.events[0];
if (!event) return [];

// 處理 postback 事件(點擊收藏按鈕)
if (event.type === 'postback') {
  const params = new URLSearchParams(event.postback.data);
  return {
    reply_token: event.replyToken,
    user_id: event.source.userId,
    event_type: 'postback',
    action: params.get('action'),
    shop_name: decodeURIComponent(params.get('name') || ''),
    cuisine: decodeURIComponent(params.get('cuisine') || ''),
    location: decodeURIComponent(params.get('loc') || ''),
    rating: parseFloat(params.get('rate') || '4.0')
  };
}

// 處理文字與位置訊息(維持原有邏輯)
if (event.type === 'message') {
  return {
    reply_token: event.replyToken,
    user_id: event.source.userId,
    event_type: 'message',
    user_message: event.message.text || '',
    msg_type: event.message.type
  };
}

return [];

Step 2:工作流分支配置

使用 Switch 節點進行分流:

分支 1(Postback 收藏):呼叫 POST /api/v1/favorites/add,並用 LINE Reply API 回應「🎉 成功將【恬淡日常法式甜點】收錄到個人名單!」。

分支 2(口語查詢收藏):當使用者的自然語言是「查看我的收藏」、「我存了哪些店」,Dify 識別意圖為 list_favorites 時,呼叫 GET /api/v1/favorites/list/{user_id},將已存店家以條列式文字或輪播卡片優雅回傳!

六、真機操作成果驗收

1.觸發收藏:
在推薦卡片底部點擊「❤️ 收藏」,聊天室跳出 把【恬淡日常法式甜點】加入我的收藏!,小幫手隨即回覆:

🎉 成功將【恬淡日常法式甜點】收錄到個人名單!隨時對我說「查看我的收藏」就可以調閱囉!

2.調閱歷史收藏:
在聊天室打字:

使用者:「查看我的收藏清單」

美食小幫手:
📖 您的私房美食名單共有 1 間:

【恬淡日常法式甜點】(甜點/下午茶)
📍 板橋區 ⭐️ 4.7(收藏於 2026-10-15)

想去哪一間?隨時跟我說哦!
今日結語與明日預告
今天我們讓美食 Agent 擁有了更深度的客製化黏著度:

靈活運用了 LINE Postback Action 實現背景靜默操作。

透過 SQLite 關聯資料表 實作了專屬個人的私房店家收藏庫。

支援一鍵收錄與口語回顧,讓使用者真正把這隻 Bot 當成隨身的美食筆記本!

目前為止,所有互動都必須由使用者「先主動發話」。
明天 Day 20,我們將進入主動推播的全新領域:利用 n8n 的 Cron 排程觸發器與 LINE Push Message API,打造每逢中午 11:30 自動向使用者推播「今日午餐靈感」的貼心主動式助理! 敬請期待!


*提醒邦友,使用第三方服務/API 時,請務必評估資安風險與隱私保護
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言