ws、套餐金额计算

This commit is contained in:
磷叶 2025-04-12 16:15:50 +08:00
parent 62cef22f1f
commit 95aedfeb9c
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);
}
// 允许打款的分成状态
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);
/**
* 分成打款
* @param id 分成明细ID
* @return 结果
*/
public int payBonus(Long id);
/**
* 进行分成打款指定时间之前的分成
* @param time 时间

View File

@ -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) {

View File

@ -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)))
</if>
<if test="query.powerRange != null and query.powerRange.size() == 2">
and bd.remaining_power &gt;= #{query.powerRange[0]}
and bd.remaining_power &gt;= #{query.powerRange[0]}
and bd.remaining_power &lt;= #{query.powerRange[1]}
</if>
<if test="query.nonOrOrderStatusList != null and query.nonOrOrderStatusList.size() > 0">
and (
bd.order_device_id is null
or bo.status in
or bo.status in
<foreach item="item" collection="query.nonOrOrderStatusList" open="(" separator="," close=")">
#{item}
</foreach>
@ -381,12 +381,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</where>
</select>
<!-- resetModel -->
<!-- resetModel -->
<update id="resetModel">
update bst_device
set model_id = null
where id = #{id}
</update>
</mapper>

View File

@ -162,5 +162,12 @@ public interface DeviceService
*/
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);
/**
* 用户是否有权限访问设备的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);
}
@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 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);
}
}

View File

@ -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());

View File

@ -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<SuitIntervalFeeRule> intervalFeeRules, SuitRentalUnit rentalUnit) {
if (usedTime <= 0 || CollectionUtils.isEmptyElement(intervalFeeRules) || rentalUnit == null) {
public static BigDecimal calcAmountForIntervalFee(long seconds, List<SuitIntervalFeeRule> 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;

View File

@ -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());
}
}

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 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<Long, Session> SESSION_POOL = new ConcurrentHashMap<>();

View File

@ -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());

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.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));
}
}

View File

@ -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));
// }
// }
}

View File

@ -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);
}

View File

@ -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);
}
}
}