Merge remote-tracking branch 'origin/master'

This commit is contained in:
SjS 2025-04-12 16:52:01 +08:00
commit 007e4eed3a
18 changed files with 381 additions and 234 deletions

View File

@ -26,4 +26,9 @@ public enum BonusStatus {
return CollectionUtils.map(BonusStatus::getStatus, WAIT_DIVIDE, DIVIDEND); return CollectionUtils.map(BonusStatus::getStatus, WAIT_DIVIDE, DIVIDEND);
} }
// 允许打款的分成状态
public static List<String> canPay() {
return CollectionUtils.map(BonusStatus::getStatus, WAIT_DIVIDE);
}
} }

View File

@ -98,6 +98,13 @@ public interface BonusService
*/ */
public int payBonus(BonusVO bonus); public int payBonus(BonusVO bonus);
/**
* 分成打款
* @param id 分成明细ID
* @return 结果
*/
public int payBonus(Long id);
/** /**
* 进行分成打款指定时间之前的分成 * 进行分成打款指定时间之前的分成
* @param time 时间 * @param time 时间

View File

@ -265,9 +265,9 @@ public class BonusServiceImpl implements BonusService
@Override @Override
public int payBonus(BonusVO bonus) { public int payBonus(BonusVO bonus) {
if (bonus == null) { ServiceUtil.assertion(bonus == null, "待打款的分成不存在");
return 0; ServiceUtil.assertion(!BonusStatus.canPay().contains(bonus.getStatus()), "ID为%s的分成当前状态不允许打款", bonus.getId());
}
Integer result = transactionTemplate.execute(status -> { Integer result = transactionTemplate.execute(status -> {
// 更新分成状态为已分成 // 更新分成状态为已分成
int pay = bonusMapper.pay(bonus.getId(), bonus.getWaitAmount(), LocalDateTime.now()); int pay = bonusMapper.pay(bonus.getId(), bonus.getWaitAmount(), LocalDateTime.now());
@ -288,6 +288,16 @@ public class BonusServiceImpl implements BonusService
return result == null ? 0 : result; return result == null ? 0 : result;
} }
@Override
public int payBonus(Long id) {
if (id == null) {
return 0;
}
BonusVO bonus = this.selectBonusById(id);
return this.payBonus(bonus);
}
@Override @Override
public boolean refundByBst(BonusBstType bstType, Long bstId, BigDecimal refundAmount, BigDecimal payAmount, String reason) { public boolean refundByBst(BonusBstType bstType, Long bstId, BigDecimal refundAmount, BigDecimal payAmount, String reason) {
if (bstType == null || bstId == null || refundAmount == null || payAmount == null) { if (bstType == null || bstId == null || refundAmount == null || payAmount == null) {

View File

@ -162,5 +162,12 @@ public interface DeviceService
*/ */
public DeviceVO selectAvaliableDevice(Long id, String sn); public DeviceVO selectAvaliableDevice(Long id, String sn);
/**
* 根据mac查询设备
* @param mac
* @return
*/
public DeviceVO selectByMac(String mac);
} }

View File

@ -18,4 +18,12 @@ public interface DeviceValidator {
*/ */
boolean canDeleteAll(List<Long> ids); boolean canDeleteAll(List<Long> ids);
/**
* 用户是否有权限访问设备的websocket链接
* @param mac
* @param userId
* @return
*/
boolean canLink(String mac, Long userId);
} }

View File

@ -465,4 +465,14 @@ public class DeviceServiceImpl implements DeviceService
} }
return this.selectOne(query); return this.selectOne(query);
} }
@Override
public DeviceVO selectByMac(String mac) {
if (StringUtils.isBlank(mac)) {
return null;
}
DeviceQuery query = new DeviceQuery();
query.setEqMac(mac);
return this.selectOne(query);
}
} }

View File

@ -8,8 +8,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.ruoyi.bst.device.domain.DeviceQuery; import com.ruoyi.bst.device.domain.DeviceQuery;
import com.ruoyi.bst.device.domain.DeviceVO;
import com.ruoyi.bst.device.mapper.DeviceMapper; import com.ruoyi.bst.device.mapper.DeviceMapper;
import com.ruoyi.bst.device.service.DeviceService;
import com.ruoyi.bst.device.service.DeviceValidator; import com.ruoyi.bst.device.service.DeviceValidator;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.collection.CollectionUtils; import com.ruoyi.common.utils.collection.CollectionUtils;
@Service @Service
@ -18,6 +21,9 @@ public class DeviceValidatorImpl implements DeviceValidator {
@Autowired @Autowired
private DeviceMapper deviceMapper; private DeviceMapper deviceMapper;
@Autowired
private DeviceService deviceService;
@Override @Override
public boolean canEdit(Long id) { public boolean canEdit(Long id) {
return canOperate(Collections.singletonList(id)); return canOperate(Collections.singletonList(id));
@ -42,4 +48,38 @@ public class DeviceValidatorImpl implements DeviceValidator {
return new HashSet<>(idList).containsAll(ids); return new HashSet<>(idList).containsAll(ids);
} }
@Override
public boolean canLink(String mac, Long userId) {
if (StringUtils.isBlank(mac) || userId == null) {
return false;
}
DeviceVO device = deviceService.selectByMac(mac);
return canView(device, userId);
}
// 是否可以查看设备
private boolean canView(DeviceVO device, Long userId) {
return isMch(device, userId) || isAreaUser(device, userId) || isOrderUser(device, userId) || isAreaAgent(device, userId);
}
// 是否是商户
private boolean isMch(DeviceVO device, Long userId) {
return device != null && device.getMchId() != null && device.getMchId().equals(userId);
}
// 是否是运营区管理员
private boolean isAreaUser(DeviceVO device, Long userId) {
return device.getAreaUserId() != null && device.getAreaUserId().equals(userId);
}
// 是否是订单用户
private boolean isOrderUser(DeviceVO device, Long userId) {
return device.getOrderUserId() != null && device.getOrderUserId().equals(userId);
}
// 是否是运营区代理
private boolean isAreaAgent(DeviceVO device, Long userId) {
return device.getAreaAgentId() != null && device.getAreaAgentId().equals(userId);
}
} }

View File

@ -87,12 +87,15 @@ public class SuitValidatorImpl implements SuitValidator {
ServiceUtil.assertion(rule.getFee() == null, "费用不能为空"); ServiceUtil.assertion(rule.getFee() == null, "费用不能为空");
ServiceUtil.assertion(rule.getStart() < 0, "区间开始时间不能小于0"); ServiceUtil.assertion(rule.getStart() < 0, "区间开始时间不能小于0");
ServiceUtil.assertion(rule.getEachUnit() <= 0, "计费间隔必须大于0"); ServiceUtil.assertion(rule.getEachUnit() <= 0, "计费间隔必须大于0");
ServiceUtil.assertion(rule.getFee().compareTo(BigDecimal.ZERO) <= 0, "费用不能小于等于0"); ServiceUtil.assertion(rule.getFee().compareTo(BigDecimal.ZERO) <= 0, "费用不能小于或者等于0");
if (i < intervalRules.size() - 1) { if (i < intervalRules.size() - 1) {
SuitIntervalFeeRule nextRule = intervalRules.get(i + 1); SuitIntervalFeeRule nextRule = intervalRules.get(i + 1);
ServiceUtil.assertion(rule.getEnd() == null, "区间结束时间不能为空"); ServiceUtil.assertion(rule.getEnd() == null, "区间结束时间不能为空");
ServiceUtil.assertion(rule.getStart() > rule.getEnd(), "区间的结束时间不允许小于开始时间"); ServiceUtil.assertion(rule.getStart() > rule.getEnd(), "区间的结束时间不允许小于开始时间");
ServiceUtil.assertion(rule.getEnd() != nextRule.getStart(), "区间的结束时间必须等于下一个区间的开始时间"); ServiceUtil.assertion(rule.getEnd() != nextRule.getStart(), "区间的结束时间必须等于下一个区间的开始时间");
int eachUnit = rule.getEachUnit();
int duration = rule.getEnd() - rule.getStart();
ServiceUtil.assertion(duration % eachUnit != 0, "区间间隔必须能够被区间整除");
} }
} catch (Exception e) { } catch (Exception e) {
throw new ServiceException("" + (i + 1) + "条收费规则校验失败:" + e.getMessage()); throw new ServiceException("" + (i + 1) + "条收费规则校验失败:" + e.getMessage());

View File

@ -1,6 +1,8 @@
package com.ruoyi.bst.suit.utils; package com.ruoyi.bst.suit.utils;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Comparator;
import java.util.List; import java.util.List;
import com.ruoyi.bst.suit.domain.SuitVO; import com.ruoyi.bst.suit.domain.SuitVO;
@ -35,15 +37,22 @@ public class SuitUtil {
return 0; return 0;
} }
long seconds = 0;
// 起步价计费 // 起步价计费
if (SuitRidingRule.START_FEE.getCode().equals(suit.getRidingRule())) { if (SuitRidingRule.START_FEE.getCode().equals(suit.getRidingRule())) {
return calcTotalTimeForStartFee(depositAmount, suit.getStartRule(), rentalUnit); seconds = calcTotalTimeForStartFee(depositAmount, suit.getStartRule(), rentalUnit);
} }
// 区间计费 // 区间计费
else if (SuitRidingRule.INTERVAL_FEE.getCode().equals(suit.getRidingRule())) { else if (SuitRidingRule.INTERVAL_FEE.getCode().equals(suit.getRidingRule())) {
return calcTotalTimeForIntervalFee(depositAmount, suit.getIntervalRule(), rentalUnit); seconds = calcTotalTimeForIntervalFee(depositAmount, suit.getIntervalRule(), rentalUnit);
} }
return 0;
// 不允许时间低于免费时长
if (suit.getFreeRideTime() != null) {
seconds = Math.max(seconds, suit.getFreeRideTime() * 60);
}
return seconds;
} }
/** /**
@ -102,252 +111,117 @@ public class SuitUtil {
} }
// 按照区间开始时间排序 // 按照区间开始时间排序
intervalFeeRules.sort((a, b) -> Integer.compare(a.getStart(), b.getStart())); intervalFeeRules.sort(Comparator.comparingInt(SuitIntervalFeeRule::getStart));
BigDecimal remainingAmount = depositAmount; BigDecimal amount = depositAmount; // 剩余金额
long totalTime = 0; int totalTime = 0; // 总时长
// 获取单位对应的秒数 // 获取单位对应的秒数
long unitSeconds = rentalUnit.getSeconds(); long unitSeconds = rentalUnit.getSeconds();
for (SuitIntervalFeeRule rule : intervalFeeRules) { for (int i = 0; i < intervalFeeRules.size(); i++) {
// 处理最后一个区间的end可能为空的情况 SuitIntervalFeeRule rule = intervalFeeRules.get(i);
if ( intervalFeeRules.indexOf(rule) == intervalFeeRules.size() - 1) { BigDecimal fee = rule.getFee(); // 每个时间段的金额
// 如果是最后一个区间且end为0可以认为是无限长 int eachUnitTime = rule.getEachUnit(); // 每个时间段的时长
// 这里我们可以计算剩余金额能买多少时间 if (fee.compareTo(BigDecimal.ZERO) <= 0 || eachUnitTime <= 0) {
BigDecimal eachUnitFee = rule.getFee();
int eachUnitTime = rule.getEachUnit();
if (eachUnitFee.compareTo(BigDecimal.ZERO) > 0 && eachUnitTime > 0) {
// 计算剩余金额可以购买的完整单位数
BigDecimal completeUnits = remainingAmount.divide(eachUnitFee, 0, BigDecimal.ROUND_DOWN);
totalTime += completeUnits.multiply(BigDecimal.valueOf(eachUnitTime * unitSeconds)).longValue();
// 计算最后不足一个计费单位的时间按比例计算
BigDecimal remainingAmountForLastUnit = remainingAmount.remainder(eachUnitFee);
if (remainingAmountForLastUnit.compareTo(BigDecimal.ZERO) > 0) {
totalTime += remainingAmountForLastUnit
.multiply(BigDecimal.valueOf(eachUnitTime * unitSeconds))
.divide(eachUnitFee, 0, BigDecimal.ROUND_DOWN)
.longValue();
}
}
break; // 处理完最后一个区间后退出循环
}
// 计算当前区间的时间范围单位
int end = rule.getEnd();
int intervalDuration = end - rule.getStart();
if (intervalDuration <= 0) {
continue; // 跳过无效区间
}
// 计算当前区间内可以购买的完整计费单位数量
BigDecimal eachUnitFee = rule.getFee();
int eachUnitTime = rule.getEachUnit();
if (eachUnitFee.compareTo(BigDecimal.ZERO) <= 0 || eachUnitTime <= 0) {
continue; // 跳过无效计费规则 continue; // 跳过无效计费规则
} }
// 计算可以购买几个区间金额不足也按1个单位计算
int count = amount.divide(fee, 0, RoundingMode.UP).intValue();
// 计算当前区间内可以购买的完整单位数 // 非最后节点限制最大购买数量
int maxUnitsInInterval = intervalDuration / eachUnitTime; if (i < intervalFeeRules.size() - 1) {
BigDecimal maxCostForInterval = eachUnitFee.multiply(BigDecimal.valueOf(maxUnitsInInterval)); int duration =rule.getEnd() - rule.getStart();
// 计算最大金额
// 如果剩余金额足够支付整个区间 int maxCount = BigDecimal.valueOf(duration).divide(BigDecimal.valueOf(eachUnitTime), 0, RoundingMode.UP).intValue();
if (remainingAmount.compareTo(maxCostForInterval) >= 0) { count = Math.min(count, maxCount);
totalTime += intervalDuration * unitSeconds;
remainingAmount = remainingAmount.subtract(maxCostForInterval);
} else {
// 计算剩余金额可以购买的完整单位数
BigDecimal completeUnits = remainingAmount.divide(eachUnitFee, 0, BigDecimal.ROUND_DOWN);
totalTime += completeUnits.multiply(BigDecimal.valueOf(eachUnitTime * unitSeconds)).longValue();
// 计算最后不足一个计费单位的时间按比例计算
BigDecimal remainingAmountForLastUnit = remainingAmount.remainder(eachUnitFee);
if (remainingAmountForLastUnit.compareTo(BigDecimal.ZERO) > 0) {
totalTime += remainingAmountForLastUnit
.multiply(BigDecimal.valueOf(eachUnitTime * unitSeconds))
.divide(eachUnitFee, 0, BigDecimal.ROUND_DOWN)
.longValue();
} }
// 已经用完所有金额退出循环 // 记入时长并扣减金额
totalTime += count * eachUnitTime;
amount = amount.subtract(fee.multiply(BigDecimal.valueOf(count)));
// 金额不足则跳出循环
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
break; break;
} }
} }
return totalTime; return totalTime * unitSeconds;
}
/**
* 计算已使用时长所需的金额
* @param suit 套餐
* @param usedTime 已使用时长
* @return 所需金额
*/
public static BigDecimal calcAmount(SuitVO suit, long usedTime) {
if (suit == null || usedTime <= 0) {
return BigDecimal.ZERO;
}
// 租赁单位
SuitRentalUnit rentalUnit = SuitRentalUnit.parse(suit.getRentalUnit());
if (rentalUnit == null) {
return BigDecimal.ZERO;
}
// 起步价计费
if (SuitRidingRule.START_FEE.getCode().equals(suit.getRidingRule())) {
return calcAmountForStartFee(usedTime, suit.getStartRule(), rentalUnit);
}
// 区间计费
else if (SuitRidingRule.INTERVAL_FEE.getCode().equals(suit.getRidingRule())) {
return calcAmountForIntervalFee(usedTime, suit.getIntervalRule(), rentalUnit);
}
return BigDecimal.ZERO;
} }
/** /**
* 计算起步价计费所需金额 * 计算起步价计费所需金额
* @param usedTime 已使用时长 * @param seconds 已使用时长
* @param startRule 起步价计费规则 * @param rule 起步价计费规则
* @param rentalUnit 租赁单位 * @param rentalUnit 租赁单位
* @return 所需金额 * @return 所需金额
*/ */
public static BigDecimal calcAmountForStartFee(long usedTime, SuitStartFeeRule startRule, SuitRentalUnit rentalUnit) { public static BigDecimal calcAmountForStartFee(long seconds, SuitStartFeeRule rule, SuitRentalUnit rentalUnit) {
if (usedTime <= 0 || startRule == null || rentalUnit == null) { if (seconds <= 0 || rule == null || rentalUnit == null) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
// 获取单位对应的秒数 // 获取单位对应的秒数
long unitSeconds = rentalUnit.getSeconds(); long unitSeconds = rentalUnit.getSeconds();
// 将秒转换为计费单位 BigDecimal startingPrice = rule.getStartingPrice(); // 起步价
BigDecimal usedTimeInUnit = BigDecimal.valueOf(usedTime).divide(BigDecimal.valueOf(unitSeconds), 0, BigDecimal.ROUND_UP); long startSeconds = rule.getStartingTime() * unitSeconds; // 起步时长
// 起步价和起步时长 // 如果使用时长小于等于起步时长返回起步价费用
BigDecimal startingPrice = startRule.getStartingPrice(); if (seconds <= startSeconds) {
int startingTime = startRule.getStartingTime(); return rule.getStartingPrice();
// 如果使用时长小于等于起步时长按比例计算费用
if (usedTimeInUnit.intValue() <= startingTime) {
return startingPrice.multiply(usedTimeInUnit).divide(BigDecimal.valueOf(startingTime), 2, BigDecimal.ROUND_UP);
} }
// 超出起步时长的部分 // 超出起步的时长
BigDecimal extraTime = usedTimeInUnit.subtract(BigDecimal.valueOf(startingTime)); long extraTime = seconds - startSeconds; // 超出的时长
long outTime = rule.getTimeoutTime() * unitSeconds; // 超出时长周期
long extraRange = extraTime / outTime + (extraTime % outTime > 0 ? 1 : 0); // 超出的周期
BigDecimal extraFee = MathUtils.mulDecimal(rule.getTimeoutPrice(), BigDecimal.valueOf(extraRange));
// 超时费用和超时时长 // 总费用 = 起步价 + 超出费用
BigDecimal timeoutPrice = startRule.getTimeoutPrice(); return MathUtils.addDecimal(startingPrice, extraFee);
int timeoutTime = startRule.getTimeoutTime();
// 计算完整的超时周期数
BigDecimal fullTimeoutPeriods = extraTime.divide(BigDecimal.valueOf(timeoutTime), 0, BigDecimal.ROUND_DOWN);
// 计算最后不足一个超时周期的部分
BigDecimal remainingTime = extraTime.remainder(BigDecimal.valueOf(timeoutTime));
BigDecimal lastPeriodFee = BigDecimal.ZERO;
if (remainingTime.compareTo(BigDecimal.ZERO) > 0) {
lastPeriodFee = timeoutPrice.multiply(remainingTime).divide(BigDecimal.valueOf(timeoutTime), 2, BigDecimal.ROUND_UP);
}
// 总费用 = 起步价 + 完整超时周期费用 + 最后一个不完整周期费用
return startingPrice
.add(fullTimeoutPeriods.multiply(timeoutPrice))
.add(lastPeriodFee);
} }
/** /**
* 计算区间计费所需金额 * 计算区间计费所需金额
* @param usedTime 已使用时长 * @param seconds 已使用时长
* @param intervalFeeRules 区间计费规则列表 * @param rules 区间计费规则列表
* @param rentalUnit 租赁单位 * @param rentalUnit 租赁单位
* @return 所需金额 * @return 所需金额
*/ */
public static BigDecimal calcAmountForIntervalFee(long usedTime, List<SuitIntervalFeeRule> intervalFeeRules, SuitRentalUnit rentalUnit) { public static BigDecimal calcAmountForIntervalFee(long seconds, List<SuitIntervalFeeRule> rules, SuitRentalUnit rentalUnit) {
if (usedTime <= 0 || CollectionUtils.isEmptyElement(intervalFeeRules) || rentalUnit == null) { if (seconds <= 0 || CollectionUtils.isEmptyElement(rules) || rentalUnit == null) {
return BigDecimal.ZERO; return BigDecimal.ZERO;
} }
// 获取单位对应的秒数 // 获取单位对应的秒数
long unitSeconds = rentalUnit.getSeconds(); long unitSeconds = rentalUnit.getSeconds();
// 将秒转换为计费单位
int usedTimeInUnit = (int) Math.ceil((double) usedTime / unitSeconds);
// 按照区间开始时间排序 // 按照区间开始时间排序
intervalFeeRules.sort((a, b) -> Integer.compare(a.getStart(), b.getStart())); rules.sort(Comparator.comparingInt(SuitIntervalFeeRule::getStart));
// 总金额
BigDecimal totalAmount = BigDecimal.ZERO; BigDecimal totalAmount = BigDecimal.ZERO;
int remainingTime = usedTimeInUnit;
for (SuitIntervalFeeRule rule : intervalFeeRules) { for (int i = 0; i < rules.size(); i++) {
// 如果没有剩余时间需要计费退出循环 SuitIntervalFeeRule rule = rules.get(i);
if (remainingTime <= 0) { long end = seconds;
if (i < rules.size() - 1) {
end = Math.min(end, rule.getEnd() * unitSeconds); // 取区间最小的终值
}
// 计算价格并扣减剩余时长
long duration = end - rule.getStart() * unitSeconds; // 区间使用时长
long eachUnit = rule.getEachUnit() * unitSeconds; // 间隔
long range = duration / eachUnit + (duration % eachUnit > 0 ? 1 : 0); // 当前区间使用的周期不满一个周期也按一个周期算
BigDecimal partAmount = MathUtils.mulDecimal(rule.getFee(), BigDecimal.valueOf(range));
totalAmount = MathUtils.addDecimal(totalAmount, partAmount);
// 当前已使用时长大于等于最大时长跳出循环
if (end >= seconds) {
break; break;
} }
// 处理最后一个区间的end可能为0的情况
boolean isLastInterval = intervalFeeRules.indexOf(rule) == intervalFeeRules.size() - 1;
if (isLastInterval) {
// 最后一个区间且end为0表示无限长
BigDecimal eachUnitFee = rule.getFee();
int eachUnitTime = rule.getEachUnit();
if (eachUnitFee.compareTo(BigDecimal.ZERO) > 0 && eachUnitTime > 0) {
// 计算完整单位数
int completeUnits = remainingTime / eachUnitTime;
totalAmount = totalAmount.add(eachUnitFee.multiply(BigDecimal.valueOf(completeUnits)));
// 计算最后不足一个单位的费用
int remainingUnitTime = remainingTime % eachUnitTime;
if (remainingUnitTime > 0) {
totalAmount = totalAmount.add(
eachUnitFee.multiply(BigDecimal.valueOf(remainingUnitTime))
.divide(BigDecimal.valueOf(eachUnitTime), 2, BigDecimal.ROUND_UP)
);
}
}
remainingTime = 0;
break;
}
// 计算当前区间的时间范围
int end = rule.getEnd();
int intervalDuration = end - rule.getStart();
if (intervalDuration <= 0) {
continue; // 跳过无效区间
}
// 计算在当前区间内的使用时长
int timeInCurrentInterval = Math.min(remainingTime, intervalDuration);
// 计算当前区间内的费用
BigDecimal eachUnitFee = rule.getFee();
int eachUnitTime = rule.getEachUnit();
if (eachUnitFee.compareTo(BigDecimal.ZERO) <= 0 || eachUnitTime <= 0) {
continue; // 跳过无效计费规则
}
// 计算完整单位数
int completeUnits = timeInCurrentInterval / eachUnitTime;
totalAmount = totalAmount.add(eachUnitFee.multiply(BigDecimal.valueOf(completeUnits)));
// 计算最后不足一个单位的费用
int remainingUnitTime = timeInCurrentInterval % eachUnitTime;
if (remainingUnitTime > 0) {
totalAmount = totalAmount.add(
eachUnitFee.multiply(BigDecimal.valueOf(remainingUnitTime))
.divide(BigDecimal.valueOf(eachUnitTime), 2, BigDecimal.ROUND_UP)
);
}
// 更新剩余需要计费的时间
remainingTime -= timeInCurrentInterval;
} }
return totalAmount; return totalAmount;

View File

@ -5,6 +5,7 @@ import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import com.ruoyi.ws.service.DeviceWebSocketService;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@ -63,6 +64,9 @@ public class IotReceiveServiceImpl implements IotReceiveService {
@Autowired @Autowired
private DeviceIotService deviceIotService; private DeviceIotService deviceIotService;
@Autowired
private DeviceWebSocketService deviceWebSocketService;
@Override @Override
public void handleReceive(ReceiveMsg msg) { public void handleReceive(ReceiveMsg msg) {
if (msg == null) { if (msg == null) {
@ -222,5 +226,8 @@ public class IotReceiveServiceImpl implements IotReceiveService {
// 暂存到Redis缓存 // 暂存到Redis缓存
redisCache.rightPush(CacheConstants.LOCATION_LOG_QUEUE, po); redisCache.rightPush(CacheConstants.LOCATION_LOG_QUEUE, po);
// 发送消息给ws服务
deviceWebSocketService.sendMessageToDevice(po, device.getMac());
} }
} }

View File

@ -0,0 +1,147 @@
package com.ruoyi.ws.service;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.Session;
import org.springframework.stereotype.Service;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
public class DeviceWebSocketService {
// MAC地址到Session集合的映射
private static final Map<String, Set<Session>> MAC_TO_SESSIONS = new ConcurrentHashMap<>();
/**
* 添加设备WebSocket会话的MAC地址
*/
public void addMacToSession(String mac, Session session) {
if (mac == null || session == null) {
return;
}
// 添加MAC到Session的映射
Set<Session> sessions = MAC_TO_SESSIONS.computeIfAbsent(mac, k -> new HashSet<>());
synchronized (sessions) {
sessions.add(session);
}
log.info("Session添加MAC地址{}成功当前Session关联的MAC地址数量{}", mac, sessions.size());
}
/**
* 从Session中移除指定的MAC地址
*/
public void removeMacFromSession(String mac, Session session) {
if (mac == null || session == null) {
return;
}
// 从MAC到Session的映射中移除
Set<Session> sessions = MAC_TO_SESSIONS.get(mac);
if (sessions != null) {
synchronized (sessions) {
sessions.remove(session);
if (sessions.isEmpty()) {
MAC_TO_SESSIONS.remove(mac);
}
}
}
log.info("从Session中移除MAC地址{}成功", mac);
}
/**
* 移除设备WebSocket会话
*/
public void removeSession(Session session) {
if (session == null) {
return;
}
// 遍历所有MAC地址,移除对应的session
for (Map.Entry<String, Set<Session>> entry : MAC_TO_SESSIONS.entrySet()) {
String mac = entry.getKey();
Set<Session> sessions = entry.getValue();
if (sessions != null) {
synchronized (sessions) {
sessions.remove(session);
if (sessions.isEmpty()) {
MAC_TO_SESSIONS.remove(mac);
}
}
}
}
log.info("WebSocket会话已移除");
}
/**
* 获取MAC地址关联的所有WebSocket会话
*/
public Set<Session> getSessions(String mac) {
if (mac == null) {
return new HashSet<>();
}
Set<Session> sessions = MAC_TO_SESSIONS.get(mac);
if (sessions == null) {
return new HashSet<>();
}
synchronized (sessions) {
return new HashSet<>(sessions);
}
}
/**
* 向指定MAC地址发送消息
*
* @param message 消息内容
* @param macs 目标MAC地址数组
*/
public void sendMessageToDevice(Object message, String... macs) {
if (message == null) {
return;
}
this.sendMessageToDevice(message, Arrays.asList(macs));
}
/**
* 向指定MAC地址发送消息
*
* @param message 消息内容
* @param macs 目标MAC地址集合
*/
public void sendMessageToDevice(Object message, Collection<String> macs) {
if (message == null) {
return;
}
String msg = JSON.toJSONString(message);
for (String mac : macs) {
Set<Session> sessions = this.getSessions(mac);
for (Session session : sessions) {
if (session != null) {
try {
if (session.isOpen()) {
session.getBasicRemote().sendText(msg);
log.info("向MAC地址{}发送消息成功: {}", mac, msg);
}
} catch (Exception e) {
log.error("发送消息给MAC地址{}失败", mac, e);
}
}
}
}
}
}

View File

@ -12,11 +12,9 @@ import java.util.concurrent.Executors;
import javax.websocket.Session; import javax.websocket.Session;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.ruoyi.system.user.service.UserService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -25,10 +23,7 @@ import lombok.extern.slf4j.Slf4j;
*/ */
@Service @Service
@Slf4j @Slf4j
public class WebSocketService { public class UserWebSocketService {
@Autowired
private UserService userService;
// 使用ConcurrentHashMap存储会话 // 使用ConcurrentHashMap存储会话
private static final Map<Long, Session> SESSION_POOL = new ConcurrentHashMap<>(); private static final Map<Long, Session> SESSION_POOL = new ConcurrentHashMap<>();

View File

@ -131,7 +131,7 @@ public class AppOrderController extends BaseController {
@ApiOperation("操作订单设备关闭") @ApiOperation("操作订单设备关闭")
@PutMapping("/closeDevice") @PutMapping("/closeDevice")
public AjaxResult closeDevice(@RequestBody @Validated OrderCloseDeviceDTO dto) { public AjaxResult closeDevice(@Validated OrderCloseDeviceDTO dto) {
OrderVO order = orderService.selectOrderById(dto.getOrderId()); OrderVO order = orderService.selectOrderById(dto.getOrderId());
ServiceUtil.assertion(order == null, "订单不存在"); ServiceUtil.assertion(order == null, "订单不存在");
ServiceUtil.assertion(!orderValidator.canCloseDevice(order, getUserId()), "您无权操作ID为%s的订单设备关闭", order.getId()); ServiceUtil.assertion(!orderValidator.canCloseDevice(order, getUserId()), "您无权操作ID为%s的订单设备关闭", order.getId());

View File

@ -9,6 +9,7 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@ -82,4 +83,14 @@ public class BonusController extends BaseController
return success(bonusService.preview(deviceId)); return success(bonusService.preview(deviceId));
} }
/**
* 支付分成
*/
@PreAuthorize("@ss.hasPermi('bst:bonus:pay')")
@PutMapping("/pay")
@Log(title = "支付分成", businessType = BusinessType.OTHER)
public AjaxResult pay(Long id) {
return success(bonusService.payBonus(id));
}
} }

View File

@ -4,6 +4,9 @@ import java.util.List;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import com.ruoyi.bst.suit.domain.enums.SuitRentalUnit;
import com.ruoyi.bst.suit.domain.enums.SuitRidingRule;
import com.ruoyi.bst.suit.utils.SuitUtil;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@ -144,4 +147,16 @@ public class SuitController extends BaseController
{ {
return toAjax(suitService.deleteSuitByIds(ids)); return toAjax(suitService.deleteSuitByIds(ids));
} }
// @GetMapping("/test")
// public AjaxResult test(Long seconds, Long suitId)
// {
// SuitVO suit = suitService.selectSuitById(suitId);
// SuitRentalUnit rentalUnit = SuitRentalUnit.parse(suit.getRentalUnit());
// if (SuitRidingRule.INTERVAL_FEE.getCode().equals(suit.getRidingRule())) {
// return success(SuitUtil.calcAmountForIntervalFee(seconds, suit.getIntervalRule(), rentalUnit));
// } else {
// return success(SuitUtil.calcAmountForStartFee(seconds, suit.getStartRule(), rentalUnit));
// }
// }
} }

View File

@ -8,24 +8,26 @@ import javax.websocket.OnOpen;
import javax.websocket.Session; import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint; import javax.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.ruoyi.bst.device.service.DeviceValidator;
import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.framework.web.service.TokenService; import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.ws.config.WebSocketAuthConfigurator; import com.ruoyi.ws.config.WebSocketAuthConfigurator;
import com.ruoyi.ws.service.WebSocketService; import com.ruoyi.ws.service.DeviceWebSocketService;
import lombok.extern.slf4j.Slf4j;
/** /**
* WebSocket服务类 * WebSocket服务类
*/ */
@ServerEndpoint(value = "/ws/message", configurator = WebSocketAuthConfigurator.class) @ServerEndpoint(value = "/ws/device", configurator = WebSocketAuthConfigurator.class)
@Component @Component
@Slf4j @Slf4j
public class MessageWebSocket { public class DeviceWebSocket {
/** /**
* 建立连接 * 建立连接
*/ */
@ -33,15 +35,24 @@ public class MessageWebSocket {
public void onOpen(Session session, EndpointConfig config) { public void onOpen(Session session, EndpointConfig config) {
try { try {
String token = (String) config.getUserProperties().get("token"); String token = (String) config.getUserProperties().get("token");
String mac = (String) config.getUserProperties().get("mac");
if (token != null) { if (token != null) {
TokenService tokenService = SpringUtils.getBean(TokenService.class); TokenService tokenService = SpringUtils.getBean(TokenService.class);
WebSocketService webSocketService = SpringUtils.getBean(WebSocketService.class); DeviceWebSocketService webSocketService = SpringUtils.getBean(DeviceWebSocketService.class);
DeviceValidator deviceValidator = SpringUtils.getBean(DeviceValidator.class);
LoginUser loginUser = tokenService.getLoginUser(token); LoginUser loginUser = tokenService.getLoginUser(token);
tokenService.verifyToken(loginUser); tokenService.verifyToken(loginUser);
if (loginUser != null) { if (loginUser != null) {
Long userId = loginUser.getUserId(); Long userId = loginUser.getUserId();
webSocketService.addSession(userId, session); // 校验用户是否有权限连接设备
log.info("用户{}连接WebSocket成功", userId); if (!deviceValidator.canLink(mac, userId)) {
String reason = String.format("用户%s无权访问%s的数据", userId, mac);
session.close(new CloseReason(CloseReason.CloseCodes.VIOLATED_POLICY, reason));
return;
}
// 添加设备WebSocket会话的MAC地址
webSocketService.addMacToSession(mac, session);
log.info("用户 {} 连接Mac {} 成功", userId, mac);
return; return;
} }
} }
@ -55,15 +66,11 @@ public class MessageWebSocket {
* 关闭连接 * 关闭连接
*/ */
@OnClose @OnClose
public void onClose() { public void onClose(Session session) {
try { try {
LoginUser loginUser = SecurityUtils.getLoginUser(); DeviceWebSocketService webSocketService = SpringUtils.getBean(DeviceWebSocketService.class);
if (loginUser != null) { webSocketService.removeSession(session);
Long userId = loginUser.getUserId(); log.info("用户{}断开WebSocket连接", session);
WebSocketService webSocketService = SpringUtils.getBean(WebSocketService.class);
webSocketService.removeSession(userId);
log.info("用户{}断开WebSocket连接", userId);
}
} catch (Exception e) { } catch (Exception e) {
log.error("WebSocket关闭异常", e); log.error("WebSocket关闭异常", e);
} }

View File

@ -10,10 +10,11 @@ import javax.websocket.server.ServerEndpointConfig;
public class WebSocketAuthConfigurator extends ServerEndpointConfig.Configurator { public class WebSocketAuthConfigurator extends ServerEndpointConfig.Configurator {
@Override @Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
String token = null;
if (!request.getParameterMap().isEmpty()) { if (!request.getParameterMap().isEmpty()) {
token = request.getParameterMap().get("token").get(0); String token = request.getParameterMap().get("token").get(0);
} String mac = request.getParameterMap().get("mac").get(0);
sec.getUserProperties().put("token", token); sec.getUserProperties().put("token", token);
sec.getUserProperties().put("mac", mac);
}
} }
} }