客户积分兑换模块的核心需求
客户积分兑换模块的核心目标是让客户使用账户内的积分兑换指定商品,整个流程需要保证数据准确、逻辑严谨。首先需要明确几个核心业务规则:客户积分余额必须大于等于兑换所需积分、兑换商品当前库存充足、同一客户单次兑换数量不能超过上限、兑换成功后需要同步扣减客户积分、扣减商品库存、新增积分变动记录。

前置条件校验逻辑实现
在正式执行兑换操作前,需要先完成所有前置条件的校验,避免无效操作进入后续流程。校验逻辑需要按照业务优先级依次执行,任何一个条件不满足都直接返回失败提示。
import java.util.HashMap;
import java.util.Map;
/**
* 积分兑换前置校验工具类
*/
public class ExchangeValidator {
// 模拟客户积分数据 key:客户ID value:积分余额
private static final Map<Long, Integer> customerPoints = new HashMap<>();
// 模拟商品库存数据 key:商品ID value:库存数量
private static final Map<Long, Integer> productStock = new HashMap<>();
// 模拟商品兑换规则 key:商品ID value:所需积分
private static final Map<Long, Integer> productExchangePoints = new HashMap<>();
static {
// 初始化测试数据
customerPoints.put(1001L, 5000);
productStock.put(2001L, 10);
productExchangePoints.put(2001L, 1000);
}
/**
* 校验兑换前置条件
* @param customerId 客户ID
* @param productId 商品ID
* @param exchangeNum 兑换数量
* @return 校验结果 包含是否成功和失败原因
*/
public static Map<String, Object> validate(Long customerId, Long productId, int exchangeNum) {
Map<String, Object> result = new HashMap<>();
// 1. 校验客户是否存在
if (!customerPoints.containsKey(customerId)) {
result.put("success", false);
result.put("msg", "客户不存在");
return result;
}
// 2. 校验商品是否存在
if (!productStock.containsKey(productId) || !productExchangePoints.containsKey(productId)) {
result.put("success", false);
result.put("msg", "兑换商品不存在");
return result;
}
// 3. 校验兑换数量是否合法
if (exchangeNum <= 0) {
result.put("success", false);
result.put("msg", "兑换数量必须大于0");
return result;
}
// 4. 校验客户积分是否充足
int needPoints = productExchangePoints.get(productId) * exchangeNum;
if (customerPoints.get(customerId) < needPoints) {
result.put("success", false);
result.put("msg", "积分余额不足,所需积分:" + needPoints);
return result;
}
// 5. 校验商品库存是否充足
if (productStock.get(productId) < exchangeNum) {
result.put("success", false);
result.put("msg", "商品库存不足,当前库存:" + productStock.get(productId));
return result;
}
// 6. 校验单次兑换上限(假设单个商品单次最多兑换5件)
if (exchangeNum > 5) {
result.put("success", false);
result.put("msg", "单次兑换数量不能超过5件");
return result;
}
result.put("success", true);
result.put("msg", "校验通过");
return result;
}
}
数据更新逻辑实现
前置校验通过后,需要执行三个核心数据的更新操作:扣减客户积分、扣减商品库存、新增积分变动记录。为了保证数据一致性,这三个操作需要放在同一个事务中执行,任何一个操作失败都需要回滚所有改动。
下面是基于Spring框架的事务管理实现的兑换核心逻辑:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
@Service
public class ExchangeService {
// 复用之前的模拟数据
private static final Map<Long, Integer> customerPoints = new HashMap<>();
private static final Map<Long, Integer> productStock = new HashMap<>();
private static final Map<Long, Integer> productExchangePoints = new HashMap<>();
// 模拟积分变动记录 key:记录ID value:记录内容
private static final Map<Long, String> pointRecords = new HashMap<>();
private static long recordId = 1;
static {
customerPoints.put(1001L, 5000);
productStock.put(2001L, 10);
productExchangePoints.put(2001L, 1000);
}
/**
* 执行积分兑换操作 添加事务注解保证数据一致性
* @param customerId 客户ID
* @param productId 商品ID
* @param exchangeNum 兑换数量
* @return 兑换结果
*/
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> doExchange(Long customerId, Long productId, int exchangeNum) {
Map<String, Object> result = new HashMap<>();
// 先调用校验方法
Map<String, Object> validateResult = ExchangeValidator.validate(customerId, productId, exchangeNum);
if (!(Boolean) validateResult.get("success")) {
result.put("success", false);
result.put("msg", validateResult.get("msg"));
return result;
}
try {
// 1. 扣减客户积分
int needPoints = productExchangePoints.get(productId) * exchangeNum;
int currentPoints = customerPoints.get(customerId);
customerPoints.put(customerId, currentPoints - needPoints);
// 2. 扣减商品库存
int currentStock = productStock.get(productId);
productStock.put(productId, currentStock - exchangeNum);
// 3. 新增积分变动记录
String record = String.format("客户%d兑换商品%d%d件,扣减积分%d", customerId, productId, exchangeNum, needPoints);
pointRecords.put(recordId++, record);
result.put("success", true);
result.put("msg", "兑换成功");
return result;
} catch (Exception e) {
// 出现异常自动回滚事务
throw new RuntimeException("兑换过程出现异常,已回滚所有操作", e);
}
}
}
常见问题与优化建议
实际开发中还需要注意几个问题:首先是并发场景下的数据一致性,比如多个请求同时兑换同一个商品,可能会出现超卖问题,可以给库存扣减操作添加分布式锁或者数据库行锁;其次是积分变动记录需要包含完整的操作上下文,比如操作时间、操作人、关联订单号等,方便后续对账;另外可以把兑换规则抽象成配置项,支持后台动态修改兑换所需积分、单次兑换上限等规则,不需要修改代码就能调整业务规则。
下面是用ReentrantLock实现的简单本地锁,避免并发场景下的库存超扣问题:
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* 带本地锁的兑换服务 解决并发超卖问题
*/
public class ExchangeServiceWithLock {
private static final Map<Long, Integer> customerPoints = new HashMap<>();
private static final Map<Long, Integer> productStock = new HashMap<>();
private static final Map<Long, Integer> productExchangePoints = new HashMap<>();
// 商品ID对应的锁 不同商品用不同的锁 减少锁竞争
private static final ConcurrentHashMap<Long, ReentrantLock> productLocks = new ConcurrentHashMap<>();
static {
customerPoints.put(1001L, 5000);
productStock.put(2001L, 10);
productExchangePoints.put(2001L, 1000);
}
public Map<String, Object> doExchangeWithLock(Long customerId, Long productId, int exchangeNum) {
// 获取当前商品对应的锁
ReentrantLock lock = productLocks.computeIfAbsent(productId, k -> new ReentrantLock());
lock.lock();
try {
// 先校验
Map<String, Object> validateResult = ExchangeValidator.validate(customerId, productId, exchangeNum);
if (!(Boolean) validateResult.get("success")) {
Map<String, Object> result = new HashMap<>();
result.put("success", false);
result.put("msg", validateResult.get("msg"));
return result;
}
// 执行数据更新
int needPoints = productExchangePoints.get(productId) * exchangeNum;
customerPoints.put(customerId, customerPoints.get(customerId) - needPoints);
productStock.put(productId, productStock.get(productId) - exchangeNum);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("msg", "兑换成功");
return result;
} finally {
lock.unlock();
}
}
}