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

积分系统完整设计案例
需求分析
业务场景:电商平台的会员积分系统
核心功能:
- 积分获取(消费、签到、评价、推荐)
- 积分消耗(兑换商品、抵扣现金)
- 积分查询与管理
- 积分过期与调整
数据库设计
-- 积分账户表 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>
关键业务场景流程图
[消费场景]
用户下单(锁定积分) → 支付成功(+积分) → 订单完成(确认积分)
↓
未支付/取消订单 → 释放锁定积分
[积分兑换场景]
用户选择商品 → 验证积分 → 创建兑换单(扣减积分) → 配货发货
↓
取消兑换 → 退还积分(冻结中)
系统优化建议
-
性能优化:
- 使用Redis缓存积分明细
- 积分流水异步写入
- 定时批量处理过期积分
-
安全控制:
- 防止并发重复发放(分布式锁)
- 积分倍率控制
- 异常交易监控
-
业务扩展:
- 积分商城
- 积分转赠
- 积分抽奖
- 积分抵扣优惠券
这个案例涵盖了积分系统的主要功能模块,实际应用中还需要根据具体业务场景进行适当调整和扩展,如果需要详细实现某个具体功能,可以进一步深入探讨。