闸机扣费接口调整

This commit is contained in:
hejiayang
2025-10-27 18:06:42 +08:00
parent 9e7c1b4495
commit 660a713a7d
4 changed files with 198 additions and 14 deletions
@@ -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;
}
}
@@ -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<StClient
*/
List<StClientMemberLevelDeductRuleVo> selectStClientMemberLevelDeductRulePage(Page<StClientMemberLevelDeductRuleVo> page, @Param("vo") StClientMemberLevelDemotionTimeVO stClientMemberLevelDeductRule);
DateTime getMaxTimeByUserId(@Param("userId") String userId);
}
@@ -17,5 +17,8 @@
</if>
order by is_enable desc, create_time desc
</select>
<select id="getMaxTimeByUserId" resultType="cn.hutool.core.date.DateTime">
SELECT MAX(create_time) FROM st_bbzx_order_info WHERE member_id = #{userId}
</select>
</mapper>
@@ -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<S
@Autowired
private ISlBaseDinnerTimeService stBaseDinnerTimeService;
@Resource
private RedisLockUtil redisLockUtil;
@Override
@@ -232,23 +234,118 @@ public class StClientMemberLevelDeductRuleServiceImpl extends JeecgServiceImpl<S
}
// @Override
// public Result<StClientMemberSpendVO> bindUserOfflineCardCode(@RequestBody StRfidoffLineVo stRfidoffLineVo){
// String deviceCode = stRfidoffLineVo.getDeviceCode();
// String restId = stRfidoffLineVo.getRestId();
// List<StRfidVo> 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<StRfidVo> filterRecentThanOneMinute(List<StRfidVo> 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<StRfidVo> filterDuplicateTimestamp(List<StRfidVo> dataList) {
// 使用 LinkedHashMap 保留插入顺序,key=dateTimevalue=StRfidVo(重复时覆盖,仅保留最后一次出现的记录)
Map<String, StRfidVo> 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<StClientMemberSpendVO> bindUserOfflineCardCode(@RequestBody StRfidoffLineVo stRfidoffLineVo){
public Result<StClientMemberSpendVO> bindUserOfflineCardCode(StRfidoffLineVo stRfidoffLineVo) {
String deviceCode = stRfidoffLineVo.getDeviceCode();
String restId = stRfidoffLineVo.getRestId();
List<StRfidVo> 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<String, List<StRfidVo>> userListMaps = voListAll.stream().collect(Collectors.groupingBy(StRfidVo::getUserId));
for (Map.Entry<String, List<StRfidVo>> 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<StRfidVo> 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<S
* @return
*/
private Result<StClientMemberSpendVO> unifiedDeductionInterface(List<StRfidVo> 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<S
// 存储错误信息
String errorMessages = null;
//用户最后一次扣费时间
if(maxTime != null){
//转毫秒级时间戳
long timestampMs = maxTime.getTime();
voListAll = voListAll.stream().filter(vo -> Long.parseLong(vo.getDateTime()) - timestampMs > 6000)
.sorted(Comparator.comparing(a -> Long.parseLong(a.getDateTime()), Comparator.nullsLast(Comparator.naturalOrder())))
.collect(Collectors.toList());
}
// 将扣费信息按照餐次分组
Map<String, List<StRfidVo>> stringListMap = groupByMeal(voListAll, restId);
StMiniMineBalanceVo stMiniMineBalanceVoOnline = new StMiniMineBalanceVo();