SCRUM-251 批閱點數實作分析
JIRA: SCRUM-251 Parent: SCRUM-14 (後台-數值管理頁) 難度: ⭐⭐⭐⭐⭐ (最高) 預估工作量: 7-10 天 涉及系統: WriteAhead API + LTRUST API
功能需求摘要
- 批閱扣點機制 - 用戶送出批閱時扣除點數
- LTRUST 整合 - 與 LTRUST 平台同步點數狀態
- 訂閱制服務 - 支援訂閱方案的點數邏輯
- 資料統計 - 點數消耗統計
- 財會對帳 - 管理後台交易報表
現有程式碼分析
已完成部分
| 檔案 | 狀態 | 說明 |
|---|---|---|
LtrustService::deductPoints() | 🟡 Stub | 方法已定義,有 TODO 註解待實作 |
UserPointsModel | 🟢 可用 | 本地點數讀取:getOrCreate(), hasEnoughPoints(), getPoints() |
LogsUserPointsModel | 🟢 可用 | 點數日誌表結構已存在 |
AnswerController::checkPoints() | 🟢 可用 | 檢查點數 API |
待完成部分
| 檔案/功能 | 狀態 | 說明 |
|---|---|---|
UserPointsModel::deduct() | 🔴 缺失 | 本地扣點方法不存在 |
AnswerController::scoring() | 🟡 不完整 | 評分後無扣點邏輯 |
PointsTransactionService | 🔴 缺失 | 點數交易服務需新增 |
| 訂閱制表/服務 | 🔴 缺失 | user_subscriptions 表與相關服務 |
| 財會報表 API | 🔴 缺失 | 後台統計 API |
扣點流程設計
┌─────────────────────────────────────────────────────────────────┐
│ 前端送出批閱 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 1: 檢查本地點數 (UserPointsModel::hasEnoughPoints) │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
點數不足 ❌ 點數足夠 ✓
回傳購買 URL │
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 2: 呼叫 AES 評分 API (AesService::getScore) │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
評分失敗 ❌ 評分成功 ✓
回滾 / 通知 │
▼
┌─────────────────────────────────────────────────────────────────┐
│ Step 3: 扣除點數 (Transaction) │
│ 3a. 扣 LTRUST 點數 (LtrustService::deductPoints) │
│ 3b. 更新本地點數 (UserPointsModel::deduct) │
│ 3c. 記錄交易日誌 (LogsUserPointsModel::insert) │
└─────────────────────────────────────────────────────────────────┘
│ │
▼ ▼
扣點失敗 ❌ 扣點成功 ✓
標記待處理 回傳評分結果
通知管理員
實作 Checklist
Phase 1: 後端核心扣點機制
1.1 完成 LtrustService 扣點 API
檔案: app/Services/External/LtrustService.php
- 確認 LTRUST API 規格 (endpoint, request/response format)
- 完成
deductPoints()實作 - 新增
getPointsBalance()查詢餘額方法 - 新增錯誤處理與重試機制
- 撰寫單元測試
// 需確認的 LTRUST API 細節
// - 實際 endpoint: /api/vendor/service-adjustment 是否正確?
// - product_code 格式規範
// - 錯誤碼定義1.2 擴展 UserPointsModel
檔案: app/Models/Web/UserPointsModel.php
- 新增
deduct(string $email, int $points): bool方法 - 新增
addPoints(string $email, int $points): bool方法 - 使用樂觀鎖或
SELECT FOR UPDATE防止併發超扣 - 新增
syncFromLtrust()同步方法
// 建議實作
public function deduct(string $email, int $points): bool
{
$db = \Config\Database::connect();
$db->transStart();
// 使用悲觀鎖
$row = $db->query("SELECT * FROM user_points WHERE user_email = ? FOR UPDATE", [$email])->getRowArray();
if ((int)$row['points'] < $points) {
$db->transRollback();
return false;
}
$this->where('user_email', $email)
->set('points', 'points - ' . $points, false)
->set('total_spent', 'total_spent + ' . $points, false)
->update();
$db->transComplete();
return $db->transStatus();
}1.3 建立點數交易服務
新檔案: app/Services/PointsTransactionService.php
- 建立服務類別
- 實作
processReviewDeduction()整合扣點流程 - 實作
logTransaction()記錄交易日誌 - 實作補償機制 (LTRUST 扣點成功但本地失敗時)
1.4 整合扣點到評分流程
檔案: app/Controllers/Web/AnswerController.php
- 修改
scoring()方法 - 評分成功後呼叫扣點服務
- 處理扣點失敗情況
// 修改後的 scoring() 流程
public function scoring()
{
// ... 現有驗證邏輯 ...
// Step 1: 再次檢查點數 (防止併發)
if (!$this->userPointsModel->hasEnoughPoints($userEmail)) {
return $this->respond([
'code' => 402,
'message' => '點數不足',
'redirect_url' => env('POINTS_PURCHASE_URL'),
]);
}
// Step 2: 呼叫評分 API
$result = $this->aesService->getScore($data['question'], $data['text']);
if (!$result['success']) {
return $this->fail('評分服務暫時無法使用');
}
// Step 3: 扣除點數 (新增)
$deductResult = $this->pointsService->processReviewDeduction(
$userEmail,
$data['id'],
1 // 扣除點數
);
if (!$deductResult['success']) {
// 評分成功但扣點失敗 - 記錄待處理
log_message('error', "扣點失敗: {$userEmail}, answer_id: {$data['id']}");
}
// Step 4: 儲存評分結果
$this->model->edit([...]);
return $this->respond([...]);
}Phase 2: 點數日誌與統計
2.1 完善 LogsUserPointsModel
檔案: app/Models/Web/LogsUserPointsModel.php
- 定義 type 常數 (EARN, SPEND, REFUND, SYNC)
- 定義 source 常數 (REVIEW, PURCHASE, SUBSCRIPTION, ADMIN_ADJUST)
- 新增
logDeduction()便捷方法 - 新增查詢統計方法
// 建議常數定義
const TYPE_EARN = 'earn';
const TYPE_SPEND = 'spend';
const TYPE_REFUND = 'refund';
const TYPE_SYNC = 'sync';
const SOURCE_REVIEW = 'review';
const SOURCE_PURCHASE = 'purchase';
const SOURCE_SUBSCRIPTION = 'subscription';
const SOURCE_ADMIN_ADJUST = 'admin_adjust';2.2 後台統計 API
新檔案: app/Controllers/Admin/PointsController.php
-
getTransactionList()- 交易明細列表 -
getSummary()- 統計摘要 (總消耗、總儲值等) -
exportReport()- 匯出財會報表
Phase 3: 訂閱制服務
3.1 確認訂閱需求
- 與 PM 確認訂閱方案內容
- 確認 LTRUST 訂閱 API 規格
- 確認訂閱與點數的關係 (訂閱用戶免點數? 訂閱贈點?)
3.2 資料庫設計
新表: user_subscriptions
- 設計表結構
- 建立 Migration
-- 建議表結構
CREATE TABLE user_subscriptions (
id INT PRIMARY KEY AUTO_INCREMENT,
user_email VARCHAR(255) NOT NULL,
plan_code VARCHAR(50) NOT NULL,
ltrust_subscription_id VARCHAR(100),
status ENUM('active', 'expired', 'cancelled') NOT NULL,
started_at DATETIME NOT NULL,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_user_status (user_email, status),
INDEX idx_expires (expires_at)
);3.3 訂閱服務實作
新檔案: app/Services/SubscriptionService.php
-
hasActiveSubscription(string $email): bool -
syncSubscriptionFromLtrust(string $email) - 整合到扣點邏輯 (訂閱用戶跳過扣點)
Phase 4: 前端優化
4.1 點數不足提示 UX
Codebase: writeahead-web
- 優化
app/pages/practice/[id].vue點數不足提示 - 顯示目前剩餘點數
- 提供明確購買按鈕/連結
- 考慮 Toast 或 Modal 形式
Phase 5: 後台財會頁面 (Optional)
Codebase: writeahead-admin-web
- 建立
app/pages/finance/points.vue頁面 - 交易明細表格
- 統計摘要卡片
- 匯出功能
關鍵挑戰與解決方案
| 挑戰 | 風險等級 | 解決方案 |
|---|---|---|
| LTRUST API 規格未確認 | 🔴 高 | 需與 LTRUST 團隊確認,這是阻塞項 |
| 交易一致性 | 🟡 中 | DB Transaction + 補償機制 + 待處理佇列 |
| 併發超扣 | 🟡 中 | SELECT FOR UPDATE 悲觀鎖 |
| 訂閱制邏輯不明 | 🔴 高 | 需 PM 確認訂閱方案細節 |
| LTRUST 與本地點數不同步 | 🟡 中 | 定期同步 + 每次登入同步 |
依賴與阻塞項
外部依賴
| 項目 | 負責方 | 狀態 | 說明 |
|---|---|---|---|
| LTRUST 扣點 API 規格 | LTRUST 團隊 | ⏳ 待確認 | 需確認 endpoint、錯誤碼、測試環境 |
| LTRUST 訂閱 API 規格 | LTRUST 團隊 | ⏳ 待確認 | 訂閱狀態查詢、webhook 通知 |
| 訂閱方案定義 | PM | ⏳ 待確認 | 方案內容、與點數關係 |
建議開發順序
Week 1:
├── [Day 1-2] 確認 LTRUST API 規格 (阻塞項)
├── [Day 3-4] Phase 1.1-1.2: 完成 LtrustService & UserPointsModel
└── [Day 5] Phase 1.3: 建立 PointsTransactionService
Week 2:
├── [Day 1-2] Phase 1.4: 整合扣點到 scoring()
├── [Day 3-4] Phase 2: 日誌與統計
└── [Day 5] Phase 4: 前端 UX 優化
Week 3 (如訂閱需求確認):
├── Phase 3: 訂閱制服務
└── Phase 5: 後台財會頁面 (Optional)
測試要點
單元測試
-
LtrustService::deductPoints()- Mock HTTP 測試 -
UserPointsModel::deduct()- 併發測試 -
PointsTransactionService- 各種失敗情境
整合測試
- 完整批閱流程 (點數足夠)
- 點數不足情境
- LTRUST API 失敗情境
- 併發批閱測試
手動測試
- 點數不足時 UX 流程
- 購買點數後返回繼續批閱
- 後台財會報表資料正確性
關鍵檔案路徑
WriteAhead 後端 (/home/matt/Github/writeahead-main-api)
app/Services/External/LtrustService.php # LTRUST 整合服務
app/Models/Web/UserPointsModel.php # 點數 Model
app/Models/Web/LogsUserPointsModel.php # 點數日誌 Model
app/Controllers/Web/AnswerController.php # 批閱控制器
app/Controllers/Web/AuthController.php # 登入時取得點數
# 待建立
app/Services/PointsTransactionService.php # 點數交易服務
app/Services/SubscriptionService.php # 訂閱服務
app/Controllers/Admin/PointsController.php # 後台財會 API
app/Models/Web/UserSubscriptionsModel.php # 訂閱 Model
WriteAhead 前端 (/home/matt/Github/writeahead-web)
app/pages/practice/[id].vue # 練習頁 (需優化點數 UX)
app/composables/usePractice.ts # 練習相關邏輯
相關筆記
- WriteAhead Sprint 4 工程分析報告 - Sprint 4 總覽
- (待建立) LTRUST 整合技術文件
- (待建立) WriteAhead 點數系統架構
附錄:JIRA 附件
此 issue 包含 3 張附件圖片 (需查看以了解完整需求):
image-20260205-065332.pngimage-20260205-065722.pngimage-20260205-065823.png
更新紀錄
| 日期 | 更新內容 |
|---|---|
| 2026-02-06 | 初始分析,建立完整 checklist |