From 95aedfeb9cc142e50264606228aa21d978b7d621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A3=B7=E5=8F=B6?= <14103883+leaf-phos@user.noreply.gitee.com> Date: Sat, 12 Apr 2025 16:15:50 +0800 Subject: [PATCH] =?UTF-8?q?ws=E3=80=81=E5=A5=97=E9=A4=90=E9=87=91=E9=A2=9D?= =?UTF-8?q?=E8=AE=A1=E7=AE=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../bst/bonus/domain/enums/BonusStatus.java | 5 + .../ruoyi/bst/bonus/service/BonusService.java | 7 + .../bonus/service/impl/BonusServiceImpl.java | 16 +- .../ruoyi/bst/device/mapper/DeviceMapper.xml | 8 +- .../bst/device/service/DeviceService.java | 7 + .../bst/device/service/DeviceValidator.java | 8 + .../service/impl/DeviceServiceImpl.java | 10 + .../service/impl/DeviceValidatorImpl.java | 40 +++ .../suit/service/impl/SuitValidatorImpl.java | 5 +- .../com/ruoyi/bst/suit/utils/SuitUtil.java | 272 +++++------------- .../service/impl/IotReceiveServiceImpl.java | 7 + .../ws/service/DeviceWebSocketService.java | 147 ++++++++++ ...Service.java => UserWebSocketService.java} | 7 +- .../com/ruoyi/web/app/AppOrderController.java | 2 +- .../com/ruoyi/web/bst/BonusController.java | 11 + .../com/ruoyi/web/bst/SuitController.java | 15 + ...ageWebSocket.java => DeviceWebSocket.java} | 39 +-- .../ws/config/WebSocketAuthConfigurator.java | 9 +- 18 files changed, 381 insertions(+), 234 deletions(-) create mode 100644 ruoyi-service/src/main/java/com/ruoyi/ws/service/DeviceWebSocketService.java rename ruoyi-service/src/main/java/com/ruoyi/ws/service/{WebSocketService.java => UserWebSocketService.java} (96%) rename ruoyi-web/src/main/java/com/ruoyi/ws/{MessageWebSocket.java => DeviceWebSocket.java} (56%) diff --git a/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/domain/enums/BonusStatus.java b/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/domain/enums/BonusStatus.java index b03ff9c..046690d 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/domain/enums/BonusStatus.java +++ b/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/domain/enums/BonusStatus.java @@ -26,4 +26,9 @@ public enum BonusStatus { return CollectionUtils.map(BonusStatus::getStatus, WAIT_DIVIDE, DIVIDEND); } + // 允许打款的分成状态 + public static List canPay() { + return CollectionUtils.map(BonusStatus::getStatus, WAIT_DIVIDE); + } + } diff --git a/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/service/BonusService.java b/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/service/BonusService.java index 3f32023..1b734d9 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/service/BonusService.java +++ b/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/service/BonusService.java @@ -98,6 +98,13 @@ public interface BonusService */ public int payBonus(BonusVO bonus); + /** + * 分成打款 + * @param id 分成明细ID + * @return 结果 + */ + public int payBonus(Long id); + /** * 进行分成打款指定时间之前的分成 * @param time 时间 diff --git a/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/service/impl/BonusServiceImpl.java b/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/service/impl/BonusServiceImpl.java index 7692e2c..0774dbb 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/service/impl/BonusServiceImpl.java +++ b/ruoyi-service/src/main/java/com/ruoyi/bst/bonus/service/impl/BonusServiceImpl.java @@ -265,9 +265,9 @@ public class BonusServiceImpl implements BonusService @Override public int payBonus(BonusVO bonus) { - if (bonus == null) { - return 0; - } + ServiceUtil.assertion(bonus == null, "待打款的分成不存在"); + ServiceUtil.assertion(!BonusStatus.canPay().contains(bonus.getStatus()), "ID为%s的分成当前状态不允许打款", bonus.getId()); + Integer result = transactionTemplate.execute(status -> { // 更新分成状态为已分成 int pay = bonusMapper.pay(bonus.getId(), bonus.getWaitAmount(), LocalDateTime.now()); @@ -288,6 +288,16 @@ public class BonusServiceImpl implements BonusService 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 public boolean refundByBst(BonusBstType bstType, Long bstId, BigDecimal refundAmount, BigDecimal payAmount, String reason) { if (bstType == null || bstId == null || refundAmount == null || payAmount == null) { diff --git a/ruoyi-service/src/main/java/com/ruoyi/bst/device/mapper/DeviceMapper.xml b/ruoyi-service/src/main/java/com/ruoyi/bst/device/mapper/DeviceMapper.xml index 43e6d70..a3ceb9a 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/bst/device/mapper/DeviceMapper.xml +++ b/ruoyi-service/src/main/java/com/ruoyi/bst/device/mapper/DeviceMapper.xml @@ -128,13 +128,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and #{query.radius} >= round(st_distance_sphere(point(#{query.center[0]}, #{query.center[1]}), point(bd.longitude, bd.latitude))) - and bd.remaining_power >= #{query.powerRange[0]} + and bd.remaining_power >= #{query.powerRange[0]} and bd.remaining_power <= #{query.powerRange[1]} and ( bd.order_device_id is null - or bo.status in + or bo.status in #{item} @@ -381,12 +381,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - + update bst_device set model_id = null where id = #{id} - + diff --git a/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/DeviceService.java b/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/DeviceService.java index 517b3ad..8ecb6ed 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/DeviceService.java +++ b/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/DeviceService.java @@ -162,5 +162,12 @@ public interface DeviceService */ public DeviceVO selectAvaliableDevice(Long id, String sn); + /** + * 根据mac查询设备 + * @param mac + * @return + */ + public DeviceVO selectByMac(String mac); + } diff --git a/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/DeviceValidator.java b/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/DeviceValidator.java index dd910bb..93655bd 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/DeviceValidator.java +++ b/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/DeviceValidator.java @@ -18,4 +18,12 @@ public interface DeviceValidator { */ boolean canDeleteAll(List ids); + /** + * 用户是否有权限访问设备的websocket链接 + * @param mac + * @param userId + * @return + */ + boolean canLink(String mac, Long userId); + } diff --git a/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/impl/DeviceServiceImpl.java b/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/impl/DeviceServiceImpl.java index a5349a6..93776f1 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/impl/DeviceServiceImpl.java +++ b/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/impl/DeviceServiceImpl.java @@ -465,4 +465,14 @@ public class DeviceServiceImpl implements DeviceService } 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); + } } diff --git a/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/impl/DeviceValidatorImpl.java b/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/impl/DeviceValidatorImpl.java index 111f1bf..517e4b3 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/impl/DeviceValidatorImpl.java +++ b/ruoyi-service/src/main/java/com/ruoyi/bst/device/service/impl/DeviceValidatorImpl.java @@ -8,8 +8,11 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; 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.service.DeviceService; import com.ruoyi.bst.device.service.DeviceValidator; +import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.collection.CollectionUtils; @Service @@ -18,6 +21,9 @@ public class DeviceValidatorImpl implements DeviceValidator { @Autowired private DeviceMapper deviceMapper; + @Autowired + private DeviceService deviceService; + @Override public boolean canEdit(Long id) { return canOperate(Collections.singletonList(id)); @@ -42,4 +48,38 @@ public class DeviceValidatorImpl implements DeviceValidator { 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); + } + } diff --git a/ruoyi-service/src/main/java/com/ruoyi/bst/suit/service/impl/SuitValidatorImpl.java b/ruoyi-service/src/main/java/com/ruoyi/bst/suit/service/impl/SuitValidatorImpl.java index e984b3f..0ed81ea 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/bst/suit/service/impl/SuitValidatorImpl.java +++ b/ruoyi-service/src/main/java/com/ruoyi/bst/suit/service/impl/SuitValidatorImpl.java @@ -87,12 +87,15 @@ public class SuitValidatorImpl implements SuitValidator { ServiceUtil.assertion(rule.getFee() == null, "费用不能为空"); ServiceUtil.assertion(rule.getStart() < 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) { SuitIntervalFeeRule nextRule = intervalRules.get(i + 1); ServiceUtil.assertion(rule.getEnd() == null, "区间结束时间不能为空"); ServiceUtil.assertion(rule.getStart() > rule.getEnd(), "区间的结束时间不允许小于开始时间"); ServiceUtil.assertion(rule.getEnd() != nextRule.getStart(), "区间的结束时间必须等于下一个区间的开始时间"); + int eachUnit = rule.getEachUnit(); + int duration = rule.getEnd() - rule.getStart(); + ServiceUtil.assertion(duration % eachUnit != 0, "区间间隔必须能够被区间整除"); } } catch (Exception e) { throw new ServiceException("第" + (i + 1) + "条收费规则校验失败:" + e.getMessage()); diff --git a/ruoyi-service/src/main/java/com/ruoyi/bst/suit/utils/SuitUtil.java b/ruoyi-service/src/main/java/com/ruoyi/bst/suit/utils/SuitUtil.java index 9fb6a34..009bf9e 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/bst/suit/utils/SuitUtil.java +++ b/ruoyi-service/src/main/java/com/ruoyi/bst/suit/utils/SuitUtil.java @@ -1,6 +1,8 @@ package com.ruoyi.bst.suit.utils; import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.Comparator; import java.util.List; import com.ruoyi.bst.suit.domain.SuitVO; @@ -35,15 +37,22 @@ public class SuitUtil { return 0; } + long seconds = 0; // 起步价计费 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())) { - 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; - long totalTime = 0; + BigDecimal amount = depositAmount; // 剩余金额 + int totalTime = 0; // 总时长 // 获取单位对应的秒数 long unitSeconds = rentalUnit.getSeconds(); - for (SuitIntervalFeeRule rule : intervalFeeRules) { - // 处理最后一个区间的end可能为空的情况 - if ( intervalFeeRules.indexOf(rule) == intervalFeeRules.size() - 1) { - // 如果是最后一个区间且end为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) { + for (int i = 0; i < intervalFeeRules.size(); i++) { + SuitIntervalFeeRule rule = intervalFeeRules.get(i); + BigDecimal fee = rule.getFee(); // 每个时间段的金额 + int eachUnitTime = rule.getEachUnit(); // 每个时间段的时长 + if (fee.compareTo(BigDecimal.ZERO) <= 0 || eachUnitTime <= 0) { continue; // 跳过无效计费规则 } + // 计算可以购买几个区间,金额不足也按1个单位计算 + int count = amount.divide(fee, 0, RoundingMode.UP).intValue(); - // 计算当前区间内可以购买的完整单位数 - int maxUnitsInInterval = intervalDuration / eachUnitTime; - BigDecimal maxCostForInterval = eachUnitFee.multiply(BigDecimal.valueOf(maxUnitsInInterval)); + // 非最后节点,限制最大购买数量 + if (i < intervalFeeRules.size() - 1) { + int duration =rule.getEnd() - rule.getStart(); + // 计算最大金额 + int maxCount = BigDecimal.valueOf(duration).divide(BigDecimal.valueOf(eachUnitTime), 0, RoundingMode.UP).intValue(); + count = Math.min(count, maxCount); + } - // 如果剩余金额足够支付整个区间 - if (remainingAmount.compareTo(maxCostForInterval) >= 0) { - 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(); + // 记入时长,并扣减金额 + totalTime += count * eachUnitTime; + amount = amount.subtract(fee.multiply(BigDecimal.valueOf(count))); - // 计算最后不足一个计费单位的时间(按比例计算) - 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(); - } - - // 已经用完所有金额,退出循环 + // 金额不足则跳出循环 + if (amount.compareTo(BigDecimal.ZERO) <= 0) { break; } } - return totalTime; - } - - /** - * 计算已使用时长所需的金额 - * @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; + return totalTime * unitSeconds; } /** * 计算起步价计费所需金额 - * @param usedTime 已使用时长(秒) - * @param startRule 起步价计费规则 + * @param seconds 已使用时长(秒) + * @param rule 起步价计费规则 * @param rentalUnit 租赁单位 * @return 所需金额 */ - public static BigDecimal calcAmountForStartFee(long usedTime, SuitStartFeeRule startRule, SuitRentalUnit rentalUnit) { - if (usedTime <= 0 || startRule == null || rentalUnit == null) { + public static BigDecimal calcAmountForStartFee(long seconds, SuitStartFeeRule rule, SuitRentalUnit rentalUnit) { + if (seconds <= 0 || rule == null || rentalUnit == null) { return BigDecimal.ZERO; } // 获取单位对应的秒数 long unitSeconds = rentalUnit.getSeconds(); - // 将秒转换为计费单位 - BigDecimal usedTimeInUnit = BigDecimal.valueOf(usedTime).divide(BigDecimal.valueOf(unitSeconds), 0, BigDecimal.ROUND_UP); + BigDecimal startingPrice = rule.getStartingPrice(); // 起步价(元) + long startSeconds = rule.getStartingTime() * unitSeconds; // 起步时长(秒) - // 起步价和起步时长 - BigDecimal startingPrice = startRule.getStartingPrice(); - int startingTime = startRule.getStartingTime(); - - // 如果使用时长小于等于起步时长,按比例计算费用 - if (usedTimeInUnit.intValue() <= startingTime) { - return startingPrice.multiply(usedTimeInUnit).divide(BigDecimal.valueOf(startingTime), 2, BigDecimal.ROUND_UP); + // 如果使用时长小于等于起步时长,返回起步价费用 + if (seconds <= startSeconds) { + return rule.getStartingPrice(); } - // 超出起步时长的部分 - 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(); - 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); + // 总费用 = 起步价 + 超出费用 + return MathUtils.addDecimal(startingPrice, extraFee); } /** * 计算区间计费所需金额 - * @param usedTime 已使用时长(秒) - * @param intervalFeeRules 区间计费规则列表 + * @param seconds 已使用时长(秒) + * @param rules 区间计费规则列表 * @param rentalUnit 租赁单位 * @return 所需金额 */ - public static BigDecimal calcAmountForIntervalFee(long usedTime, List intervalFeeRules, SuitRentalUnit rentalUnit) { - if (usedTime <= 0 || CollectionUtils.isEmptyElement(intervalFeeRules) || rentalUnit == null) { + public static BigDecimal calcAmountForIntervalFee(long seconds, List rules, SuitRentalUnit rentalUnit) { + if (seconds <= 0 || CollectionUtils.isEmptyElement(rules) || rentalUnit == null) { return BigDecimal.ZERO; } // 获取单位对应的秒数 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; - int remainingTime = usedTimeInUnit; - for (SuitIntervalFeeRule rule : intervalFeeRules) { - // 如果没有剩余时间需要计费,退出循环 - if (remainingTime <= 0) { + for (int i = 0; i < rules.size(); i++) { + SuitIntervalFeeRule rule = rules.get(i); + 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; } - - // 处理最后一个区间的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; diff --git a/ruoyi-service/src/main/java/com/ruoyi/iot/service/impl/IotReceiveServiceImpl.java b/ruoyi-service/src/main/java/com/ruoyi/iot/service/impl/IotReceiveServiceImpl.java index c08604a..1eec0c2 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/iot/service/impl/IotReceiveServiceImpl.java +++ b/ruoyi-service/src/main/java/com/ruoyi/iot/service/impl/IotReceiveServiceImpl.java @@ -5,6 +5,7 @@ import java.time.LocalDateTime; import java.util.List; import java.util.concurrent.TimeUnit; +import com.ruoyi.ws.service.DeviceWebSocketService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -63,6 +64,9 @@ public class IotReceiveServiceImpl implements IotReceiveService { @Autowired private DeviceIotService deviceIotService; + @Autowired + private DeviceWebSocketService deviceWebSocketService; + @Override public void handleReceive(ReceiveMsg msg) { if (msg == null) { @@ -222,5 +226,8 @@ public class IotReceiveServiceImpl implements IotReceiveService { // 暂存到Redis缓存 redisCache.rightPush(CacheConstants.LOCATION_LOG_QUEUE, po); + + // 发送消息给ws服务 + deviceWebSocketService.sendMessageToDevice(po, device.getMac()); } } diff --git a/ruoyi-service/src/main/java/com/ruoyi/ws/service/DeviceWebSocketService.java b/ruoyi-service/src/main/java/com/ruoyi/ws/service/DeviceWebSocketService.java new file mode 100644 index 0000000..466954c --- /dev/null +++ b/ruoyi-service/src/main/java/com/ruoyi/ws/service/DeviceWebSocketService.java @@ -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> MAC_TO_SESSIONS = new ConcurrentHashMap<>(); + + /** + * 添加设备WebSocket会话的MAC地址 + */ + public void addMacToSession(String mac, Session session) { + if (mac == null || session == null) { + return; + } + + // 添加MAC到Session的映射 + Set 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 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> entry : MAC_TO_SESSIONS.entrySet()) { + String mac = entry.getKey(); + Set 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 getSessions(String mac) { + if (mac == null) { + return new HashSet<>(); + } + Set 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 macs) { + if (message == null) { + return; + } + + String msg = JSON.toJSONString(message); + for (String mac : macs) { + Set 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); + } + } + } + } + } + +} diff --git a/ruoyi-service/src/main/java/com/ruoyi/ws/service/WebSocketService.java b/ruoyi-service/src/main/java/com/ruoyi/ws/service/UserWebSocketService.java similarity index 96% rename from ruoyi-service/src/main/java/com/ruoyi/ws/service/WebSocketService.java rename to ruoyi-service/src/main/java/com/ruoyi/ws/service/UserWebSocketService.java index 9a9df92..13457eb 100644 --- a/ruoyi-service/src/main/java/com/ruoyi/ws/service/WebSocketService.java +++ b/ruoyi-service/src/main/java/com/ruoyi/ws/service/UserWebSocketService.java @@ -12,11 +12,9 @@ import java.util.concurrent.Executors; import javax.websocket.Session; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.alibaba.fastjson2.JSON; -import com.ruoyi.system.user.service.UserService; import lombok.extern.slf4j.Slf4j; @@ -25,10 +23,7 @@ import lombok.extern.slf4j.Slf4j; */ @Service @Slf4j -public class WebSocketService { - - @Autowired - private UserService userService; +public class UserWebSocketService { // 使用ConcurrentHashMap存储会话 private static final Map SESSION_POOL = new ConcurrentHashMap<>(); diff --git a/ruoyi-web/src/main/java/com/ruoyi/web/app/AppOrderController.java b/ruoyi-web/src/main/java/com/ruoyi/web/app/AppOrderController.java index 2496c93..1156180 100644 --- a/ruoyi-web/src/main/java/com/ruoyi/web/app/AppOrderController.java +++ b/ruoyi-web/src/main/java/com/ruoyi/web/app/AppOrderController.java @@ -131,7 +131,7 @@ public class AppOrderController extends BaseController { @ApiOperation("操作订单设备关闭") @PutMapping("/closeDevice") - public AjaxResult closeDevice(@RequestBody @Validated OrderCloseDeviceDTO dto) { + public AjaxResult closeDevice(@Validated OrderCloseDeviceDTO dto) { OrderVO order = orderService.selectOrderById(dto.getOrderId()); ServiceUtil.assertion(order == null, "订单不存在"); ServiceUtil.assertion(!orderValidator.canCloseDevice(order, getUserId()), "您无权操作ID为%s的订单设备关闭", order.getId()); diff --git a/ruoyi-web/src/main/java/com/ruoyi/web/bst/BonusController.java b/ruoyi-web/src/main/java/com/ruoyi/web/bst/BonusController.java index 4bb01e9..5537083 100644 --- a/ruoyi-web/src/main/java/com/ruoyi/web/bst/BonusController.java +++ b/ruoyi-web/src/main/java/com/ruoyi/web/bst/BonusController.java @@ -9,6 +9,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; 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.RestController; @@ -82,4 +83,14 @@ public class BonusController extends BaseController 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)); + } + } diff --git a/ruoyi-web/src/main/java/com/ruoyi/web/bst/SuitController.java b/ruoyi-web/src/main/java/com/ruoyi/web/bst/SuitController.java index a8bae4e..75aae86 100644 --- a/ruoyi-web/src/main/java/com/ruoyi/web/bst/SuitController.java +++ b/ruoyi-web/src/main/java/com/ruoyi/web/bst/SuitController.java @@ -4,6 +4,9 @@ import java.util.List; 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.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; @@ -144,4 +147,16 @@ public class SuitController extends BaseController { 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)); +// } +// } } diff --git a/ruoyi-web/src/main/java/com/ruoyi/ws/MessageWebSocket.java b/ruoyi-web/src/main/java/com/ruoyi/ws/DeviceWebSocket.java similarity index 56% rename from ruoyi-web/src/main/java/com/ruoyi/ws/MessageWebSocket.java rename to ruoyi-web/src/main/java/com/ruoyi/ws/DeviceWebSocket.java index f654310..586a9e3 100644 --- a/ruoyi-web/src/main/java/com/ruoyi/ws/MessageWebSocket.java +++ b/ruoyi-web/src/main/java/com/ruoyi/ws/DeviceWebSocket.java @@ -8,24 +8,26 @@ import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.ServerEndpoint; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import com.ruoyi.bst.device.service.DeviceValidator; import com.ruoyi.common.core.domain.model.LoginUser; import com.ruoyi.common.utils.SecurityUtils; import com.ruoyi.common.utils.spring.SpringUtils; import com.ruoyi.framework.web.service.TokenService; import com.ruoyi.ws.config.WebSocketAuthConfigurator; -import com.ruoyi.ws.service.WebSocketService; +import com.ruoyi.ws.service.DeviceWebSocketService; + +import lombok.extern.slf4j.Slf4j; /** * WebSocket服务类 */ -@ServerEndpoint(value = "/ws/message", configurator = WebSocketAuthConfigurator.class) +@ServerEndpoint(value = "/ws/device", configurator = WebSocketAuthConfigurator.class) @Component @Slf4j -public class MessageWebSocket { +public class DeviceWebSocket { + /** * 建立连接 */ @@ -33,15 +35,24 @@ public class MessageWebSocket { public void onOpen(Session session, EndpointConfig config) { try { String token = (String) config.getUserProperties().get("token"); + String mac = (String) config.getUserProperties().get("mac"); if (token != null) { 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); tokenService.verifyToken(loginUser); if (loginUser != null) { 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; } } @@ -55,15 +66,11 @@ public class MessageWebSocket { * 关闭连接 */ @OnClose - public void onClose() { + public void onClose(Session session) { try { - LoginUser loginUser = SecurityUtils.getLoginUser(); - if (loginUser != null) { - Long userId = loginUser.getUserId(); - WebSocketService webSocketService = SpringUtils.getBean(WebSocketService.class); - webSocketService.removeSession(userId); - log.info("用户{}断开WebSocket连接", userId); - } + DeviceWebSocketService webSocketService = SpringUtils.getBean(DeviceWebSocketService.class); + webSocketService.removeSession(session); + log.info("用户{}断开WebSocket连接", session); } catch (Exception e) { log.error("WebSocket关闭异常", e); } diff --git a/ruoyi-web/src/main/java/com/ruoyi/ws/config/WebSocketAuthConfigurator.java b/ruoyi-web/src/main/java/com/ruoyi/ws/config/WebSocketAuthConfigurator.java index c1c7f54..fbbbbb2 100644 --- a/ruoyi-web/src/main/java/com/ruoyi/ws/config/WebSocketAuthConfigurator.java +++ b/ruoyi-web/src/main/java/com/ruoyi/ws/config/WebSocketAuthConfigurator.java @@ -10,10 +10,11 @@ import javax.websocket.server.ServerEndpointConfig; public class WebSocketAuthConfigurator extends ServerEndpointConfig.Configurator { @Override public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) { - String token = null; 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("mac", mac); } - sec.getUserProperties().put("token", token); } -} \ No newline at end of file +}