开发记录--会员节

礼品券

会员节活动,针对小程序会员发放礼品券,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);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值