背景:積分項目,每次添加的積分都有一個有效期,有效期爲一年,如2017-01-02添加了一條積分記錄,到2018-01-02這條記錄應該是過期的。當前項目設計有兩張表:積分明細表(存放積分添加、使用明細)、積分總額表(用戶當前的積分額度)。由於每條積分的過期時間各不相同,如何正確地將過期的積分作廢?消費時,如何優先使用即將過期的積分?
1.問題的提出
剛開始系統有兩張表:積分明細表、積分總額表,積分總額直接由積分明細表統計。
以下是積分明細表,需要將內容展示給用戶,讓用戶知道自己積分的添加、消耗情況:
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 | 備註 |
---|---|---|---|---|---|
1 | 1 | 10 | 2017-01-02 | 2018-01-02 | 新增積分 |
2 | 1 | 20 | 2017-01-04 | 2018-01-04 | 新增積分 |
3 | 1 | -30 | 2017-01-08 | 2018-01-08 | 使用積分 |
假設id爲1的會在2017-01-08後,再無積分添加與消耗的情況。
現在有一張積分總額表,記錄的了會員的積分額度,由於積分是過期的,那麼按不同時間來統計,就會出現以下情況:
在2018-01-01統計時,id爲1的會員積分額度是0,這沒問題。
會員id | 積分總值 | 更新時間 |
---|---|---|
1 | 0 | 2018-01-01 |
那在2018-01-02統計時呢,記錄id爲1的記錄已經過期了,只統計記錄id爲2與3的記錄,會出現以下情況:
會員id | 積分總值 | 更新時間 |
---|---|---|
1 | -10 | 2018-01-02 |
在2018-01-04統計時,記錄id爲1與3的記錄已經過期了,只統計記錄id爲3的記錄,差額就更大了:
會員id | 積分總值 | 更新時間 |
---|---|---|
1 | -30 | 2018-01-04 |
只有在2018-01-08統計時,記錄id爲1、2、3的記錄已經過期了,積分值纔會正常:
會員id | 積分總值 | 更新時間 |
---|---|---|
1 | 0 | 2018-01-08 |
從上面的情況來看,單純從積分明細表來統計,會出現積分值爲負數的情況。
2.解決方案
項目當前的方案中,積分明細表記錄了用戶使用添加與使用情況,需要展現給用戶,所以無法在這上面下手,爲此新添加了一張表來記錄當前可用積分明細。該表的關鍵字段如下:
字段 | 含義 |
---|---|
積分值 | 該記錄的積分值 |
添加時間 | 該記錄的添加時間 |
過期時間 | 該記錄的過期時間 |
如,在2017-01-02該會員的可用積分記錄爲
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 |
---|---|---|---|---|
100 | 1 | 10 | 2017-01-02 | 2018-01-02 |
在2017-01-04該會員的可用積分記錄爲
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 |
---|---|---|---|---|
100 | 1 | 10 | 2017-01-02 | 2018-01-02 |
101 | 1 | 20 | 2017-01-04 | 2018-01-04 |
在2017-01-08該會員的可用積分記錄爲
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 |
---|---|---|---|---|
由於當前會員無可用積分,所以表中記錄爲空。
新加了可用積分表後,後續用戶的積分總額就可以直接從該表中統計了。
明白了大致原理後,接下來解決以下幾個問題。
2.1 積分過期處理
以會員id爲2的用戶爲例
可用積分表如下:
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 |
---|---|---|---|---|
103 | 2 | 10 | 2017-01-02 | 2018-01-02 |
104 | 2 | 20 | 2017-01-04 | 2018-01-04 |
105 | 2 | 20 | 2017-01-06 | 2018-01-06 |
積分明細表如下:
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 | 備註 |
---|---|---|---|---|---|
4 | 2 | 10 | 2017-01-02 | 2018-01-02 | 新增積分 |
5 | 2 | 20 | 2017-01-04 | 2018-01-04 | 新增積分 |
6 | 2 | 20 | 2017-01-06 | 2018-01-06 | 新增積分 |
積分總額表如下:
會員id | 積分總值 | 更新時間 |
---|---|---|
2 | 50 | 2018-01-01 |
在2018-01-02統計該用戶的可用積分時,發現有一條記錄id爲103的記錄已過期,那麼統計完之後,各表變化如下:
可用積分表及時刪除過期記錄:
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 |
---|---|---|---|---|
104 | 2 | 20 | 2017-01-04 | 2018-01-04 |
105 | 2 | 20 | 2017-01-06 | 2018-01-06 |
積分明細表添加一條過期明細:
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 | 備註 |
---|---|---|---|---|---|
4 | 2 | 10 | 2017-01-02 | 2018-01-02 | 新增積分 |
5 | 2 | 20 | 2017-01-04 | 2018-01-04 | 新增積分 |
6 | 2 | 20 | 2017-01-06 | 2018-01-06 | 新增積分 |
7 | 2 | -10 | 2018-01-02 | - | 積分過期 |
積分總額表更新爲正確的積分總額:
會員id | 積分總值 | 更新時間 |
---|---|---|
2 | 40 | 2018-01-04 |
小結:積分過期時,可用積分表刪除過期記錄,積分明細表添加一條過期明細,積分總額表更新爲當前積分總額。
那麼積分過期在什麼時候觸發呢?可以有以下方案:
- 可以每天定時進行一次,對所有的會員操作積分過期計算;
- 用戶當天首次登錄時,進行積分計算,排除過期積分;
- 每天定時掃描可用積分表1次,將過期積分刪除,並及時更新會員積分總額。
第一種方案需要全部遍歷會員表,如果會員表太大,效率低下,而且如果會員並沒有過期積分,會做許多無效的統計。第二種方案採用被動觸發的形式,雖然也會有無效的積分統計,未能及時清理過期積分,但避免了積分的會員表的全表掃描,性能略高。第三種方案從過期積分入手,主動觸發,能及時清理過期積分,只針對過期積分,沒有無效的統計,性能優於前兩種。
2.2 積分使用
積分使用策略如下:
- 現在要凍結point個積分
- 將
可用積分表
的可用積分記錄按過期時間升序排列,依次累加積分額度sumPoint直到sumPoint>=point
或記錄全部遍歷完,用recordIds記錄符合要求的積分id,用targetRecord記錄最後一條積分記錄。 - 判斷sumPoint與point大小,若:
- sumPoint < point,則表明用戶積分不足,返回false;
- sumPoint = point,則表明當前記錄剛好等於消耗記錄,進行下一步;
- sumPoint > point,則表明最後一條記錄大於積分額度,需要將其拆分成兩條記錄,一條用於抵扣(recordId不變,積分額度爲
(sumPoint-point)
),另一條記錄積分剩餘(額度爲最後一條記錄的額度-(sumPoint-point)
,過期時間爲最後一條記錄的過期時間)。
- 處理可用積分表中的記錄、積分明細表記錄、積分總額。
還是以會員id爲2的用戶爲例,用戶各表如下:
可用積分表如下:
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 |
---|---|---|---|---|
103 | 2 | 10 | 2017-01-02 | 2018-01-02 |
104 | 2 | 20 | 2017-01-04 | 2018-01-04 |
105 | 2 | 20 | 2017-01-06 | 2018-01-06 |
積分明細表如下:
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 | 備註 |
---|---|---|---|---|---|
4 | 2 | 10 | 2017-01-02 | 2018-01-02 | 新增積分 |
5 | 2 | 20 | 2017-01-04 | 2018-01-04 | 新增積分 |
6 | 2 | 20 | 2017-01-06 | 2018-01-06 | 新增積分 |
積分總額表如下:
會員id | 積分總值 | 更新時間 |
---|---|---|
2 | 50 | 2017-12-01 |
假設該用戶在2017-12-01需要使用40積分,使用過程如下:
1. 按積分過期時間排序,排序得到的記錄id是103,104,105.這一步的目的是,優先使用最早過期的積分記錄。
2. 累加積分記錄,這裏累加103,104,105的記錄,發現10+20+20>40,滿足條件了。
3. 發現50大於了40,需要把id爲105的記錄拆到兩條記錄,結果如下:
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 |
---|---|---|---|---|
103 | 2 | 10 | 2017-01-02 | 2018-01-02 |
104 | 2 | 20 | 2017-01-04 | 2018-01-04 |
105 | 2 | 10 | 2017-01-06 | 2018-01-06 |
106 | 2 | 10 | 2017-01-06 | 2018-01-06 |
id爲106的記錄由105拆出來的,過期時間與105相同,現在105的積分值(10)+現在的106的積分值(10)=原來的105的積分值(20)
4. 刪除可用積分表中id爲103,104,105的記錄:
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 |
---|---|---|---|---|
106 | 2 | 10 | 2017-01-06 | 2018-01-06 |
5. 處理積分明細表與積分總額表
積分明細表如下:
記錄id | 會員id | 積分值 | 添加時間 | 過期時間 | 備註 |
---|---|---|---|---|---|
4 | 2 | 10 | 2017-01-02 | 2018-01-02 | 新增積分 |
5 | 2 | 20 | 2017-01-04 | 2018-01-04 | 新增積分 |
6 | 2 | 20 | 2017-01-06 | 2018-01-06 | 新增積分 |
7 | 2 | -40 | 2017-12-01 | - | 使用積分 |
積分總額表如下:
會員id | 積分總值 | 更新時間 |
---|---|---|
2 | 10 | 2017-12-01 |
相關的java代碼如下:
-
int pageNo = 0;
-
int pageSize = 10;
-
int sumPoint = 0;
-
//目標記錄
-
PointRecord targetRecord = null;
-
//保存要更新的recordId
-
List<Long> recordIds = new ArrayList<>();
-
//查詢可用積分記錄
-
PointRecordSearch search = new PointRecordSearch();
-
search.setMemberId(memberId);
-
search.setOrderFields(" expire_time ASC ");
-
while(sumPoint < point) {
-
search.setStart(pageNo * pageSize);
-
search.setLimit(pageSize);
-
List<PointRecord> pointRecords = pointRecordMapper.queryForPages(search);
-
if(CollectionUtils.isEmpty(pointRecords)) {
-
break;
-
}
-
for(PointRecord pointRecord : pointRecords) {
-
//累加,直到值大於等於point
-
sumPoint += pointRecord.getPoint();
-
if(sumPoint >= point) {
-
targetRecord = pointRecord;
-
break;
-
}
-
//記錄相應的id值
-
recordIds.add(pointRecord.getRecordId());
-
}
-
pageNo++;
-
}
-
if(sumPoint < point) {
-
//當前積分不足以消耗
-
return false;
-
} else if(sumPoint == point) {
-
//加上最後一條記錄的值後,總額剛好等於使用額度
-
recordIds.add(targetRecord.getRecordId());
-
} else {
-
// 加上最後一條記錄的值後,總額剛好大於使用額度,此時需要將最後一條記錄拆成兩條記錄
-
int leftPoint = sumPoint - point;
-
int usePoint = targetRecord.getPoint() - leftPoint;
-
//添加一條記錄
-
PointRecord newPointRecord = new PointRecord();
-
newPointRecord.setCreateTime(targetRecord.getCreateTime());
-
newPointRecord.setExpireTime(targetRecord.getExpireTime());
-
newPointRecord.setMemberId(targetRecord.getMemberId());
-
newPointRecord.setPoint(leftPoint);
-
newPointRecord.setUpdateTime(new Date());
-
pointRecordMapper.insertValues(newPointRecord);
-
//修改最後一條記錄的積分值
-
PointRecord updateModel = new PointRecord();
-
updateModel.setRecordId(targetRecord.getRecordId());
-
updateModel.setPoint(usePoint);
-
pointRecordMapper.updateById(updateModel);
-
}
-
//按id刪除記錄
-
pointRecordMapper.deleteByIds(recordIds);
-
//處理積分明細
-
PointDetail detail = new PointDetail();
-
detail.setCreateTime(new Date());
-
detail.setMemberId(memberId);
-
detail.setPoint(-point);
-
detail.setDesc("使用積分");
-
pointDetailMapper.add(detail);
-
//處理積分總額
-
PointTotal pointTotal = new PointTotal();
-
pointTotal.setId(memberId);
-
pointTotal.setPoint(pointRecordMapper.sumByMemberId(memberId));
-
pointTotalMapper.updateById();
總結:積分過期問題的難點在於每條積分都有過期時間,不好把控。引入了一張新表後,使積分明細與可用積分得以分離,明細表僅展示用戶的積分添加與使用記錄,用戶的可用積分額度由該表進行統計,這樣積分的過期問題就完美得到了解決。