首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫

積分系統儲存過程設計實戰:獲取與消費邏輯完整解析

     編輯:MySQL 資料庫相關知識

會員積分系統是電商、O2O 平台核心模組。本文以真實專案 sp_GM_JF_AddScore 為例,深度解析積分獲取儲存過程的核心邏輯:防重複領取機制、明細與總帳雙寫入、樂觀鎖並發控制、事務原子性保障,供架構師與後端工程師參考借鑒。

核心資料表結構

表名用途關鍵欄位
GM_JF會員總積分帳戶ACCOUNT_ID, JF_AMOUNT(總積分), TTL_JF_AMOUNT(可用積分), Version(樂觀鎖)
GM_JF_DETAIL積分變動明細流水ACCOUNT_ID, JF_CategoryNumber, CARD_NUM, HQ_JF_AMOUNT, USE_DATE, USE_DESC
GM_JF_ACTION_RULES積分動作規則定義AN_CategoryNumber, RepetitionsCycle(周期天數), RepetitionsFrequency(周期內最大次數)
GM_JF_GOODS_RULES積分商品兌換規則(本範例未直接使用)

儲存過程完整碼 sp_GM_JF_AddScore

ALTER PROCEDURE [dbo].[sp_GM_JF_AddScore]
    @ACCOUNT_ID           varchar(30),      -- 會員帳號
    @JF_CategoryNumber    varchar(15),      -- 動作類別代碼
    @CARD_NUM             int,              -- 關聯卡號/訂單號
    @HQ_JF_AMOUNT         int,              -- 本次獲得積分數
    @State                varchar(16),      -- 業務狀態標記
    @USE_DESC             varchar(400),     -- 描述備註
    @StatusCode           int OUTPUT        -- 0失敗 1成功 2重複領取
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE 
        @repetitionsCycle       float = 0,   -- 規則周期(天)
        @repetitionsCycle_sec   int   = 0,   -- 規則周期(秒)
        @repetitionsFrequency   int   = 0,   -- 周期內允許最大次數
        @realFrequency          int   = 0,   -- 實際已領取次數
        @USE_DATE               datetime = GETDATE();

    /* ---------- 1. 讀取規則判斷是否允許重複領取 ---------- */
    SELECT TOP 1 
        @repetitionsCycle = RepetitionsCycle,
        @repetitionsFrequency = RepetitionsFrequency
    FROM GM_JF_ACTION_RULES
    WHERE AN_CategoryNumber = @JF_CategoryNumber;

    IF (@repetitionsCycle < 1)  -- 秒級周期
    BEGIN
        SET @repetitionsCycle_sec = (@repetitionsCycle - 1) * 24 * 60 * 60;
        SELECT @realFrequency = COUNT(1)
        FROM GM_JF_DETAIL
        WHERE ACCOUNT_ID = @ACCOUNT_ID
          AND JF_CategoryNumber = @JF_CategoryNumber
          AND USE_DATE <= @USE_DATE
          AND USE_DATE >= CONVERT(varchar(19), DATEADD(SECOND, -@repetitionsCycle_sec, @USE_DATE), 120);
    END
    ELSE  -- 天級周期
    BEGIN
        SELECT @realFrequency = COUNT(1)
        FROM GM_JF_DETAIL
        WHERE ACCOUNT_ID = @ACCOUNT_ID
          AND JF_CategoryNumber = @JF_CategoryNumber
          AND USE_DATE <= @USE_DATE
          AND USE_DATE >= CONVERT(varchar(10), DATEADD(DAY, -(@repetitionsCycle - 1), @USE_DATE), 120);
    END

    IF (@realFrequency >= @repetitionsFrequency)
    BEGIN
        SET @StatusCode = 2;  -- 重複領取
        RETURN 2;
    END

    /* ---------- 2. 事務處理:寫明細 + 更新總帳 ---------- */
    DECLARE @count int = 0;
    DECLARE @temp_table TABLE (
        ACCOUNT_ID varchar(30),
        JF_AMOUNT decimal(16,2),
        TTL_JF_AMOUNT decimal(16,2),
        Last_Update_Time datetime,
        [Version] int
    );

    BEGIN TRAN;

    -- 2.1 寫入明細流水
    INSERT INTO GM_JF_DETAIL (ACCOUNT_ID, JF_CategoryNumber, CARD_NUM, HQ_JF_AMOUNT, [State], USE_DESC)
    VALUES (@ACCOUNT_ID, @JF_CategoryNumber, @CARD_NUM, @HQ_JF_AMOUNT, @State, @USE_DESC);

    -- 2.2 讀取總帳現狀至表變數(避免長時間鎖表)
    INSERT INTO @temp_table
    SELECT ACCOUNT_ID, JF_AMOUNT, TTL_JF_AMOUNT, Last_Update_Time, [Version]
    FROM GM_JF WHERE ACCOUNT_ID = @ACCOUNT_ID;

    SELECT @count = COUNT(1) FROM @temp_table;

    IF (@count = 0)  -- 首次獲得積分,建立總帳
    BEGIN
        INSERT INTO GM_JF (ACCOUNT_ID, JF_AMOUNT, TTL_JF_AMOUNT)
        VALUES (@ACCOUNT_ID, @HQ_JF_AMOUNT, @HQ_JF_AMOUNT);
    END
    ELSE  -- 更新既有總帳,樂觀鎖檢查 Version
    BEGIN
        DECLARE @JF_AMOUNT decimal(16,2), @TTL_JF_AMOUNT decimal(16,2), @Version int;
        SELECT @JF_AMOUNT = JF_AMOUNT, @TTL_JF_AMOUNT = TTL_JF_AMOUNT, @Version = [Version]
        FROM @temp_table WHERE ACCOUNT_ID = @ACCOUNT_ID;

        UPDATE GM_JF
        SET JF_AMOUNT = @JF_AMOUNT + @HQ_JF_AMOUNT,
            TTL_JF_AMOUNT = @TTL_JF_AMOUNT + @HQ_JF_AMOUNT,
            Last_Update_Time = GETDATE(),
            [Version] = @Version + 1
        WHERE ACCOUNT_ID = @ACCOUNT_ID AND [Version] = @Version;  -- 關鍵:樂觀鎖條件

        IF (@@ROWCOUNT = 0)  -- 版本號不符,並發衝突
        BEGIN
            ROLLBACK TRAN;
            SET @StatusCode = 0;
            RETURN 0;
        END
    END

    COMMIT TRAN;
    SET @StatusCode = 1;

    IF (@@ERROR <> 0)
    BEGIN
        SET @StatusCode = 0;
        ROLLBACK TRAN;
    END
END

關鍵設計模式解析

1. 防重複領取:時間窗口 + 頻次限制

  • 規則表定義 RepetitionsCycle(週期長度)與 RepetitionsFrequency(最大次數)
  • 支援「天級」(如每日一次)與「秒級」(如每小時三次)雙模式
  • 查詢明細表統計時間窗內實際領取次數,超限直接返回狀態碼 2

2. 雙寫一致性:明細流水 + 總帳快照

  • 明細表 GM_JF_DETAIL 保留完整審計軌跡,支援對帳、爭議處理
  • 總帳表 GM_JF 提供高效餘額查詢,避免每次 SUM 明細表
  • 單一事務保證「要麼全成功,要麼全回滾」

3. 樂觀鎖解決高並發扣減/增加衝突

  • 總帳表引入 Version 欄位
  • 更新時 WHERE Version = @OldVersion,@@ROWCOUNT = 0 表示衝突
  • 衝突時回滾,上層應用可重試或拋出錯誤
  • 優於悲觀鎖(SELECT ... FOR UPDATE),減少鎖持有時間,提升吞吐

4. 表變數替代臨時表減輕 tempdb 壓力

  • @temp_table 僅存在於記憶體/執行緒區域,無鎖競爭
  • 適用於小結果集(單筆會員總帳)的暫存

呼叫範例與測試

DECLARE @StatusCode int = 1;
EXEC sp_GM_JF_AddScore 
    'admin', 'AN_JF_001_001', 1, 5, 0, '', @StatusCode OUTPUT;
PRINT @StatusCode;  -- 1=成功 2=重複 0=失敗

擴展建議

  1. 積分扣減/兌換:建立對應 sp_GM_JF_DeductScore,邏輯鏡像,需檢查 TTL_JF_AMOUNT >= 扣減量
  2. 分布式系統:引入 Redis 分布式鎖或 Seata AT 模式替代本地事務
  3. 審計日誌:在明細表增加 OperatorID、ClientIP、TraceID 欄位
  4. 效能優化:明細表按 ACCOUNT_ID + USE_DATE 分區;總帳表建立 ACCOUNT_ID 唯一索引
  5. 冪等性:上層傳入唯一業務流水號 BizSeqNo,明細表建立唯一索引防重複提交

此儲存過程展示了企業級積分系統核心寫入路徑的最佳實踐:規則驅動、雙帳一致、樂觀並發、事務保障。理解並靈活套用這些模式,能顯著提升金融級業務模組的可靠性與擴展性。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved