积分系统案例

wen java案例 1

我来为你设计一个完整的积分系统案例,这个案例将涵盖数据库设计、核心功能实现和业务规则。

积分系统案例

积分系统完整设计案例

需求分析

业务场景:电商平台的会员积分系统

核心功能

  • 积分获取(消费、签到、评价、推荐)
  • 积分消耗(兑换商品、抵扣现金)
  • 积分查询与管理
  • 积分过期与调整

数据库设计

-- 积分账户表
CREATE TABLE `points_account` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `total_points` int(11) NOT NULL DEFAULT '0' COMMENT '总积分',
  `available_points` int(11) NOT NULL DEFAULT '0' COMMENT '可用积分',
  `frozen_points` int(11) NOT NULL DEFAULT '0' COMMENT '冻结积分',
  `level` varchar(20) DEFAULT 'NORMAL' COMMENT '会员等级',
  `version` int(11) DEFAULT '0' COMMENT '乐观锁版本号',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 积分流水表
CREATE TABLE `points_record` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `points` int(11) NOT NULL COMMENT '变动积分(正负)',
  `type` varchar(30) NOT NULL COMMENT '变动类型',
  `order_no` varchar(50) DEFAULT NULL COMMENT '关联订单号',
  `source` varchar(50) DEFAULT NULL COMMENT '来源',
  `balance` int(11) NOT NULL COMMENT '变动后余额',
  `expire_time` datetime DEFAULT NULL COMMENT '过期时间',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user_id_create_time` (`user_id`, `create_time`),
  KEY `idx_order_no` (`order_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 积分规则表
CREATE TABLE `points_rule` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `rule_code` varchar(30) NOT NULL COMMENT '规则编码',
  `rule_name` varchar(100) NOT NULL COMMENT '规则名称',
  `points_value` int(11) NOT NULL COMMENT '积分值',
  `rule_type` varchar(20) NOT NULL COMMENT '规则类型',
  `status` tinyint(1) DEFAULT '1' COMMENT '是否启用',
  `start_time` datetime DEFAULT NULL COMMENT '生效时间',
  `end_time` datetime DEFAULT NULL COMMENT '失效时间',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_rule_code` (`rule_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 积分商品兑换表
CREATE TABLE `points_exchange` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_id` bigint(20) NOT NULL,
  `goods_id` bigint(20) NOT NULL COMMENT '商品ID',
  `goods_name` varchar(100) NOT NULL,
  `cost_points` int(11) NOT NULL COMMENT '消耗积分',
  `exchange_no` varchar(50) NOT NULL COMMENT '兑换单号',
  `status` varchar(20) DEFAULT 'PENDING' COMMENT '状态',
  `consignee` varchar(50) DEFAULT NULL COMMENT '收货人',
  `phone` varchar(20) DEFAULT NULL,
  `address` varchar(200) DEFAULT NULL,
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_exchange_no` (`exchange_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

核心代码实现

// 积分服务接口
public interface PointsService {
    // 增加积分
    Result addPoints(PointsAddRequest request);
    // 扣减积分
    Result deductPoints(PointsDeductRequest request);
    // 冻结积分(下单时)
    Result freezePoints(Long userId, int points, String orderNo);
    // 解冻积分(取消订单)
    Result unfreezePoints(Long userId, String orderNo);
    // 确认扣减(订单完成)
    Result confirmDeduct(Long userId, String orderNo);
    // 查询积分明细
    PageResult<PointsRecord> queryRecords(Long userId, int page, int size);
    // 兑换商品
    Result exchangeGoods(ExchangeRequest request);
}
// 实现类
@Service
@Slf4j
public class PointsServiceImpl implements PointsService {
    @Autowired
    private PointsAccountMapper accountMapper;
    @Autowired
    private PointsRecordMapper recordMapper;
    @Autowired
    private PointsRuleMapper ruleMapper;
    @Autowired
    private PointsExchangeMapper exchangeMapper;
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result addPoints(PointsAddRequest request) {
        // 1. 校验请求
        if (request.getPoints() <= 0) {
            return Result.error("积分必须大于0");
        }
        // 2. 获取规则配置
        PointsRule rule = ruleMapper.selectByCode(request.getRuleCode());
        if (rule == null || rule.getStatus() != 1) {
            return Result.error("积分规则不存在或已禁用");
        }
        // 3. 防止重复发放(根据订单号)
        String lockKey = "points:add:" + request.getOrderNo();
        Boolean notExist = redisTemplate.opsForValue()
            .setIfAbsent(lockKey, "1", 5, TimeUnit.MINUTES);
        if (!Boolean.TRUE.equals(notExist)) {
            return Result.error("该订单已发放过积分");
        }
        try {
            // 4. 乐观锁更新积分
            PointsAccount account = accountMapper.selectByUserIdForUpdate(request.getUserId());
            if (account == null) {
                // 创建账户
                account = createAccount(request.getUserId());
            }
            int updated = accountMapper.addPoints(
                account.getId(), 
                request.getPoints(),
                account.getVersion()
            );
            if (updated == 0) {
                throw new BusinessException("更新积分失败");
            }
            // 5. 记录积分流水
            PointsRecord record = new PointsRecord();
            record.setUserId(request.getUserId());
            record.setPoints(request.getPoints());
            record.setType(request.getRuleCode());
            record.setOrderNo(request.getOrderNo());
            record.setBalance(account.getTotalPoints() + request.getPoints());
            // 设置过期时间(一般有效期1年)
            record.setExpireTime(LocalDateTime.now().plusYears(1));
            record.setRemark(request.getRemark());
            recordMapper.insert(record);
            return Result.success("积分增加成功");
        } catch (Exception e) {
            log.error("增加积分失败", e);
            throw new BusinessException("增加积分失败");
        } finally {
            redisTemplate.delete(lockKey);
        }
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Result deductPoints(PointsDeductRequest request) {
        // 1. 校验积分是否足够
        PointsAccount account = accountMapper.selectByUserIdForUpdate(request.getUserId());
        if (account.getAvailablePoints() < request.getPoints()) {
            return Result.error("积分不足");
        }
        // 2. 扣减积分
        int updated = accountMapper.deductPoints(
            account.getId(),
            request.getPoints()
        );
        if (updated == 0) {
            throw new BusinessException("扣减积分失败");
        }
        // 3. 记录流水(负积分)
        PointsRecord record = new PointsRecord();
        record.setUserId(request.getUserId());
        record.setPoints(-request.getPoints());
        record.setType(request.getType());
        record.setOrderNo(request.getOrderNo());
        record.setBalance(account.getAvailablePoints() - request.getPoints());
        record.setRemark(request.getRemark());
        recordMapper.insert(record);
        return Result.success("扣减成功");
    }
    @Override
    public Result exchangeGoods(ExchangeRequest request) {
        // 1. 查询商品信息
        PointsGoods goods = goodsService.getById(request.getGoodsId());
        if (goods == null) {
            return Result.error("商品不存在");
        }
        // 2. 检查库存
        if (goods.getStock() <= 0) {
            return Result.error("商品库存不足");
        }
        // 3. 检查用户积分
        PointsAccount account = accountMapper.selectByUserId(request.getUserId());
        if (account.getAvailablePoints() < goods.getPoints()) {
            return Result.error("积分不足");
        }
        // 4. 生成兑换单号
        String exchangeNo = generateExchangeNo();
        // 5. 创建兑换记录并扣减积分(事务)
        this.doExchange(request, goods, exchangeNo);
        return Result.success("兑换成功", exchangeNo);
    }
}

积分规则配置案例

public enum PointsRuleEnum {
    REGISTER("REGISTER", "注册奖励", 100),
    DAILY_SIGN("DAILY_SIGN", "每日签到", 5),
    PERFECT_INFO("PERFECT_INFO", "完善资料", 50),
    FIRST_ORDER("FIRST_ORDER", "首次下单", 100),
    CONSUME("CONSUME", "消费积分", 0),  // 按消费金额比例
    REVIEW("REVIEW", "评价奖励", 20),
    SHARE("SHARE", "分享奖励", 30),
    INVITE("INVITE", "邀请注册", 200);
    private String code;
    private String name;
    private int defaultPoints;
    // 消费积分规则:每消费1元得1积分
    public static int calculateConsumePoints(BigDecimal amount) {
        return amount.setScale(0, RoundingMode.DOWN).intValue();
    }
    // 会员等级倍率
    public static double getLevelRate(String level) {
        switch (level) {
            case "SILVER": return 1.2;
            case "GOLD": return 1.5;
            case "DIAMOND": return 2.0;
            default: return 1.0;
        }
    }
}

签到功能实现

@Service
public class SignInService {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private PointsService pointsService;
    // 签到奖励规则
    private static final Map<Integer, Integer> SIGN_REWARDS = Maps.newHashMap();
    static {
        SIGN_REWARDS.put(1, 5);
        SIGN_REWARDS.put(2, 6);
        SIGN_REWARDS.put(3, 7);
        SIGN_REWARDS.put(4, 8);
        SIGN_REWARDS.put(5, 10);
        SIGN_REWARDS.put(6, 12);
        SIGN_REWARDS.put(7, 20);  // 连续7天额外奖励
    }
    public Result dailySign(Long userId) {
        String today = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
        String key = "user:sign:" + userId + ":" + today;
        // 1. 防止重复签到
        Boolean signed = redisTemplate.opsForValue()
            .setIfAbsent(key, "1", 24, TimeUnit.HOURS);
        if (!Boolean.TRUE.equals(signed)) {
            return Result.error("今日已签到");
        }
        // 2. 计算连续签到天数
        int continuousDays = getContinuousDays(userId);
        // 3. 获取签到奖励积分
        int rewardPoints = SIGN_REWARDS.getOrDefault(continuousDays, 5);
        // 4. 发放积分
        PointsAddRequest request = new PointsAddRequest();
        request.setUserId(userId);
        request.setPoints(rewardPoints);
        request.setRuleCode("DAILY_SIGN");
        request.setRemark("第" + continuousDays + "天连续签到");
        return pointsService.addPoints(request);
    }
    private int getContinuousDays(Long userId) {
        // 使用BitMap存储签到状态
        String signKey = "user:sign:bitmap:" + userId;
        LocalDate today = LocalDate.now();
        // 计算连续天数逻辑(省略具体实现)
        int count = 0;
        for (int i = 0; i < 7; i++) {
            LocalDate date = today.minusDays(i);
            Boolean hasSigned = redisTemplate.opsForValue()
                .getBit(signKey, date.getDayOfMonth() - 1);
            if (hasSigned) {
                count++;
            } else {
                break;
            }
        }
        return count;
    }
}

积分过期处理

@Component
public class PointsExpireJob {
    @Autowired
    private PointsRecordMapper recordMapper;
    @Autowired
    private PointsAccountMapper accountMapper;
    @Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点执行
    public void processExpirePoints() {
        log.info("开始处理过期积分");
        // 1. 查询已过期未处理的积分记录
        List<PointsRecord> expireRecords = recordMapper
            .selectExpiredRecords(LocalDateTime.now());
        // 2. 按用户分组处理
        Map<Long, List<PointsRecord>> groupByUser = expireRecords.stream()
            .collect(Collectors.groupingBy(PointsRecord::getUserId));
        for (Map.Entry<Long, List<PointsRecord>> entry : groupByUser.entrySet()) {
            Long userId = entry.getKey();
            List<PointsRecord> records = entry.getValue();
            int totalExpire = records.stream()
                .mapToInt(PointsRecord::getPoints)
                .sum();
            // 3. 更新账户积分
            PointsAccount account = accountMapper.selectByUserIdForUpdate(userId);
            int newAvailable = account.getAvailablePoints() - totalExpire;
            // 4. 更新账户并记录过期流水
            if (newAvailable >= 0) {
                accountMapper.updateAvailablePoints(account.getId(), newAvailable);
                recordMapper.insertExpireRecord(userId, -totalExpire);
            }
        }
        log.info("过期积分处理完成,共处理{}条记录", expireRecords.size());
    }
}

积分查询接口

@RestController
@RequestMapping("/api/points")
public class PointsController {
    @Autowired
    private PointsService pointsService;
    @Autowired
    private PointsQueryService queryService;
    // 查询积分总览
    @GetMapping("/summary")
    public Result getPointsSummary(@RequestParam Long userId) {
        PointsSummary summary = new PointsSummary();
        // 获取账户信息
        PointsAccount account = queryService.getAccountByUserId(userId);
        summary.setTotalPoints(account.getTotalPoints());
        summary.setAvailablePoints(account.getAvailablePoints());
        summary.setFrozenPoints(account.getFrozenPoints());
        // 本月获得积分
        summary.setMonthEarn(recordMapper.getMonthEarnPoints(userId));
        // 本月消耗积分
        summary.setMonthSpend(recordMapper.getMonthSpendPoints(userId));
        // 即将过期积分(30天内)
        summary.setExpiringSoon(recordMapper.getExpiringPoints(userId, 30));
        return Result.success(summary);
    }
    // 查询积分明细(带筛选)
    @GetMapping("/records")
    public Result getPointsRecords(
        @RequestParam Long userId,
        @RequestParam(required = false) String type,
        @RequestParam(required = false) String startTime,
        @RequestParam(required = false) String endTime,
        @RequestParam(defaultValue = "1") int page,
        @RequestParam(defaultValue = "10") int size) {
        PageResult<PointsRecord> result = queryService.queryRecords(
            userId, type, startTime, endTime, page, size);
        return Result.success(result);
    }
}

前端页面展示

<!-- 积分管理页面 -->
<div class="points-page">
    <!-- 积分总览卡片 -->
    <div class="points-summary">
        <div class="total-points">
            <h3>当前积分</h3>
            <span class="number" id="totalPoints">2,580</span>
        </div>
        <div class="points-statistics">
            <div class="stat-item">
                <label>本月获得</label>
                <span>356</span>
            </div>
            <div class="stat-item">
                <label>本月消耗</label>
                <span>120</span>
            </div>
            <div class="stat-item" style="color: #e6a23c;">
                <label>30天内过期</label>
                <span>200</span>
            </div>
        </div>
    </div>
    <!-- 积分明细表格 -->
    <div class="points-records">
        <div class="filter-bar">
            <select id="typeFilter">
                <option value="">全部类型</option>
                <option value="CONSUME">消费获取</option>
                <option value="EXCHANGE">兑换消耗</option>
                <option value="SIGN">签到</option>
            </select>
            <button onclick="loadRecords()">查询</button>
        </div>
        <table>
            <thead>
                <tr>
                    <th>时间</th>
                    <th>类型</th>
                    <th>金额变动</th>
                    <th>余额</th>
                    <th>说明</th>
                </tr>
            </thead>
            <tbody id="recordsTable">
            </tbody>
        </table>
        <div class="pagination" id="pagination"></div>
    </div>
</div>

关键业务场景流程图

[消费场景]
用户下单(锁定积分) → 支付成功(+积分) → 订单完成(确认积分)
                                ↓
             未支付/取消订单 → 释放锁定积分
[积分兑换场景]
用户选择商品 → 验证积分 → 创建兑换单(扣减积分) → 配货发货
                              ↓
                   取消兑换 → 退还积分(冻结中)

系统优化建议

  1. 性能优化

    • 使用Redis缓存积分明细
    • 积分流水异步写入
    • 定时批量处理过期积分
  2. 安全控制

    • 防止并发重复发放(分布式锁)
    • 积分倍率控制
    • 异常交易监控
  3. 业务扩展

    • 积分商城
    • 积分转赠
    • 积分抽奖
    • 积分抵扣优惠券

这个案例涵盖了积分系统的主要功能模块,实际应用中还需要根据具体业务场景进行适当调整和扩展,如果需要详细实现某个具体功能,可以进一步深入探讨。

抱歉,评论功能暂时关闭!