會員可用點數是「算出來的」,不是存在某個欄位裡的。
九成的點數功能,是這樣做的:
$user->increment('points', 150);
$user->decrement('points', 600);
users 表加一個 points 欄位,加加減減。
這個設計撐不過三個月,因為它回答不出客服每天都會被問的問題:
一個數字,什麼都答不出來。
我的做法是把點數拆成「一張張點數卡」和「一筆筆使用紀錄」。
point_credits(點數從哪裡來)
user_id
points 這張卡原本有幾點
remaining_points 還剩幾點
type order_reward / admin_adjustment / ...
order_id 來自哪張訂單
available_at 什麼時候可以開始用
expires_at 什麼時候過期
message 備註
point_usages(點數用到哪裡去)
user_id
point_credit_id 扣的是哪一張卡
points 扣了幾點
type redemption / expiration / ...
order_id 用在哪張訂單
每一次發點、每一次折抵,都是 append 一列。沒有任何一個欄位會被覆蓋掉。
public function availablePoints(): int
{
return (int) PointCredit::where('user_id', $this->id)
->where('available_at', '<=', now())
->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))
->sum('remaining_points');
}
已經生效、還沒過期的點數卡,剩餘點數加總。 這就是可用餘額。
不需要維護、不需要同步、不會跟明細對不起來——因為明細就是餘額的唯一來源。
客服可以自己查。 打開會員的點數明細,一列一列是什麼時候發的、哪張訂單發的、用在哪張訂單、什麼時候過期。不用工程師介入。
出錯可以追。 點數算錯的時候,你看得到是哪一筆發錯、哪一筆扣錯,而不是只看到一個「現在是 380 點」然後完全不知道怎麼來的。
點數效期才有辦法做。 這是關鍵——只要點數會過期,一個 balance 欄位就必然不夠用。因為你得知道「這 150 點是哪天發的、什麼時候過期」,而那需要一列資料,不是一個數字。