LikeShop 单商户标准版积分体系:签到、消费返积分与积分兑换逻辑

在私域运营的留存工具里,积分体系是普及率最高、但也最容易做"塌"的一个。

很多系统的积分模块做得很简单:用户表加个积分余额字段,下单加一点、消费扣一点。跑起来之后才发现问题:重复签到重复发积分、退款了积分没扣回、积分对不上账找不到原因、用户积分无限累积最后变成财务包袱。

本质上,积分不是一个"字段",是一套微型账户系统------有账户、有流水、有获取规则、有消耗场景、有有效期、有对账依据。LikeShop 单商户标准版的积分模块就是按这个思路设计的:底层统一账户与流水,上层封装签到、消费返、兑换、抵现等业务场景,所有变动可追溯、可对账、可回退。

这篇文章从表结构设计、三个核心业务场景、关键设计原则三个层面拆解实现思路,附核心代码片段。

一、整体架构:账户-流水-业务三层设计

积分体系从上到下分三层,底层稳了,上层加多少场景都不会乱。

复制代码
业务场景层:签到、消费返现、积分抵现、积分兑换、新人奖励
    ↓
流水记录层:每一笔积分变动都生成流水,类型、来源、金额、余额快照
    ↓
账户底层:用户积分账户、余额、累计收支、有效期

核心原则只有一条:永远不直接修改积分余额,所有变动都通过流水驱动。余额只是冗余展示字段,真实账目以流水为准。这样出了问题可以逐笔对账,也能随时按维度统计获取、消耗的构成。

二、核心数据库设计

2.1 用户积分账户表

每个用户一条账户记录,存当前余额和累计统计,不存明细。

sql 复制代码
CREATE TABLE ls_user_integral_account (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL UNIQUE COMMENT '用户ID',
  balance INT NOT NULL DEFAULT 0 COMMENT '当前可用积分',
  total_earned INT NOT NULL DEFAULT 0 COMMENT '累计获取积分',
  total_spent INT NOT NULL DEFAULT 0 COMMENT '累计消耗积分',
  version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
  update_time DATETIME NOT NULL,
  create_time DATETIME NOT NULL,
  INDEX idx_user_id(user_id)
);

2.2 积分流水表(核心)

所有积分变动都必须落流水,这是对账和追溯的唯一依据。

sql 复制代码
CREATE TABLE ls_integral_log (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  
  type TINYINT NOT NULL COMMENT '类型:1获取 2消耗',
  source VARCHAR(32) NOT NULL COMMENT '来源:sign/order/refund/exchange/deduct',
  amount INT NOT NULL COMMENT '变动积分(正数获取,负数消耗)',
  balance_after INT NOT NULL COMMENT '变动后余额快照',
  
  biz_no VARCHAR(64) COMMENT '关联业务单号:订单号、签到日期等',
  remark VARCHAR(255) COMMENT '备注',
  
  create_time DATETIME NOT NULL,
  INDEX idx_user_id(user_id),
  INDEX idx_biz_no(biz_no),
  INDEX idx_create_time(create_time)
);

2.3 签到记录表

专门记录每日签到,用来计算连续天数和断签重置。

sql 复制代码
CREATE TABLE ls_user_sign_in (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  sign_date DATE NOT NULL COMMENT '签到日期',
  continuous_days INT NOT NULL COMMENT '连续签到天数',
  reward_points INT NOT NULL COMMENT '本次获得积分',
  create_time DATETIME NOT NULL,
  UNIQUE KEY uk_user_date(user_id, sign_date),
  INDEX idx_user_id(user_id)
);

2.4 积分兑换订单表

积分商城兑换商品/优惠券时生成,记录兑换明细与发货状态。

sql 复制代码
CREATE TABLE ls_integral_exchange_order (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  goods_id BIGINT COMMENT '兑换商品ID',
  goods_name VARCHAR(128),
  need_points INT NOT NULL COMMENT '消耗积分',
  status TINYINT NOT NULL COMMENT '0待发货 1已发货 2已完成',
  express_no VARCHAR(64),
  create_time DATETIME NOT NULL,
  INDEX idx_user_id(user_id)
);

三、核心场景一:每日签到与连续奖励

签到是最低成本的日活工具。规则通常是:每日签到得基础积分,连续天数越多奖励越高,断签重置回第一天。

3.1 规则设计

  • 第 1 天 5 积分,第 2 天 8 积分,第 7 天 20 积分,7 天一个周期循环;
  • 自然日判断,一天只能签一次;
  • 当天没签就算断签,连续天数重置为 1;
  • 签到奖励积分实时到账。

3.2 核心实现代码

java 复制代码
@Service
public class SignInService {

    @Autowired
    private UserSignInMapper signInMapper;
    @Autowired
    private IntegralAccountService accountService;

    /**
     * 用户签到
     */
    @Transactional(rollbackFor = Exception.class)
    public SignInResult signIn(Long userId) {
        LocalDate today = LocalDate.now();

        // 1. 幂等校验:今天是否已签到
        UserSignIn todayRecord = signInMapper.getByDate(userId, today);
        if (todayRecord != null) {
            throw new BizException("今日已签到");
        }

        // 2. 查询昨天签到记录,计算连续天数
        UserSignIn yesterdayRecord = signInMapper.getByDate(userId, today.minusDays(1));
        int continuousDays = yesterdayRecord == null 
                ? 1 
                : yesterdayRecord.getContinuousDays() + 1;

        // 3. 根据连续天数计算奖励积分(7天一个周期)
        int rewardPoints = calcReward(continuousDays);

        // 4. 写入签到记录
        UserSignIn record = new UserSignIn();
        record.setUserId(userId);
        record.setSignDate(today);
        record.setContinuousDays(continuousDays);
        record.setRewardPoints(rewardPoints);
        signInMapper.insert(record);

        // 5. 增加积分,写流水
        accountService.addIntegral(
                userId, 
                rewardPoints, 
                IntegralSource.SIGN,
                "签到奖励",
                "sign_" + today
        );

        return new SignInResult(continuousDays, rewardPoints);
    }

    /**
     * 阶梯奖励计算
     */
    private int calcReward(int continuousDays) {
        int[] rewards = {5, 8, 10, 12, 15, 18, 20};
        int index = (continuousDays - 1) % rewards.length;
        return rewards[index];
    }
}

积分增减统一走 IntegralAccountService,不在业务代码里直接改余额,保证流水和余额一致。

四、核心场景二:消费返积分与退款回退

消费返积分是最主要的积分获取渠道,规则简单但细节多,尤其是退款反向处理很容易遗漏。

4.1 规则设计

  • 按实付金额比例返还,例:1 元 = 1 积分;
  • 仅商品实付部分计算,运费、优惠券抵扣部分不计入;
  • 支付成功后发放,还是确认收货后发放,可配置;
  • 发生退款时,按退款比例扣回对应积分。

4.2 支付成功返积分

通过订单支付事件驱动,不侵入主订单流程。

java 复制代码
@Service
public class OrderIntegralService {

    @Autowired
    private IntegralAccountService accountService;

    /**
     * 订单支付成功,发放消费积分
     */
    @EventListener
    public void onOrderPaid(OrderPaidEvent event) {
        Order order = event.getOrder();
        Long userId = order.getUserId();
        
        // 1. 计算可返积分的金额(实付 - 运费)
        BigDecimal payableAmount = order.getPayAmount()
                .subtract(order.getFreightAmount());
        if (payableAmount.compareTo(BigDecimal.ZERO) <= 0) {
            return;
        }

        // 2. 按比例计算积分
        BigDecimal rate = getIntegralRate(); // 配置化,如 1:1
        int points = payableAmount.multiply(rate).intValue();
        if (points <= 0) {
            return;
        }

        // 3. 幂等校验:同一订单只发一次
        if (accountService.existsBizNo(userId, "order_" + order.getOrderSn())) {
            return;
        }

        // 4. 发放积分
        accountService.addIntegral(
                userId,
                points,
                IntegralSource.ORDER,
                "购物返积分",
                "order_" + order.getOrderSn()
        );
    }
}

4.3 退款扣回积分

退款是最容易漏的环节。全额退款扣回全部积分,部分退款按比例扣回。

java 复制代码
@EventListener
public void onOrderRefund(OrderRefundEvent event) {
    OrderRefund refund = event.getRefund();
    Order order = event.getOrder();
    Long userId = order.getUserId();

    // 按退款金额占比,计算应扣回积分
    BigDecimal ratio = refund.getRefundAmount()
            .divide(order.getPayAmount(), 4, RoundingMode.HALF_UP);
    
    // 查询该订单发放的总积分
    int totalPoints = accountService.getPointsByBizNo(
            userId, "order_" + order.getOrderSn());
    int deductPoints = new BigDecimal(totalPoints)
            .multiply(ratio).intValue();

    if (deductPoints > 0) {
        accountService.deductIntegral(
                userId,
                deductPoints,
                IntegralSource.REFUND_DEDUCT,
                "订单退款扣回积分",
                "refund_" + refund.getRefundSn()
        );
    }
}

五、核心场景三:积分消耗的两种主流模式

积分只有能花出去,用户才会在乎赚积分。常见的消耗场景有两种:下单积分抵现 和积分商城兑换。

5.1 下单积分抵现

用户提交订单时可勾选使用积分抵扣现金,规则可配置抵扣比例和上限。

java 复制代码
@Service
public class IntegralDeductService {

    /**
     * 下单时计算可抵扣金额
     */
    public BigDecimal calcDeductAmount(Long userId, BigDecimal orderAmount) {
        int userBalance = accountService.getBalance(userId);
        if (userBalance <= 0) {
            return BigDecimal.ZERO;
        }

        // 规则:100积分 = 1元,单笔抵扣不超过订单金额的20%
        BigDecimal maxDeductByRatio = orderAmount.multiply(new BigDecimal("0.2"));
        BigDecimal maxDeductByBalance = new BigDecimal(userBalance)
                .divide(new BigDecimal(100), 2, RoundingMode.DOWN);

        return maxDeductByBalance.min(maxDeductByRatio);
    }

    /**
     * 提交订单时扣减积分
     */
    @Transactional(rollbackFor = Exception.class)
    public void deductOnOrder(Long userId, String orderSn, int usePoints) {
        // 1. 校验余额
        int balance = accountService.getBalance(userId);
        if (balance < usePoints) {
            throw new BizException("积分不足");
        }

        // 2. 扣减积分
        accountService.deductIntegral(
                userId,
                usePoints,
                IntegralSource.ORDER_DEDUCT,
                "订单抵扣",
                "deduct_" + orderSn
        );
    }
}

注意:订单取消或退款时,抵扣的积分要原路退回,和消费返积分的反向逻辑同理。

5.2 积分商城兑换

用户在积分商城用纯积分或"积分+现金"兑换商品、优惠券、周边实物。核心逻辑是校验余额 → 扣积分 → 生成兑换订单 → 发货/发放。

java 复制代码
@Transactional(rollbackFor = Exception.class)
public ExchangeResult exchangeGoods(Long userId, Long goodsId) {
    IntegralGoods goods = goodsMapper.selectById(goodsId);
    if (goods.getStock() <= 0) {
        throw new BizException("库存不足");
    }

    // 1. 扣减积分
    accountService.deductIntegral(
            userId,
            goods.getNeedPoints(),
            IntegralSource.EXCHANGE,
            "积分兑换:" + goods.getName(),
            "exchange_" + IdGenerator.nextId()
    );

    // 2. 扣兑换库存
    int affected = jdbcTemplate.update(
        "UPDATE ls_integral_goods SET stock = stock - 1 WHERE id = ? AND stock > 0",
        goodsId
    );
    if (affected == 0) {
        throw new BizException("手慢了,库存不足");
    }

    // 3. 生成兑换订单
    IntegralExchangeOrder order = new IntegralExchangeOrder();
    order.setUserId(userId);
    order.setGoodsId(goodsId);
    order.setGoodsName(goods.getName());
    order.setNeedPoints(goods.getNeedPoints());
    order.setStatus(0);
    exchangeOrderMapper.insert(order);

    return new ExchangeResult(order.getId(), goods.getNeedPoints());
}

六、账户层的关键设计原则

所有业务场景最终都落到账户层,这一层做对了,上层怎么加场景都稳。

6.1 流水驱动,余额冗余

永远不直接 update 余额字段完事。每一次变动都先生成流水,再更新余额,余额是流水的快照。对账时以流水为准,余额只做展示优化。

6.2 幂等控制,按业务单号去重

同一笔订单、同一天签到、同一个退款单,只能产生一次积分变动。用 biz_no 做唯一约束,防止重试、重复事件导致积分多发或多扣。

6.3 并发安全,乐观锁防护

高并发下直接改余额会出现超扣。用版本号乐观锁或者条件更新保证原子性:

java 复制代码
// 条件更新扣积分:只有余额足够时才扣成功
int update = jdbcTemplate.update(
    "UPDATE ls_user_integral_account " +
    "SET balance = balance - ?, version = version + 1 " +
    "WHERE user_id = ? AND balance >= ? AND version = ?",
    amount, userId, amount, version
);
if (update == 0) {
    throw new BizException("积分不足或状态已变更,请重试");
}

6.4 有效期管理,避免无限累积

积分设置有效期,例如滚动 12 个月有效或每年年底清零。到期前发消息提醒用户消耗,既提升活跃度,也避免积分池无限膨胀变成财务负担。

七、常见坑与最佳实践

  1. 只存余额不存流水

    坑:积分对不上账,查不到哪笔多了哪笔少了。

    最佳实践:流水必存,业务号必录,可追溯可对账。

  2. 退款不回退积分

    坑:用户退款了,积分还在手里,等于白嫖。

    最佳实践:退款事件驱动积分扣回,全额部分额都要处理。

  3. 签到无幂等

    坑:网络重试、多点几次,重复发积分。

    最佳实践:用户+日期唯一索引,业务号去重。

  4. 并发扣减负积分

    坑:同时两笔订单抵扣,余额扣成负数。

    最佳实践:乐观锁或条件更新,数据库层面保证原子性。

  5. 积分无有效期

    坑:积分越积越多,最后平台背不动。

    最佳实践:设置有效期,到期清零,提前提醒。

结语

积分体系看起来是个很简单的小功能,真要做扎实,本质上是一套微型账务系统。

LikeShop 单商户标准版的这套设计,核心就是把账户层和业务层分开:底层账户统一管流水、管幂等、管并发安全;上层签到、消费、兑换、抵现等场景只管业务规则,不用重复造轮子。这样不仅上线时稳,后续二开加新的积分玩法,也只需要在业务层扩展,不会动到底层账户逻辑。

对做电商系统的团队来说,积分模块属于"不起眼但很容易埋雷"的部分。底层按账户系统的标准做扎实了,后面运营怎么玩花样都不会出账务事故。

相关推荐
阿俊-全栈开发10 天前
LikeShop 单商户标准版商品管理二开:分类、SKU 规格与库存联动实现
android·likeshop·likeshop开源商城
阿俊-全栈开发13 天前
LikeShop 版本升级与备份:从旧版平滑迁移的完整步骤
jvm·数据库·oracle·开源·likeshop·likeshop开源商城
阿俊-全栈开发21 天前
LikeShop 单商户SaaS商城分销模块二开:佣金计算与结算逻辑实现
java·开发语言·微服务·likeshop·likeshop开源商城·likeshop多商户
阿俊-全栈开发22 天前
LikeShop单商户高级版定时任务实现:订单超时关闭与自动确认收货
android·likeshop·likeshop开源商城
阿俊-全栈开发24 天前
LikeShop 后台菜单与权限体系解析:角色、权限、菜单表怎么配
likeshop·likeshop开源商城·likeshop多商户
阿俊-全栈开发1 个月前
LikeShop 二开环境搭建:本地开发、接口调试与前端联调全流程
likeshop
kepppt5 个月前
2026年开源商城系统推荐:哪种商城更适合长期二次开发?——真正决定商城系统长期价值的,从来不是“前期功能多少”,而是“后期还能不能持续扩展”
开源商城·likeshop
kepppt5 个月前
哪个开源商城系统更适合二次开发?2026年很多企业开始重视“长期维护成本”——很多系统前期开发很快,但真正决定企业未来成本的,其实是“后期还能不能继续改”
开源商城·likeshop