昨天完成了 Query Service 正式接進 LINE,讓使用者可以透過自然語言查詢消費與訂閱資料。今天則把焦點放回「訂閱管理」。
原本 SubWise 已經可以辨識使用者正在建立訂閱,也能把訂閱資料寫入 Google Sheets,但實際測試時發現一個很真實的問題:
如果使用者只說「我每個月訂 YouTube Premium,月費 199 元」,卻沒有告訴我下次什麼時候扣款,系統該怎麼辦?
這就是 Day 14 要解決的問題。今天主要完成四件事:檢查既有的 subscription_service.py、擴充訂閱資料欄位(支援 Category 與 Note)、當使用者沒有提供扣款日期時由 Python 自動推算、把修正後的資料一路傳回 app.py,讓 LINE 顯示正確的扣款日期。
在 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 的訂閱資料只有 Service、Price、Billing Cycle、Next Billing Date、Status 五個欄位,今天擴充成七個,新增 Category 與 Note:
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 格式更加一致。
實際測試輸入「我每個月訂 YouTube Premium,月費199元」,Gemini 正確判斷出這是訂閱,但 next_billing_date 是 None——這不是 Gemini 的錯,因為使用者確實沒有提供扣款日期。問題變成:Python 接下來該怎麼處理?
原本程式遇到缺少日期就直接失敗:
if not next_billing_date:
print("⚠️ 缺少下次扣款日期")
return False
這是一個很重要的開發經驗:AI 沒有提供資料,不代表整個流程一定要失敗,有些欄位其實可以由程式根據規則推導出來。
加入 datetime 與 calendar.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) 可以確保日期落在該月實際存在的範圍內,這種處理雖然只有幾行程式碼,卻能避免真實使用時遇到的日期例外。
日期算出來後又遇到另一個問題:原本 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 在收到空值時,依照今天日期與扣款週期自動推算下一次扣款日,降低資料寫入失敗的機會:
測試二:確認既有訂閱可以更新
輸入「我每個月訂 Spotify,月費199元」,Spotify 原本已經存在,系統沒有新增第二筆,而是顯示「🔄 發現既有訂閱:Spotify → ✅ 已更新第 3 列資料」,並將下次扣款日期更新為 2026-09-14,證明今天新增的自動日期計算沒有破壞原本的新增/更新邏輯。
測試三:確認查詢功能仍然正常
輸入「我有哪些訂閱」,成功透過 query_service.py 取得 Netflix、Spotify、YouTube Premium、Disney+、Apple Music,並正確顯示各自的扣款日期,代表今天新增的訂閱寫入邏輯,沒有破壞前一天完成的 Query Service。
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() 讓日期計算更加安全。
subscription_service 回傳值,確保 LINE 顯示處理後的正確資料目前 SubWise 還是被動的——使用者主動詢問,SubWise 才會回答。Day 15 就是讓 SubWise 不再只是「等使用者詢問」,而是能根據訂閱資料主動發現即將到期的扣款,並提醒使用者。這也將成為接下來訂閱提醒功能的基礎。我們明天見!