diff --git a/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/RedisLockUtil.java b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/RedisLockUtil.java new file mode 100644 index 0000000..ad3f020 --- /dev/null +++ b/jeecg-boot-base-core/src/main/java/org/jeecg/common/util/RedisLockUtil.java @@ -0,0 +1,73 @@ +package org.jeecg.common.util; + +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.UUID; +import java.util.concurrent.TimeUnit; + +@Component +public class RedisLockUtil { + + private final StringRedisTemplate stringRedisTemplate; + + // 锁的默认过期时间:1分钟 + private static final long DEFAULT_LOCK_EXPIRE = 60; + // 锁的前缀,避免键名冲突 + private static final String LOCK_PREFIX = "food:lock:"; + + public RedisLockUtil(StringRedisTemplate stringRedisTemplate) { + this.stringRedisTemplate = stringRedisTemplate; + } + + /** + * 获取分布式锁 + * @param key 业务键(这里使用菜品ID) + * @return 锁标识(释放锁时需要),null表示获取锁失败 + */ + public String tryLock(String key) { + return tryLock(key, DEFAULT_LOCK_EXPIRE, TimeUnit.SECONDS); + } + + /** + * 获取分布式锁 + * @param key 业务键 + * @param expire 过期时间 + * @param timeUnit 时间单位 + * @return 锁标识,null表示获取锁失败 + */ + public String tryLock(String key, long expire, TimeUnit timeUnit) { + // 生成唯一标识,用于释放锁时的验证 + String lockValue = UUID.randomUUID().toString(); + String lockKey = LOCK_PREFIX + key; + + // 使用setIfAbsent实现分布式锁,原子操作 + Boolean success = stringRedisTemplate.opsForValue() + .setIfAbsent(lockKey, lockValue, expire, timeUnit); + + return Boolean.TRUE.equals(success) ? lockValue : null; + } + + /** + * 释放分布式锁 + * @param key 业务键 + * @param lockValue 获取锁时返回的标识 + * @return 是否释放成功 + */ + public boolean releaseLock(String key, String lockValue) { + if (lockValue == null) { + return false; + } + + String lockKey = LOCK_PREFIX + key; + String currentValue = stringRedisTemplate.opsForValue().get(lockKey); + + // 验证是否是自己持有的锁 + if (lockValue.equals(currentValue)) { + // 执行删除操作 + return Boolean.TRUE.equals(stringRedisTemplate.delete(lockKey)); + } + + return false; + } +} diff --git a/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/mapper/StClientMemberLevelDeductRuleMapper.java b/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/mapper/StClientMemberLevelDeductRuleMapper.java index 63a5b94..fe0c813 100644 --- a/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/mapper/StClientMemberLevelDeductRuleMapper.java +++ b/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/mapper/StClientMemberLevelDeductRuleMapper.java @@ -3,6 +3,7 @@ */ package org.jeecg.payment.mapper; +import cn.hutool.core.date.DateTime; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.apache.ibatis.annotations.Param; @@ -24,4 +25,5 @@ public interface StClientMemberLevelDeductRuleMapper extends BaseMapper selectStClientMemberLevelDeductRulePage(Page page, @Param("vo") StClientMemberLevelDemotionTimeVO stClientMemberLevelDeductRule); + DateTime getMaxTimeByUserId(@Param("userId") String userId); } diff --git a/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/mapper/xml/StClientMemberLevelDeductRuleMapper.xml b/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/mapper/xml/StClientMemberLevelDeductRuleMapper.xml index a6aedd3..cb9a7cc 100644 --- a/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/mapper/xml/StClientMemberLevelDeductRuleMapper.xml +++ b/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/mapper/xml/StClientMemberLevelDeductRuleMapper.xml @@ -17,5 +17,8 @@ order by is_enable desc, create_time desc + diff --git a/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/service/impl/StClientMemberLevelDeductRuleServiceImpl.java b/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/service/impl/StClientMemberLevelDeductRuleServiceImpl.java index c22232a..c404b78 100644 --- a/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/service/impl/StClientMemberLevelDeductRuleServiceImpl.java +++ b/jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/payment/service/impl/StClientMemberLevelDeductRuleServiceImpl.java @@ -16,6 +16,8 @@ import org.apache.commons.lang3.StringUtils; import org.jeecg.common.api.vo.Result; import org.jeecg.common.system.base.service.impl.JeecgServiceImpl; import org.jeecg.common.util.DateUtils; +import org.jeecg.common.util.RedisLockUtil; +import org.jeecg.common.util.RedisUtil; import org.jeecg.payment.entity.StBbzxOrderInfo; import org.jeecg.payment.entity.StClientMemberLevelDeductRule; import org.jeecg.payment.enums.DeductionTypeEnum; @@ -37,9 +39,7 @@ import org.springframework.web.bind.annotation.RequestBody; import javax.annotation.Resource; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.Date; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; @@ -61,6 +61,8 @@ public class StClientMemberLevelDeductRuleServiceImpl extends JeecgServiceImpl bindUserOfflineCardCode(@RequestBody StRfidoffLineVo stRfidoffLineVo){ +// String deviceCode = stRfidoffLineVo.getDeviceCode(); +// String restId = stRfidoffLineVo.getRestId(); +// List voListAll = stRfidoffLineVo.getVoList(); +// if(CollectionUtils.isEmpty(voListAll)){ +// return Result.error("扣费信息不能为空"); +// } +// //扣费规则(扣费类型(1:固定扣费,2:固定+补贴扣费,3:按量扣费,4:自定义金额)) +// StClientMemberLevelDeductRuleVo levelDeductRuleVo = this.getDeductRuleByCanteenId(restId); +// if(voListAll.size() > 1){//离线扣费 +// //加异步处理 +// CompletableFuture.runAsync(() -> { +// unifiedDeductionInterface(voListAll,restId,deviceCode,levelDeductRuleVo,false); +// }); +// } else {//在线扣费 +// return unifiedDeductionInterface(voListAll,restId,deviceCode,levelDeductRuleVo,true); +// } +// return Result.OK("扣费成功"); +// } + /** + * 过滤与当前时间相差小于1分钟的记录 + */ + private static List filterRecentThanOneMinute(List dataList,long maxTime) { + long oneMinuteMs = 60 * 1000; // 1分钟 = 60000毫秒 + + return dataList.stream() + .filter(vo -> { + // 跳过 dateTime 为空或格式错误的记录(视为无效数据,直接过滤) + if (StringUtils.isBlank(vo.getDateTime())) { + return false; + } + try { + long recordTime = Long.parseLong(vo.getDateTime()); // 记录时间戳(毫秒) + long timeDiff = maxTime - recordTime; // 时间差(当前时间 - 记录时间) + // 保留:时间差 >= 1分钟(或记录时间在当前时间之后,避免未来时间戳) + return timeDiff >= oneMinuteMs || recordTime > maxTime; + } catch (NumberFormatException e) { + // dateTime 不是有效数字(如非时间戳字符串),视为无效数据 + return false; + } + }) + .collect(Collectors.toList()); + } + + /** + * 移除时间戳(dateTime)重复的记录(保留第一条) + */ + private static List filterDuplicateTimestamp(List dataList) { + // 使用 LinkedHashMap 保留插入顺序,key=dateTime,value=StRfidVo(重复时覆盖,仅保留最后一次出现的记录) + Map uniqueMap = new LinkedHashMap<>(); + for (StRfidVo vo : dataList) { + String dateTime = vo.getDateTime(); + if (StringUtils.isNotBlank(dateTime)) { // 仅对 dateTime 非空的记录去重 + uniqueMap.put(dateTime, vo); + } else { + // dateTime 为空的记录直接保留(已通过步骤1过滤无效数据,此处可忽略) + uniqueMap.put(UUID.randomUUID().toString(), vo); // 用UUID避免key冲突 + } + } + // 转换为 List 并返回 + return new ArrayList<>(uniqueMap.values()); + } + + /** + * 食堂扣费主接口 + */ @Override - public Result bindUserOfflineCardCode(@RequestBody StRfidoffLineVo stRfidoffLineVo){ + public Result bindUserOfflineCardCode(StRfidoffLineVo stRfidoffLineVo) { String deviceCode = stRfidoffLineVo.getDeviceCode(); String restId = stRfidoffLineVo.getRestId(); List voListAll = stRfidoffLineVo.getVoList(); if(CollectionUtils.isEmpty(voListAll)){ return Result.error("扣费信息不能为空"); } - //扣费规则(扣费类型(1:固定扣费,2:固定+补贴扣费,3:按量扣费,4:自定义金额)) - StClientMemberLevelDeductRuleVo levelDeductRuleVo = this.getDeductRuleByCanteenId(restId); - if(voListAll.size() > 1){//离线扣费 - //加异步处理 - CompletableFuture.runAsync(() -> { - unifiedDeductionInterface(voListAll,restId,deviceCode,levelDeductRuleVo,false); - }); - } else {//在线扣费 - return unifiedDeductionInterface(voListAll,restId,deviceCode,levelDeductRuleVo,true); + //去重 + voListAll = filterDuplicateTimestamp(voListAll); + //过滤时间小于1分钟的记录 + voListAll = filterRecentThanOneMinute(voListAll,System.currentTimeMillis()); + + Map> userListMaps = voListAll.stream().collect(Collectors.groupingBy(StRfidVo::getUserId)); + for (Map.Entry> userListMap : userListMaps.entrySet()) { + String userId = userListMap.getKey(); + // 尝试获取锁,超时时间1分钟 + String lockValue = redisLockUtil.tryLock(userId); + if (lockValue == null) { + return null; + } + try { + //根据用户获取之后一次扣费时间 + DateTime maxTime = baseMapper.getMaxTimeByUserId(userId); + + if(CollectionUtils.isEmpty(voListAll)){ + return Result.error("扣费信息不能为空"); + } + //扣费规则(扣费类型(1:固定扣费,2:固定+补贴扣费,3:按量扣费,4:自定义金额)) + StClientMemberLevelDeductRuleVo levelDeductRuleVo = this.getDeductRuleByCanteenId(restId); + if(voListAll.size() > 1){//离线扣费 + //加异步处理 + List finalVoListAll = voListAll; + CompletableFuture.runAsync(() -> { + unifiedDeductionInterface(finalVoListAll,restId,deviceCode,levelDeductRuleVo,false,maxTime); + }); + } else {//在线扣费 + return unifiedDeductionInterface(voListAll,restId,deviceCode,levelDeductRuleVo,true,maxTime); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + //释放锁 + redisLockUtil.releaseLock(userId, lockValue); + } } return Result.OK("扣费成功"); } @@ -263,7 +360,7 @@ public class StClientMemberLevelDeductRuleServiceImpl extends JeecgServiceImpl unifiedDeductionInterface(List voListAll, String restId, String deviceCode, - StClientMemberLevelDeductRuleVo levelDeductRuleVo, Boolean online) { + StClientMemberLevelDeductRuleVo levelDeductRuleVo, Boolean online,DateTime maxTime) { long start = System.currentTimeMillis(); //声明在线扣费余额信息 StClientMemberSpendVO stClientMemberSpendVO = new StClientMemberSpendVO(); @@ -272,6 +369,15 @@ public class StClientMemberLevelDeductRuleServiceImpl extends JeecgServiceImpl Long.parseLong(vo.getDateTime()) - timestampMs > 6000) + .sorted(Comparator.comparing(a -> Long.parseLong(a.getDateTime()), Comparator.nullsLast(Comparator.naturalOrder()))) + .collect(Collectors.toList()); + } + // 将扣费信息按照餐次分组 Map> stringListMap = groupByMeal(voListAll, restId); StMiniMineBalanceVo stMiniMineBalanceVoOnline = new StMiniMineBalanceVo();