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

简介: 本文详解私域积分体系的高可用设计:摒弃简单“余额字段”思维,构建含账户、流水、规则、有效期的微型账务系统。LikeShop标准版以“流水驱动余额”为核心,覆盖签到、消费返、退款回退、积分抵现、兑换五大场景,确保可追溯、可对账、可回滚,规避超发、负扣、失效等典型风险。

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

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

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

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

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

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

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

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

二、核心数据库设计

2.1 用户积分账户表

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

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 积分流水表(核心)

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

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 签到记录表

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

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 积分兑换订单表

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

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 核心实现代码

@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 支付成功返积分

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

@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 退款扣回积分

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

@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 下单积分抵现

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

@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 积分商城兑换

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

@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 并发安全,乐观锁防护

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

// 条件更新扣积分:只有余额足够时才扣成功
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 单商户标准版的这套设计,核心就是把账户层和业务层分开:底层账户统一管流水、管幂等、管并发安全;上层签到、消费、兑换、抵现等场景只管业务规则,不用重复造轮子。这样不仅上线时稳,后续二开加新的积分玩法,也只需要在业务层扩展,不会动到底层账户逻辑。

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

目录
相关文章
|
开发者
乘风者之星来啦!发文享阿里内推机会和50W流量曝光!
乘风者计划特推出乘风者每周之星活动,期待各位博主的参与,成为“每周之星”,上榜者可获得官方流量扶持
19070 3
乘风者之星来啦!发文享阿里内推机会和50W流量曝光!
|
8月前
|
人工智能 安全 API
2026年零门槛部署OpenClaw(Clawdbot):一键生成微信公众号文章并同步至草稿箱指南
2026年,OpenClaw(原Clawdbot、Moltbot)凭借“自然语言驱动+全流程自动化+多工具集成”的核心优势,成为开源AI智能体领域的标杆项目,尤其在自媒体创作场景中,其一键生成公众号文章、自动排版并同步至草稿箱的功能,彻底解决了创作者“磨灵感、调排版、反复修改”的低效痛点,让内容创作效率实现指数级提升。
3656 4
|
16小时前
|
弹性计算 负载均衡 网络协议
阿里云共享流量包是什么?如何收费?
阿里云共享流量包是预付费公网流量套餐,支持自动抵扣ECS、EIP、CLB、共享带宽及IPv6网关等产品的IPv4/IPv6出向流量(不含精品流量),覆盖亚太/欧美区域,分全时与闲时、月/半年/年多种规格,价格实惠、开通即用。阿里云共享流量包官网:https://t.aliyun.com/U/WO80Ra
|
17小时前
|
SQL 运维 小程序
预算有限搭建校园外卖小程序:校门中转幂等与高峰验收
预算有限时先按成熟SaaS跑通单校,软件、模块、部署和第三方费用分项询价,售后拆成系统售后与运营售后,用正常单、退款和高峰批量中转验收。本文给出收餐幂等状态机、表约束、核心代码和重复收餐反例。
|
16小时前
|
运维 监控 自动驾驶
灾备预案为何总成“柜中废纸“?从PDCA循环看应急能力建设
本文从PDCA视角剖析灾备预案失效根因:业务与技术脱节、资源协调混乱、指挥调度低效。提出以业务目标驱动预案编排,覆盖单点/复合/场景等多类预案,强化智能检测、多角色协同与闭环管控,推动预案从“柜中废纸”变为可执行的“作战蓝图”。
|
16小时前
|
SQL 安全 测试技术
LikeShop 单商户高级版二开项目的 Git 分支管理与版本发布规范
针对LikeShop单商户高级版二开场景,我们提出「官方基线隔离、二开分层迭代、发布可追溯、回滚有预案」的分支管理规范。通过新增`official/base`基线分支,严格分离官方代码与二开逻辑,配合6类职责明确的分支及四级版本号(如4.2.1.3),解决合入冲突、回滚失序、多人覆盖等高频痛点,显著降低长期维护成本。
14 0
|
16小时前
|
监控 Java 关系型数据库
LikeShop 多商户高级版商城系统日志采集与监控告警体系搭建
本文针对多商户平台资金链路复杂、故障影响面广等特性,基于LikeShop高级版实践,从日志规范、采集方案、监控分层、告警体系四方面,提出轻量可落地的可观测性建设方案,覆盖入门到生产级演进路径。
19 0
|
3天前
|
SQL 前端开发 Java
LikeShop 二开代码规范:目录结构、钩子使用与版本升级兼容方案
本文为电商系统二开团队提供可落地的代码规范指南,聚焦目录隔离、钩子扩展与升级兼容三大维度。强调“最小侵入、物理隔离、扩展优先、兼容升级”原则,覆盖PHP(ThinkPHP)与Java(SpringBoot)双技术栈,助团队告别升级冲突,实现平滑迭代。
37 0
|
16小时前
|
SQL BI 数据库
LikeShop 多商户高级版营销中心:拼团、砍价、秒杀活动的底层实现
LikeShop多商户营销中心采用「基础活动抽象+子类型扩展+统一库存分账联动」架构,统一管控拼团、砍价、秒杀三大活动。通过独立库存池、原子扣减、状态机驱动、订单标记与分账联动等设计,解决超卖、分账错、状态不同步等高频痛点,显著降低维护成本。
17 0