礼品券
会员节活动,针对小程序会员发放礼品券,pc管理端配置:礼品券、活动门店。由第三方给小程序用户发放礼品券。用户可凭借礼品券在小程序预约提货,之后到预约活动门店领取礼品。
门店列表
**需求:**查询用户所选省份下的门店,并由近到远对门店进行排序。验证门店有无库存,使用户只能到有库存的门店进行预约。
**难点:**一张礼品券可以对应多个规格,一个规格对应多个sku,一个sku对应多个门店。规格与sku、sku与门店都是多对多关系。要检测门店下库存够还是不够。
功能点:
1.对省份下的门店,按照距离用户远近,进行排序
检测redis中是否存在门店地理位置缓存,如果不存在则设置缓存
if (!redisClient.exists(StoreRedisKey.GEO_LIST_KEY + provinceCode)) {
setRedisGeoList(provinceCode);
}
/**
* 将门店位置放入redis 便于排序 部分门店 并将key的过期时间设置为一天
* @param provinceCode 省份code
*/
private void setRedisGeoList(String provinceCode) {
log.info("将门店位置放入redis,provinceCode:{}", provinceCode);
// TODO 刷新门店GEO
List<Store> storeList = storeRepository.findAllByProvinceCode(provinceCode);
if (CollectionUtils.isEmpty(storeList)) {
log.info("根据省份code:{},未查询到门店", provinceCode);
throw new BusinessException("您所在的省份没有门店");
}
List<GeoPoint> geoPoints = storeList.stream().map(x -> {
GeoPoint geoPoint = new GeoPoint();
geoPoint.setLon(Double.valueOf(x.getLng()));
geoPoint.setLat(Double.valueOf(x.getLat()));
geoPoint.setPointName(x.getId().toString());
return geoPoint;
}).toList();
redisClient.setGeoList(StoreRedisKey.GEO_LIST_KEY + provinceCode, geoPoints);
redisClient.expire(StoreRedisKey.GEO_LIST_KEY + provinceCode,60*60*24);
}
// redisClient代码
/**
* 批量添加Geo坐标点
* @param key redis key
* @param pointList 坐标点
* @return 数量
*/
public int setGeoList(String key, List<GeoPoint> pointList){
try(Jedis jedis = redisFactory.getJedis()){
for (GeoPoint geoPoint : pointList) {
jedis.geoadd(key,geoPoint.getLon(),geoPoint.getLat(),geoPoint.getPointName());
}
return pointList.size();
}
}
/**
* 设置redis key过期时间
* @param key redis key
* @param seconds 过期时间 seconds (以秒为单位)。
*/
public Long expire(String key, long seconds) {
try (Jedis jedis = redisFactory.getJedis()) {
return jedis.expire(key, seconds);
}
}
// GeoPoint 自定义类
public class GeoPoint {
/**
* 坐标点经度
*/
private Double lng;
/**
* 坐标点纬度
*/
private Double lat;
/**
* 坐标点名称
*/
private String pointName;
// getter and setter
}
按照用户坐标对缓存中的门店精心排序
List<Long> sortedIdList = getSortedIdList(StoreRedisKey.GEO_LIST_KEY + provinceCode, new GeoPoint(Double.valueOf(lng), Double.valueOf(lat)));
/**
* 将门店id列表 按距离目标位置远近排序 部分门店
*
* @param key redis key
* @param point 目标位置
*/
private List<Long> getSortedIdList(String key, GeoPoint point) {
log.info("按距离目标位置远近,获取排序后的门店列表,目标位置:{}", point);
return redisClient.getGeoList(key
, point
, 10000d
, GeoUnit.KM).stream().map(x -> {
return Long.valueOf(x.getMemberByString());
}).toList();
}
// redisClient中的方法
public List<GeoRadiusResponse> getGeoList(String key, GeoPoint point, Double radius, GeoUnit geoUnit){
try(Jedis jedis = redisFactory.getJedis()){
return jedis.georadius(key, point.getLon(), point.getLat(), radius, geoUnit);
}
}
注:redis中的门店是全部门店,还需要根据已经排序好的门店列表,对参加活动的门店进行排序。
扩展点:Redis GEO
官网地址:https://redis.io/commands/?group=geo
2.验证门店库存
思路:jpa查询出来的数据会处于托管状态。这个状态下,修改了实体对应属性值,在事务提交时,会一并提交。因此,将查询出来的数据取消jpa托管。实际的去扣减库存,就可以验证库存够不够
存在问题:当一个门店配置了连个sku,会出现实际情况够,但是系统显示不够的问题。暂未解决。
// 公司特有代码,参考意义不大,而且仍有bug为解决
/**
* 校验门店库存
* @param storeStockList 门店库存
* @param storeVOList 门店列表
* @param activityVoucherGiftSettingsEntityMap 规格信息
* @param skuMap sku map
* @return List
*/
private List<StoreVO> setStockFlag(List<StoreInventory> storeStockList
, List<StoreVO> storeVOList
, Map<Integer, List<ActivityVoucherGiftSettingsEntity>> activityVoucherGiftSettingsEntityMap
, Map<String, Sku> skuMap) {
storeVOList.forEach(storeVO -> {
// 将库存集合 从jpa托管状态 脱离
List<StoreInventoryVO> storeInventoryVOS = storeStockList.stream().map(x -> {
StoreInventoryVO storeInventoryVO = new StoreInventoryVO();
BeanUtils.copyProperties(x, storeInventoryVO);
return storeInventoryVO;
}).toList();
boolean enough = false;
for (Integer integer : activityVoucherGiftSettingsEntityMap.keySet()){
List<ActivityVoucherGiftSettingsEntity> settings = activityVoucherGiftSettingsEntityMap.get(integer);
// 规格数量
Integer specificationNum = settings.get(0).getSpecificationNum();
List<Long> skuIds = settings.stream().map(x -> {
return skuMap.get(x.getSkuCode()).getId();
}).toList();
// 筛选出规格下门店库存
List<StoreInventoryVO> storeInventoryVOSInNo = storeInventoryVOS.stream().filter(x -> {
return x.getStoreId().equals(storeVO.getId());
}).filter(x -> {
return skuIds.contains(x.getSkuId());
}).toList();
if (CollectionUtils.isEmpty(storeInventoryVOSInNo)){
break;
}
for (StoreInventoryVO storeInventoryVO : storeInventoryVOSInNo) {
int stock = storeInventoryVO.getAppInventoryAssigned() - storeInventoryVO.getAppInventoryUsed();
if (stock>=specificationNum){
storeInventoryVO.setAppInventoryUsed(storeInventoryVO.getAppInventoryUsed()+specificationNum);
specificationNum = 0;
}else {
storeInventoryVO.setAppInventoryUsed(storeInventoryVO.getAppInventoryUsed()+stock);
specificationNum -= stock;
}
}
if (specificationNum > 0){
enough = false;
}else {
enough = true;
}
}
storeVO.setInventoryFlag(enough);
});
return storeVOList;
}
预约提货–扣减库存
**需求:**按照sku排序,扣减门店下库存。更新程序库存,回传pos。
**难点:**同门店列表。
功能点:
扣减库存
首先对礼品券下的sku按照规格进行分组
之后对每一个规格下的sku进行扣减库存操作,汇总成记录。之后去重sku,检验库存是否充足。
回传pos,待回传成功之后利用乐观锁修改库存。
// 参考逻辑,利用已经扣减的库存,需要扣减的库存,剩余需要扣减的。三个变量来检验库存是否充足
/**
* 扣减门店库存
*
* @param specificationNum 规格数量
* @param sortedActivityVoucherGiftSettingsDTOList skuid
* @param skuDetailDTOS 结果列表 回传pos
* @param storeInventoryUpdateDTOS 结果列表 保存库存
* @param inventoryBySkuCode 门店库存
* @param skuByCode sku主体数据
*/
private void reduceStock(Integer specificationNum
, List<ActivityVoucherGiftSettingsDTO> sortedActivityVoucherGiftSettingsDTOList
, List<SkuDetatilDTO> skuDetailDTOS
, List<StoreInventoryUpdateDTO> storeInventoryUpdateDTOS
, Map<String, List<StoreInventoryVO>> inventoryBySkuCode
, Map<String, List<SkuVO>> skuByCode) {
if (CollectionUtils.isEmpty(sortedActivityVoucherGiftSettingsDTOList)) {
throw new BusinessException("库存不足");
}
// 已经扣减的库存数量
int stockDeductTotal = 0;
for (ActivityVoucherGiftSettingsDTO x : sortedActivityVoucherGiftSettingsDTOList) {
// 礼品券某个规格下包含的所有 sku 累计可扣减库存满足规格下单数量即可下单
if (stockDeductTotal >= specificationNum) {
break;
}
if (!inventoryBySkuCode.containsKey(x.getSkuCode())) {
continue;
}
if (!skuByCode.containsKey(x.getSkuCode())) {
continue;
}
StoreInventoryVO storeInventory = inventoryBySkuCode.get(x.getSkuCode()).get(0);
SkuVO sku = skuByCode.get(x.getSkuCode()).get(0);
// 当前 sku 可用库存数量
int stock = storeInventory.getAppInventoryAssigned() - storeInventory.getAppInventoryUsed();
if (stock <= 0) {
continue;
}
// 剩余需扣减库存
int stock2deduct = specificationNum - stockDeductTotal;
StoreInventoryUpdateDTO storeInventoryUpdateDTO = new StoreInventoryUpdateDTO();
storeInventoryUpdateDTO.setStoreId(storeInventory.getStoreId());
storeInventoryUpdateDTO.setSkuId(storeInventory.getSkuId());
// 默认当前 sku 可用库存满足待扣减数量
storeInventoryUpdateDTO.setQuantity(stock2deduct);
SkuDetatilDTO skuDetatilDTO = new SkuDetatilDTO();
skuDetatilDTO.setSkuId(sku.getId());
skuDetatilDTO.setSkuCode(sku.getSkuCode());
skuDetatilDTO.setSkuName(sku.getSkuName());
// 默认当前 sku 可用库存满足待扣减数量
skuDetatilDTO.setNum(stock2deduct);
skuDetatilDTO.setWareHouseCode(sku.getWarehouseInventoryCode());
if (stock < stock2deduct) {
storeInventoryUpdateDTO.setQuantity(stock);
skuDetatilDTO.setNum(stock);
stockDeductTotal += stock;
} else {
stockDeductTotal += stock2deduct;
}
skuDetailDTOS.add(skuDetatilDTO);
storeInventoryUpdateDTOS.add(storeInventoryUpdateDTO);
}
if (stockDeductTotal < specificationNum) {
throw new BusinessException("库存不足");
}
}
// 自旋 利用乐观锁扣减库存呢
/**
* 扣减门店库存
*
* @return true 扣减成功 false 扣减失败
*/
public boolean storeInventory(Long skuId, Long storeId, Integer quantity) {
logger.info("storeInventory 尝试扣减门店库存 skuId:{} storeId: {} quantity: {} ", skuId, storeId, quantity);
boolean res = false;
int retries = 0;
while (retries < MAX_RETRY) {
// 查询门店库存
Optional<StoreInventory> inventoryOptional = storeInventoryRepository
.findOneByStoreIdAndSkuId(storeId, skuId);
if (inventoryOptional.isEmpty()) {
return false;
}
StoreInventory inventory = inventoryOptional.get();
Integer usedOld = inventory.getAppInventoryUsed();
int usedNew = usedOld + quantity;
Integer assigned = inventory.getAppInventoryAssigned();
Long versionOld = inventory.getVersion();
long versionNew = versionOld + 1;
logger.info("storeInventory storeId:{} skuId:{} 已用:{} 已分配:{} 版本号:{}",
storeId, skuId, usedOld, assigned, versionOld);
if (usedNew <= assigned) {
logger.info("storeInventory storeId:{} skuId:{} 需扣减:{} 更新后已用:{}, 更新后版本号:{} 重试次数:{}",
storeId, skuId, quantity, usedNew, versionNew, retries);
int effected = storeInventoryRepository.updateInventory(usedNew, inventory.getId(),
versionOld, versionNew);
if (1 == effected) {
res = true;
break;
} else {
retries++;
}
} else {
logger.warn("storeInventory 库存不足 已用: {} 分配: {} 需扣减: {}", usedOld, assigned, quantity);
break;
}
}
return res;
}
/**
* 更新小程序端已用库存量
*
* @param used 已用库存量
* @param id 主键id
* @param versionOld 更新前的版本号
* @param versionNew 待更新的版本号
* @return 受影响的行数
*/
@Modifying
@Query("update StoreInventory set appInventoryUsed = ?1, version = ?4 where id = ?2 and version = ?3")
int updateInventory(int used, long id, long versionOld, long versionNew);