積分有效期的設計處理方案

背景:積分項目,每次添加的積分都有一個有效期,有效期爲一年,如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. 用戶當天首次登錄時,進行積分計算,排除過期積分;
  3. 每天定時掃描可用積分表1次,將過期積分刪除,並及時更新會員積分總額。

第一種方案需要全部遍歷會員表,如果會員表太大,效率低下,而且如果會員並沒有過期積分,會做許多無效的統計。第二種方案採用被動觸發的形式,雖然也會有無效的積分統計,未能及時清理過期積分,但避免了積分的會員表的全表掃描,性能略高。第三種方案從過期積分入手,主動觸發,能及時清理過期積分,只針對過期積分,沒有無效的統計,性能優於前兩種。

2.2 積分使用

積分使用策略如下:

  1. 現在要凍結point個積分
  2. 可用積分表的可用積分記錄按過期時間升序排列,依次累加積分額度sumPoint直到sumPoint>=point或記錄全部遍歷完,用recordIds記錄符合要求的積分id,用targetRecord記錄最後一條積分記錄。
  3. 判斷sumPoint與point大小,若:
    1. sumPoint < point,則表明用戶積分不足,返回false;
    2. sumPoint = point,則表明當前記錄剛好等於消耗記錄,進行下一步;
    3. sumPoint > point,則表明最後一條記錄大於積分額度,需要將其拆分成兩條記錄,一條用於抵扣(recordId不變,積分額度爲(sumPoint-point)),另一條記錄積分剩餘(額度爲最後一條記錄的額度-(sumPoint-point),過期時間爲最後一條記錄的過期時間)。
  4. 處理可用積分表中的記錄、積分明細表記錄、積分總額。

還是以會員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代碼如下:


 
  1. int pageNo = 0;

  2. int pageSize = 10;

  3. int sumPoint = 0;

  4. //目標記錄

  5. PointRecord targetRecord = null;

  6. //保存要更新的recordId

  7. List<Long> recordIds = new ArrayList<>();

  8. //查詢可用積分記錄

  9. PointRecordSearch search = new PointRecordSearch();

  10. search.setMemberId(memberId);

  11. search.setOrderFields(" expire_time ASC ");

  12. while(sumPoint < point) {

  13. search.setStart(pageNo * pageSize);

  14. search.setLimit(pageSize);

  15. List<PointRecord> pointRecords = pointRecordMapper.queryForPages(search);

  16. if(CollectionUtils.isEmpty(pointRecords)) {

  17. break;

  18. }

  19. for(PointRecord pointRecord : pointRecords) {

  20. //累加,直到值大於等於point

  21. sumPoint += pointRecord.getPoint();

  22. if(sumPoint >= point) {

  23. targetRecord = pointRecord;

  24. break;

  25. }

  26. //記錄相應的id值

  27. recordIds.add(pointRecord.getRecordId());

  28. }

  29. pageNo++;

  30. }

  31. if(sumPoint < point) {

  32. //當前積分不足以消耗

  33. return false;

  34. } else if(sumPoint == point) {

  35. //加上最後一條記錄的值後,總額剛好等於使用額度

  36. recordIds.add(targetRecord.getRecordId());

  37. } else {

  38. // 加上最後一條記錄的值後,總額剛好大於使用額度,此時需要將最後一條記錄拆成兩條記錄

  39. int leftPoint = sumPoint - point;

  40. int usePoint = targetRecord.getPoint() - leftPoint;

  41. //添加一條記錄

  42. PointRecord newPointRecord = new PointRecord();

  43. newPointRecord.setCreateTime(targetRecord.getCreateTime());

  44. newPointRecord.setExpireTime(targetRecord.getExpireTime());

  45. newPointRecord.setMemberId(targetRecord.getMemberId());

  46. newPointRecord.setPoint(leftPoint);

  47. newPointRecord.setUpdateTime(new Date());

  48. pointRecordMapper.insertValues(newPointRecord);

  49. //修改最後一條記錄的積分值

  50. PointRecord updateModel = new PointRecord();

  51. updateModel.setRecordId(targetRecord.getRecordId());

  52. updateModel.setPoint(usePoint);

  53. pointRecordMapper.updateById(updateModel);

  54. }

  55. //按id刪除記錄

  56. pointRecordMapper.deleteByIds(recordIds);

  57. //處理積分明細

  58. PointDetail detail = new PointDetail();

  59. detail.setCreateTime(new Date());

  60. detail.setMemberId(memberId);

  61. detail.setPoint(-point);

  62. detail.setDesc("使用積分");

  63. pointDetailMapper.add(detail);

  64. //處理積分總額

  65. PointTotal pointTotal = new PointTotal();

  66. pointTotal.setId(memberId);

  67. pointTotal.setPoint(pointRecordMapper.sumByMemberId(memberId));

  68. pointTotalMapper.updateById();

總結:積分過期問題的難點在於每條積分都有過期時間,不好把控。引入了一張新表後,使積分明細與可用積分得以分離,明細表僅展示用戶的積分添加與使用記錄,用戶的可用積分額度由該表進行統計,這樣積分的過期問題就完美得到了解決。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章