iT邦幫忙

2026 iThome 鐵人賽

DAY 14
0
ChatGPT & Codex

AI 時代的輕量化開發:ChatGPT 打造 LINE 多模態記帳與續訂預警 Agent系列 第 14

[Day 14] Smart Default:當使用者沒說清楚該怎麼辦?SubWise 訂閱扣款日的智慧補全機制

  • 分享至 

  • xImage
  •  

前言

昨天完成了 Query Service 正式接進 LINE,讓使用者可以透過自然語言查詢消費與訂閱資料。今天則把焦點放回「訂閱管理」。

原本 SubWise 已經可以辨識使用者正在建立訂閱,也能把訂閱資料寫入 Google Sheets,但實際測試時發現一個很真實的問題:

如果使用者只說「我每個月訂 YouTube Premium,月費 199 元」,卻沒有告訴我下次什麼時候扣款,系統該怎麼辦?

這就是 Day 14 要解決的問題。今天主要完成四件事:檢查既有的 subscription_service.py、擴充訂閱資料欄位(支援 Category 與 Note)、當使用者沒有提供扣款日期時由 Python 自動推算、把修正後的資料一路傳回 app.py,讓 LINE 顯示正確的扣款日期。


今日實作實錄

一、確認既有的 subscription_service.py

在 Day 14 開始之前,已經有一份 subscription_service.py,具備新增訂閱、更新既有訂閱、讀取訂閱、格式化訂閱資料等功能。新增或更新時會使用服務名稱判斷是否已經存在:

for index, record in enumerate(records, start=2):
    service_name = str(record.get("Service", "")).strip()
    if service_name.lower() == name.strip().lower():
        existing_row = index
        break

這樣即使使用者再次說「我每個月訂 Spotify,月費 199 元」,系統也不會一直新增 Spotify,而是找到原本的資料並更新。這個設計在今天的測試中也再次驗證成功。

二、擴充訂閱資料欄位

原本 Google Sheets 的訂閱資料只有 ServicePriceBilling CycleNext Billing DateStatus 五個欄位,今天擴充成七個,新增 CategoryNote

category = data.get("category") or "Subscription"
note = data.get("note") or ""

worksheet.append_row([
    name, amount, billing_cycle, next_billing_date,
    "Active", category, note
])

如果 Gemini 沒有提供 Category,就預設為 Subscription;沒有 Note,就使用空字串。更新既有訂閱時,也同步更新 A~G 欄,讓訂閱資料結構和 Gemini 的 JSON 格式更加一致。

三、發現問題:扣款日期是 None

實際測試輸入「我每個月訂 YouTube Premium,月費199元」,Gemini 正確判斷出這是訂閱,但 next_billing_dateNone——這不是 Gemini 的錯,因為使用者確實沒有提供扣款日期。問題變成:Python 接下來該怎麼處理?

原本程式遇到缺少日期就直接失敗:

if not next_billing_date:
    print("⚠️ 缺少下次扣款日期")
    return False

這是一個很重要的開發經驗:AI 沒有提供資料,不代表整個流程一定要失敗,有些欄位其實可以由程式根據規則推導出來。

四、讓 Python 自動推算下一次扣款日期

加入 datetimecalendar.monthrange,當 next_billing_date 缺失且 billing_cycle == "monthly" 時,讓 Python 自動計算下一個月的同一天:

from datetime import date
from calendar import monthrange

today = date.today()

if billing_cycle == "monthly":
    if today.month == 12:
        next_year, next_month = today.year + 1, 1
    else:
        next_year, next_month = today.year, today.month + 1

    last_day = monthrange(next_year, next_month)[1]
    next_day = min(today.day, last_day)
    next_billing_date = date(next_year, next_month, next_day).isoformat()

特別使用 monthrange() 是為了處理不同月份的天數(1 月 31 天、2 月 28/29 天、4 月 30 天),不能單純把月份 +1 就結束。

為什麼還要用 min() 假設使用者在 1 月 31 日新增每月訂閱,下一個月是 2 月,但 2 月沒有 31 號,直接建立 date(2026, 2, 31) 會出錯。用 min(today.day, last_day) 可以確保日期落在該月實際存在的範圍內,這種處理雖然只有幾行程式碼,卻能避免真實使用時遇到的日期例外。

五、讓 subscription_service 回傳「處理後的資料」

日期算出來後又遇到另一個問題:原本 save_subscription() 只回傳 True,代表即使 Python 已經算出正確日期,app.py 拿到的仍是 Gemini 最初的 None,導致 Google Sheets 顯示正確日期,但 LINE 卻顯示 None

解決方式是把成功結果改成回傳完整的 data

data["next_billing_date"] = next_billing_date
data["amount"] = amount
data["category"] = category
data["note"] = note
return data

這樣就能把「Gemini 原始資料」跟「Python 處理完成後的資料」區分開來,app.py 也同步從 data.get(...) 改成 result.get(...),確保 LINE 顯示的是真正處理完成後的資料:

result = save_subscription(data)

if result:
    return (
        "✅ 訂閱建立成功!\n\n"
        f"📌 服務:{result.get('name')}\n"
        f"💰 金額:NT${result.get('amount')}\n"
        f"🔄 扣款週期:{result.get('billing_cycle')}\n"
        f"📅 下次扣款:{result.get('next_billing_date')}"
    )

完整測試

測試一:Apple Music(缺少扣款日期)
輸入「我每個月訂 Apple Music,月費165元」,Gemini 回傳 next_billing_date: None,Python 自動補上 2026-09-15,成功寫入 Google Sheets,LINE 也正確顯示「📅 下次扣款:2026-09-15」,原本會出現的 ❌ None 已經消失。

因此讓 subscription_service.py 在收到空值時,依照今天日期與扣款週期自動推算下一次扣款日,降低資料寫入失敗的機會:
https://ithelp.ithome.com.tw/upload/images/20260815/20178527RCkpI0jA88.png

測試二:確認既有訂閱可以更新
輸入「我每個月訂 Spotify,月費199元」,Spotify 原本已經存在,系統沒有新增第二筆,而是顯示「🔄 發現既有訂閱:Spotify → ✅ 已更新第 3 列資料」,並將下次扣款日期更新為 2026-09-14,證明今天新增的自動日期計算沒有破壞原本的新增/更新邏輯。
https://ithelp.ithome.com.tw/upload/images/20260815/20178527GGpmNc1r23.png

測試三:確認查詢功能仍然正常
輸入「我有哪些訂閱」,成功透過 query_service.py 取得 Netflix、Spotify、YouTube Premium、Disney+、Apple Music,並正確顯示各自的扣款日期,代表今天新增的訂閱寫入邏輯,沒有破壞前一天完成的 Query Service。
https://ithelp.ithome.com.tw/upload/images/20260815/201785278fy9vOcuen.png


今天踩到的坑

1.Gemini 回傳 null,不代表 Gemini 出錯
使用者沒有提供扣款日期時,next_billing_date: None 其實是合理結果,真正需要處理的是後續的 Python 邏輯,而不是責怪 AI 判斷錯誤。

2.不要讓 AI 負責所有資料計算
「下一次扣款日」是一個很適合交給 Python 處理的規則型計算,因此採取「Gemini 負責理解使用者說什麼,Python 負責確定的資料計算,Google Sheets 負責保存資料」的分工,比要求 Gemini 自己計算所有日期更加可靠。

3.處理後的資料要傳回下一層
如果 Python 已經把 None 修正成 2026-09-15,卻還讓 app.py 使用原始 Gemini JSON,就會出現「Google Sheets 正確、LINE 錯誤」的落差。因此把成功時的回傳值從 True 改成 data,讓後面的程式取得真正處理完成的資料。

4.月份不是永遠都有同一天
日期計算不能只寫 month + 1,還需要考慮 12 月跨年、2 月天數、30/31 天月份的差異,因此加入 monthrange() 讓日期計算更加安全。


今日完成

  • 確認並沿用既有的訂閱新增/更新判斷邏輯
  • 擴充訂閱資料欄位,新增 Category 與 Note
  • 加入 Python 自動推算下次扣款日期(處理跨月、跨年、月份天數差異)
  • 修正 subscription_service 回傳值,確保 LINE 顯示處理後的正確資料
  • 完整測試:新訂閱補全日期、既有訂閱更新、訂閱查詢皆正常運作

明日預告

目前 SubWise 還是被動的——使用者主動詢問,SubWise 才會回答。Day 15 就是讓 SubWise 不再只是「等使用者詢問」,而是能根據訂閱資料主動發現即將到期的扣款,並提醒使用者。這也將成為接下來訂閱提醒功能的基礎。我們明天見!


上一篇
[Day 13] 讓查帳功能真正上線:把 Query Service 接進 LINE Bot 即時對話
下一篇
[Day 15] 餵給 AI 的不只是帳單:用 Python 量化 SubWise 的消費結構
系列文
AI 時代的輕量化開發:ChatGPT 打造 LINE 多模態記帳與續訂預警 Agent16
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言