17 KiB
免費用戶使用限制功能實現報告
項目: DramaLing 英語學習平台 功能模組: 免費用戶使用額度管理系統 檢查日期: 2025-01-18 檢查者: Claude Code
📋 功能概述
免費用戶使用限制功能是一個雙層限制系統,通過前端本地計數和後端資料庫驗證,確保免費用戶在 3 小時內最多只能進行 5 次句子分析,防止濫用 AI 資源。
🔧 當前實現架構
📊 限制參數
// 前端常數 (generate/page.tsx)
const FREE_USER_LIMIT = 5 // 最大分析次數
const TIME_WINDOW = "3小時" // 時間窗口
const isPremium = false // 用戶類型
// 後端常數 (UsageTrackingService.cs)
const FREE_USER_ANALYSIS_LIMIT = 5 // 最大分析次數
const FREE_USER_RESET_HOURS = 3 // 3小時重置窗口
🔄 雙層限制實現機制
1️⃣ 前端限制層 (第一道防線)
檔案位置: frontend/app/generate/page.tsx:42-46
// 前端本地檢查
if (!isPremium && usageCount >= 5) {
console.log('❌ 使用次數超限')
alert('❌ 免費用戶 3 小時內只能分析 5 次句子,請稍後再試或升級到付費版本')
return
}
實現方式:
- ✅ 本地狀態:
const [usageCount, setUsageCount] = useState(0) - ✅ 即時檢查: 每次點擊分析按鈕前驗證
- ✅ 用戶回饋: 立即顯示限制提示,無需等待API
- ✅ 計數更新: 成功分析後
setUsageCount(prev => prev + 1)
特點:
- 🚀 即時回應: 無需API調用,立即提示
- 🎯 用戶友善: 清楚說明限制原因和解決方案
- 💰 引導付費: 提示升級到付費版本
2️⃣ 後端驗證層 (權威檢查)
檔案位置: backend/DramaLing.Api/Controllers/AIController.cs:515-531
// 後端權威驗證
var mockUserId = Guid.Parse("00000000-0000-0000-0000-000000000001");
var canUse = await _usageService.CheckUsageLimitAsync(mockUserId, isPremium: false);
if (!canUse)
{
return StatusCode(429, new
{
Success = false,
Error = "免費用戶使用限制已達上限",
ErrorCode = "USAGE_LIMIT_EXCEEDED",
ResetInfo = new
{
WindowHours = 3,
Limit = 5
}
});
}
實現方式:
- ✅ 資料庫查詢: 基於
WordQueryUsageStats表 - ✅ 時間窗口: 查詢過去3小時的使用記錄
- ✅ 精確計算:
SentenceAnalysisCount + LowValueWordClicks - ✅ 錯誤代碼: 結構化錯誤回應
🗄️ 資料庫實現詳情
使用統計表結構
表名: WordQueryUsageStats
-- 使用統計記錄表
CREATE TABLE WordQueryUsageStats (
Id UNIQUEIDENTIFIER PRIMARY KEY,
UserId UNIQUEIDENTIFIER NOT NULL, -- 用戶ID
SentenceAnalysisCount INT NOT NULL, -- 句子分析次數
HighValueWordClicks INT NOT NULL, -- 高價值詞彙點擊次數
LowValueWordClicks INT NOT NULL, -- 低價值詞彙點擊次數(收費)
CreatedAt DATETIME2 NOT NULL, -- 記錄建立時間
UpdatedAt DATETIME2 NOT NULL -- 最後更新時間
);
使用統計查詢邏輯
檔案位置: backend/DramaLing.Api/Services/UsageTrackingService.cs:42-47
// 計算過去3小時的使用量
var resetTime = DateTime.UtcNow.AddHours(-FREE_USER_RESET_HOURS);
var recentUsage = await _context.WordQueryUsageStats
.Where(stats => stats.UserId == userId && stats.CreatedAt >= resetTime)
.SumAsync(stats => stats.SentenceAnalysisCount + stats.LowValueWordClicks);
var canUse = recentUsage < FREE_USER_ANALYSIS_LIMIT;
特點:
- 🕐 滑動窗口: 過去3小時內的累計使用量
- 📊 綜合計算: 句子分析 + 付費詞彙查詢
- 🔒 權威性: 無法被前端繞過
- 📈 可擴展: 支援不同用戶類型的限制
🎨 用戶介面實現
UI顯示邏輯
檔案位置: frontend/app/generate/page.tsx:295-312
// 使用次數顯示
<div className="text-center text-sm text-gray-600">
{isPremium ? (
<span className="text-green-600">🌟 付費用戶:無限制使用</span>
) : (
<span className={usageCount >= 4 ? 'text-red-600' :
usageCount >= 3 ? 'text-yellow-600' : 'text-gray-600'}>
免費用戶:已使用 {usageCount}/5 次 (3小時內)
{usageCount >= 5 && <span className="block text-red-500 mt-1">已達上限,請稍後再試</span>}
</span>
)}
</div>
視覺回饋系統
| 使用次數 | 顏色 | 狀態 | 用戶體驗 |
|---|---|---|---|
| 0-2次 | text-gray-600 |
正常 | 無特殊提示 |
| 3次 | text-yellow-600 |
警告 | 黃色提醒剩餘次數 |
| 4次 | text-red-600 |
危險 | 紅色警告即將達限 |
| 5次 | text-red-500 |
限制 | 顯示"已達上限"訊息 |
按鈕狀態管理
// 分析按鈕禁用邏輯
disabled = isAnalyzing || // 正在分析中
(mode === 'manual' && (!textInput || textInput.length > 300)) || // 輸入驗證
(mode === 'screenshot') // 截圖模式未開放
注意: 前端按鈕禁用主要基於輸入驗證,使用限制檢查在函數內部進行。
🔍 實現流程分析
句子分析流程
用戶點擊「🔍 分析句子」
↓
┌─── 前端檢查 ─────────────────┐
│ 1. 檢查 isPremium 狀態 │
│ 2. 檢查 usageCount >= 5 │
│ 3. 超限則顯示錯誤並 return │
└─────────────────────────────┘
↓ (通過)
┌─── 發送 API 請求 ────────────┐
│ POST /api/ai/analyze-sentence │
└─────────────────────────────┘
↓
┌─── 後端驗證 ─────────────────┐
│ 1. 檢查資料庫使用記錄 │
│ 2. 計算過去3小時累計使用量 │
│ 3. 超限則返回 429 錯誤 │
└─────────────────────────────┘
↓ (通過)
┌─── 執行分析 ─────────────────┐
│ 1. 調用 Gemini AI │
│ 2. 處理分析結果 │
│ 3. 存入快取 │
│ 4. 更新使用統計 │
└─────────────────────────────┘
↓
┌─── 前端更新 ─────────────────┐
│ 1. 顯示分析結果 │
│ 2. usageCount + 1 │
│ 3. 更新UI狀態顯示 │
└─────────────────────────────┘
🚨 問題分析:前後端不同步
⚠️ 發現的設計問題
1. 計數邏輯不一致
前端計數:
// 簡單累加,不考慮時間窗口
setUsageCount(prev => prev + 1)
後端計數:
// 基於3小時滑動窗口的資料庫查詢
var recentUsage = await _context.WordQueryUsageStats
.Where(stats => stats.UserId == userId && stats.CreatedAt >= resetTime)
.SumAsync(stats => stats.SentenceAnalysisCount + stats.LowValueWordClicks);
2. 時間重置機制
| 層級 | 重置機制 | 問題 |
|---|---|---|
| 前端 | ❌ 無重置 | 頁面刷新會重置為0,但實際限制未重置 |
| 後端 | ✅ 3小時滑動窗口 | 正確實現,但前端不知道何時重置 |
3. 快取命中對計數的影響
當前行為:
- ✅ 新句子: 消耗1次額度 (正確)
- ❌ 快取命中: 也消耗1次額度 (可能不合理)
問題: 快取命中不應該消耗額度,因為沒有調用AI API。
🔧 當前實現的優缺點
✅ 優點
- 雙重保護: 前端 + 後端雙重驗證
- 即時回饋: 前端檢查提供即時用戶體驗
- 安全性: 後端驗證防止繞過
- 視覺提示: 分級顏色警告系統
- 付費引導: 清楚的升級提示
❌ 問題
- 不同步: 前後端計數邏輯不一致
- 無時間重置: 前端不知道何時重置額度
- 快取誤計: 快取命中也消耗額度
- 頁面重置: 刷新頁面會重置前端計數器
- 無持久化: 前端計數器無法跨頁面保持
💡 建議改善方案
🎯 短期修復
1. 修復快取計數問題
// 修改前端:快取命中不增加計數
if (result.success) {
// ... 其他處理
// 只有非快取結果才增加計數
if (!result.cached) {
setUsageCount(prev => prev + 1)
}
}
2. 前端計數同步
// 添加從後端獲取實際使用量的功能
const fetchUsageStats = async () => {
const response = await fetch('/api/ai/usage-stats')
const stats = await response.json()
setUsageCount(stats.data.recentUsage)
}
// 在組件初始化時同步
useEffect(() => {
fetchUsageStats()
}, [])
3. 時間重置提示
// 添加重置時間顯示
const nextResetTime = new Date(Date.now() + (3 * 60 * 60 * 1000))
<span className="text-xs text-gray-500 block">
額度將於 {nextResetTime.toLocaleTimeString()} 重置
</span>
🚀 中期改善
1. 統一計數系統
- 移除前端計數器
- 完全依賴後端API提供的使用統計
- 每次操作後同步最新狀態
2. 智能快取策略
- 快取命中不消耗額度
- 高價值詞彙查詢永遠免費
- 只有實際AI調用才計費
3. 增強的用戶體驗
- 實時剩餘額度顯示
- 重置時間倒計時
- 使用歷史記錄
📊 當前實現狀態評估
✅ 運作正常的部分
- 基本限制機制: 確實能防止超量使用
- 視覺回饋系統: 用戶能清楚看到使用狀態
- 後端安全驗證: 無法繞過的伺服器端檢查
- 付費用戶支援: 正確識別並給予無限制使用
⚠️ 存在問題的部分
-
前後端不同步:
- 前端: 簡單累加計數器
- 後端: 3小時滑動窗口計算
-
快取計數邏輯:
- 快取命中仍消耗前端計數器
- 實際上沒有消耗AI資源
-
頁面狀態持久性:
- 頁面刷新會重置前端計數器
- 用戶可能誤以為額度重置
🔍 技術債務
- 資料一致性: 需要統一前後端計數邏輯
- 狀態管理: 需要持久化前端狀態
- 用戶體驗: 需要更準確的額度資訊
🧪 測試用例分析
目前的實現問題測試
測試案例 1: 快取計數問題
步驟:
1. 分析句子A (usageCount = 1)
2. 返回並重新分析句子A (快取命中)
預期: usageCount 應該保持 1
實際: usageCount 變成 2 ❌
問題: 快取命中不應該消耗額度
測試案例 2: 頁面刷新問題
步驟:
1. 分析5次句子 (usageCount = 5)
2. 刷新頁面
預期: 仍然顯示限制狀態
實際: usageCount 重置為 0,可以繼續使用 ❌
問題: 前端狀態沒有持久化
測試案例 3: 後端驗證
步驟:
1. 繞過前端檢查,直接調用API
2. 在3小時內調用超過5次
預期: 後端應該返回 429 錯誤
實際: 需要驗證 ⚠️
狀態: 邏輯存在,但需要實際測試
📈 效能影響分析
前端效能
- 狀態管理: 輕量級 (
useState) - 檢查成本: O(1) 常數時間
- 記憶體使用: 微量 (單一整數值)
後端效能
- 資料庫查詢: 每次分析需要查詢使用統計
- 索引需求:
UserId + CreatedAt複合索引 - 查詢複雜度: 簡單時間範圍查詢
網路效能
- 額外API調用: 可能需要獨立的使用統計API
- 回應大小: 增加使用統計資訊
🎯 功能完整性評估
✅ 已實現功能
- 基本限制: 5次/3小時限制正確執行
- 付費區分: 付費用戶無限制使用
- 視覺提示: 清楚的使用狀態顯示
- 錯誤處理: 適當的錯誤訊息和引導
❌ 缺失功能
- 狀態同步: 前後端計數器不一致
- 時間重置: 用戶不知道何時重置
- 快取優化: 快取命中仍計費
- 歷史記錄: 無使用歷史追蹤
- 統計面板: 無詳細使用統計展示
⚠️ 部分功能
- 後端驗證: 邏輯存在但需要實際測試
- 錯誤處理: 基本實現,可更完善
- 用戶體驗: 功能性足夠,體驗可優化
🚀 改善優先級建議
高優先級 (立即修復)
- ✅ 修復快取計數: 快取命中不消耗額度
- ✅ 前端狀態同步: 從後端獲取實際使用量
- ✅ 頁面刷新處理: 持久化或重新獲取狀態
中優先級 (1-2週內)
- 時間重置提示: 顯示下次重置時間
- 使用統計API: 獨立的使用統計端點
- 增強錯誤處理: 更友善的錯誤訊息
低優先級 (未來功能)
- 使用歷史記錄: 詳細的使用歷史
- 彈性限制: 基於用戶行為的動態限制
- 統計儀表板: 管理員使用統計面板
🏁 結論
當前狀態: ⚠️ 基本可用,存在改善空間
功能性: ✅ 基本限制機制運作正常 用戶體驗: ⚠️ 可用但有混亂點 (快取計數、頁面重置) 技術實現: ⚠️ 雙層保護好,但同步性有問題 商業價值: ✅ 有效防止濫用,引導付費
關鍵改善點
- 統一計數邏輯: 前後端使用相同的計算方式
- 快取計數修復: 快取命中不應消耗額度
- 狀態持久化: 解決頁面刷新重置問題
- 時間透明度: 讓用戶知道重置時間
建議實施
第一階段: 修復快取計數和狀態同步問題 第二階段: 增加時間重置提示和統計API 第三階段: 完整的使用歷史和管理功能
🔄 系統暫時關閉記錄
執行時間: 2025-01-18 15:54 執行者: Claude Code 狀態: ✅ 使用限制系統已暫時關閉
📝 修改記錄
1. 前端修改
檔案: frontend/app/generate/page.tsx
位置: 第24行
修改內容:
// 修改前
const [isPremium] = useState(false)
// 修改後
const [isPremium] = useState(true)
2. 後端修改
檔案: backend/DramaLing.Api/Controllers/AIController.cs
位置: 第517行
修改內容:
// 修改前
var canUse = await _usageService.CheckUsageLimitAsync(mockUserId, isPremium: false);
// 修改後
var canUse = await _usageService.CheckUsageLimitAsync(mockUserId, isPremium: true);
3. 編譯問題修復
檔案: backend/DramaLing.Api/Controllers/AIController.cs
位置: 第7行 (using statements)
修改內容:
// 新增
using System.Text.Json;
🧪 測試結果
✅ 多次調用測試通過:
- 第1次調用: 成功
- 第6次調用: 成功 (原本第6次會被限制)
- API無限制調用確認成功
✅ UI顯示效果:
🌟 付費用戶:無限制使用
🔄 復原使用限制的步驟
當需要重新啟用使用限制時,請執行以下步驟:
步驟1: 復原前端設定
# 編輯檔案: frontend/app/generate/page.tsx
# 第24行改回:
const [isPremium] = useState(false)
步驟2: 復原後端設定
# 編輯檔案: backend/DramaLing.Api/Controllers/AIController.cs
# 第517行改回:
var canUse = await _usageService.CheckUsageLimitAsync(mockUserId, isPremium: false);
步驟3: 重新啟動服務
# 重新啟動後端
cd backend/DramaLing.Api
dotnet run --urls http://localhost:5000
# 前端會自動重新編譯
步驟4: 驗證復原
復原後應該看到:
免費用戶:已使用 0/5 次 (3小時內)
⚠️ 重要提醒
- 資料庫統計: 後端的使用統計仍在記錄,復原限制後會基於實際使用記錄計算
- 快取清理: 如需要完全重置統計,可考慮清理
WordQueryUsageStats表 - 前端狀態: 前端計數器會在頁面刷新後重置,但後端限制基於資料庫記錄
報告生成時間: 2025-01-18 最後更新時間: 2025-01-18 15:54 分析範圍: 前端 + 後端 + 資料庫 功能狀態: 🔄 暫時關閉,隨時可復原