Commit 92f20b9f authored by Administrator's avatar Administrator

班车模块增加接websocket

parent c1d85650
...@@ -414,6 +414,10 @@ ...@@ -414,6 +414,10 @@
<artifactId>aliyun-sdk-oss</artifactId> <artifactId>aliyun-sdk-oss</artifactId>
<version>3.10.2</version> <version>3.10.2</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
</dependencies> </dependencies>
<repositories> <repositories>
<repository> <repository>
......
...@@ -46,6 +46,13 @@ public class SecurityInterceptor extends JService implements HandlerInterceptor ...@@ -46,6 +46,13 @@ public class SecurityInterceptor extends JService implements HandlerInterceptor
@Override @Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception { Object handler) throws Exception {
// 检查请求头中是否有Upgrade: websocket
String upgradeHeader = request.getHeader("Upgrade");
if ("websocket".equalsIgnoreCase(upgradeHeader)) {
// 如果是WebSocket升级请求,可以选择不处理或特殊处理
// 然后返回true,继续执行
return true;
}
Map<String, Object> queryParams = getQueryParams(request); Map<String, Object> queryParams = getQueryParams(request);
logger.info("请求URL:"+request.getRequestURI()+queryParams); logger.info("请求URL:"+request.getRequestURI()+queryParams);
......
package com.dlbridge.jadp.config.web;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// 启用简单的内存消息代理,指定消息代理目的地前缀
config.enableSimpleBroker("/topic", "/queue");
// 设置应用程序目的地前缀
config.setApplicationDestinationPrefixes("/app");
// 设置用户目的地前缀(点对点消息)
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// 注册 STOMP 端点,指定使用 SockJS
registry.addEndpoint("/ws").setAllowedOrigins("*") // 允许所有来源,生产环境应指定具体域名
.withSockJS();
// 同时支持原生 WebSocket
registry.addEndpoint("/ws")
.setAllowedOrigins("*");
}
}
...@@ -7,12 +7,15 @@ import com.dlbridge.jadp.plugins.cmpt.dao.model.param.BusVerifyRecordParam; ...@@ -7,12 +7,15 @@ import com.dlbridge.jadp.plugins.cmpt.dao.model.param.BusVerifyRecordParam;
import com.dlbridge.jadp.plugins.cmpt.dao.model.vo.BusVerifyStatistics; import com.dlbridge.jadp.plugins.cmpt.dao.model.vo.BusVerifyStatistics;
import com.dlbridge.jadp.plugins.cmpt.dto.*; import com.dlbridge.jadp.plugins.cmpt.dto.*;
import com.dlbridge.jadp.plugins.cmpt.utils.Page; import com.dlbridge.jadp.plugins.cmpt.utils.Page;
import com.dlbridge.jadp.websocket.WebSocketSessionService;
import com.github.pagehelper.PageHelper; import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo; import com.github.pagehelper.PageInfo;
import com.google.zxing.BarcodeFormat; import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.client.j2se.MatrixToImageWriter; import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix; import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter; import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import lombok.extern.log4j.Log4j2; import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
...@@ -21,6 +24,8 @@ import com.dlbridge.jadp.plugins.cmpt.utils.PageConvertUtil; ...@@ -21,6 +24,8 @@ import com.dlbridge.jadp.plugins.cmpt.utils.PageConvertUtil;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
...@@ -33,6 +38,9 @@ public class BusService { ...@@ -33,6 +38,9 @@ public class BusService {
@Resource @Resource
private BusMapper busMapper; private BusMapper busMapper;
@Resource
WebSocketSessionService webSocketSessionService;
public List<BusTicketVerification> selectTicketByIdNumber(Integer cmptId, String idNumber) { public List<BusTicketVerification> selectTicketByIdNumber(Integer cmptId, String idNumber) {
// 获取我购买的车票列表 // 获取我购买的车票列表
List<BusTicketVerification> tickets = busMapper.selectTicketByIdNumber(cmptId, idNumber); List<BusTicketVerification> tickets = busMapper.selectTicketByIdNumber(cmptId, idNumber);
...@@ -74,8 +82,17 @@ public class BusService { ...@@ -74,8 +82,17 @@ public class BusService {
long timestamp = System.currentTimeMillis(); long timestamp = System.currentTimeMillis();
String qrContent = String.format("ticketNo=%s&timestamp=%d", ticket.getTicketNo(), timestamp); String qrContent = String.format("ticketNo=%s&timestamp=%d", ticket.getTicketNo(), timestamp);
// 设置二维码参数,去除白边
Map<EncodeHintType, Object> hints = new HashMap<>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.MARGIN, 0); // 关键:设置边距为0
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); // 错误纠正级别
QRCodeWriter qrCodeWriter = new QRCodeWriter(); QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix bitMatrix = qrCodeWriter.encode(qrContent, BarcodeFormat.QR_CODE, 250, 250); BitMatrix bitMatrix = qrCodeWriter.encode(qrContent, BarcodeFormat.QR_CODE, 200, 200, hints);
// 进一步处理去除白边
bitMatrix = removeWhiteBorder(bitMatrix);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream); MatrixToImageWriter.writeToStream(bitMatrix, "PNG", outputStream);
...@@ -87,6 +104,31 @@ public class BusService { ...@@ -87,6 +104,31 @@ public class BusService {
} }
} }
/**
* 去除二维码白边的辅助方法
*/
private BitMatrix removeWhiteBorder(BitMatrix matrix) {
int[] enclosingRectangle = matrix.getEnclosingRectangle();
if (enclosingRectangle == null) {
return matrix;
}
int left = enclosingRectangle[0];
int top = enclosingRectangle[1];
int width = enclosingRectangle[2];
int height = enclosingRectangle[3];
BitMatrix newMatrix = new BitMatrix(width, height);
for (int x = 0; x < width; x++) {
for (int y = 0; y < height; y++) {
if (matrix.get(x + left, y + top)) {
newMatrix.set(x, y);
}
}
}
return newMatrix;
}
/** /**
* 车票核销方法 * 车票核销方法
* @param param 核销参数 * @param param 核销参数
...@@ -132,6 +174,14 @@ public class BusService { ...@@ -132,6 +174,14 @@ public class BusService {
record.setVerifierId(param.getVerifierId()); record.setVerifierId(param.getVerifierId());
record.setVerificationStationId(ticket.getBoardingStationId()); record.setVerificationStationId(ticket.getBoardingStationId());
busMapper.insertVerificationRecord(record); busMapper.insertVerificationRecord(record);
// 发送核销成功通知
LocalDateTime now = LocalDateTime.now();
String successMessage = String.format("车票核销成功!班次:%s,核销时间:%s",
ticket.getScheduleName(), now.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
webSocketSessionService.sendVerificationSuccess(ticket.getTicketNo(), successMessage);
log.info("车票核销成功并已发送通知 - 票号: {}, 用户: {}", ticket.getTicketNo(), ticket.getIdNumber());
return Result.success(); return Result.success();
} }
......
package com.dlbridge.jadp.websocket;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.stereotype.Controller;
import java.util.Map;
@Slf4j
@Controller
@RequiredArgsConstructor
public class WebSocketController {
private final WebSocketSessionService sessionService;
/**
* 处理客户端发送的心跳消息
*/
@MessageMapping("/heartbeat")
public void handleHeartbeat(@Payload Map<String, Object> message) {
String sessionId = (String) message.get("sessionId");
String userId = (String) message.get("userId");
log.info("收到心跳消息 - 会话ID: {}, 用户ID: {}", sessionId, userId);
}
/**
* 处理客户端订阅特定票号
*/
@MessageMapping("/ticket/subscribe")
public void handleTicketSubscribe(@Payload Map<String, Object> message) {
String sessionId = (String) message.get("sessionId");
String userId = (String) message.get("userId");
String ticketNo = (String) message.get("ticketNo");
log.info("客户端订阅票号 - 会话ID: {}, 用户ID: {}, 票号: {}", sessionId, userId, ticketNo);
if (userId != null && ticketNo != null) {
sessionService.registerUserSession(userId, ticketNo, sessionId);
// 发送订阅成功确认
WebSocketMessage response = WebSocketMessage.info(
ticketNo,
"票号订阅成功,开始接收核销通知"
);
sessionService.sendToUser(userId, response);
}
}
/**
* 处理客户端取消订阅票号
*/
@MessageMapping("/ticket/unsubscribe")
public void handleTicketUnsubscribe(@Payload Map<String, Object> message) {
String sessionId = (String) message.get("sessionId");
String ticketNo = (String) message.get("ticketNo");
log.info("客户端取消订阅票号 - 会话ID: {}, 票号: {}", sessionId, ticketNo);
sessionService.removeTicketSession(sessionId, ticketNo);
}
/**
* 处理获取会话状态请求
*/
@SubscribeMapping("/user/queue/status")
public Map<String, Object> handleStatusRequest() {
return sessionService.getSessionStats();
}
}
\ No newline at end of file
package com.dlbridge.jadp.websocket;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.event.EventListener;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.messaging.SessionConnectEvent;
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketEventListener {
private final WebSocketSessionService sessionService;
/**
* 处理 WebSocket 连接建立事件
*/
@EventListener
public void handleWebSocketConnectListener(SessionConnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = headerAccessor.getSessionId();
String userId = headerAccessor.getFirstNativeHeader("userId");
log.info("WebSocket 连接建立 - 会话ID: {}, 用户ID: {}", sessionId, userId);
}
/**
* 处理 WebSocket 订阅事件
*/
@EventListener
public void handleWebSocketSubscribeListener(SessionSubscribeEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = headerAccessor.getSessionId();
String destination = headerAccessor.getDestination();
String userId = headerAccessor.getFirstNativeHeader("userId");
String ticketNo = headerAccessor.getFirstNativeHeader("ticketNo");
log.info("WebSocket 订阅 - 会话ID: {}, 目的地: {}, 用户ID: {}, 票号: {}",
sessionId, destination, userId, ticketNo);
if (userId != null && ticketNo != null) {
sessionService.registerUserSession(userId, ticketNo, sessionId);
}
}
/**
* 处理 WebSocket 连接断开事件
*/
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
String sessionId = headerAccessor.getSessionId();
log.info("WebSocket 连接断开 - 会话ID: {}", sessionId);
sessionService.removeUserSession(sessionId);
}
}
package com.dlbridge.jadp.websocket;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class WebSocketMessage {
private String type; // 消息类型:VERIFICATION_SUCCESS, ERROR, etc.
private String ticketNo; // 票号
private String message; // 消息内容
private LocalDateTime timestamp; // 时间戳
private Object data; // 附加数据
public static WebSocketMessage success(String ticketNo, String message) {
return new WebSocketMessage("VERIFICATION_SUCCESS", ticketNo, message, LocalDateTime.now(), null);
}
public static WebSocketMessage error(String ticketNo, String message) {
return new WebSocketMessage("ERROR", ticketNo, message, LocalDateTime.now(), null);
}
public static WebSocketMessage info(String ticketNo, String message) {
return new WebSocketMessage("INFO", ticketNo, message, LocalDateTime.now(), null);
}
}
package com.dlbridge.jadp.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.*;
@Slf4j
@Service
public class WebSocketSessionService {
private final SimpMessagingTemplate messagingTemplate;
// 存储用户与票号的映射关系(一个用户对应多个票号)
private final Map<String, Set<String>> userTicketsMap = new ConcurrentHashMap<>();
// 存储票号与用户的映射关系
private final Map<String, String> ticketUserMap = new ConcurrentHashMap<>();
// 存储会话与用户的映射关系
private final Map<String, String> sessionUserMap = new ConcurrentHashMap<>();
// 存储会话与票号的映射关系(一个会话可能监听多个票号)
private final Map<String, Set<String>> sessionTicketsMap = new ConcurrentHashMap<>();
public WebSocketSessionService(SimpMessagingTemplate messagingTemplate) {
this.messagingTemplate = messagingTemplate;
}
/**
* 注册用户会话
* @param userId 用户ID(赛事ID+证件号,例:29643-HK002949527)
* @param ticketNo 票号
* @param sessionId 会话ID
*/
public void registerUserSession(String userId, String ticketNo, String sessionId) {
// 更新用户-票号映射
userTicketsMap.computeIfAbsent(userId, k -> ConcurrentHashMap.newKeySet()).add(ticketNo);
// 更新票号-用户映射
ticketUserMap.put(ticketNo, userId);
// 更新会话-用户映射
sessionUserMap.put(sessionId, userId);
// 更新会话-票号映射
sessionTicketsMap.computeIfAbsent(sessionId, k -> ConcurrentHashMap.newKeySet()).add(ticketNo);
log.info("用户会话注册成功 - 用户: {}, 票号: {}, 会话ID: {}", userId, ticketNo, sessionId);
log.debug("当前会话状态 - 用户数: {}, 票号数: {}, 会话数: {}",
userTicketsMap.size(), ticketUserMap.size(), sessionUserMap.size());
}
/**
* 移除用户会话
* @param sessionId 会话ID
*/
public void removeUserSession(String sessionId) {
String userId = sessionUserMap.remove(sessionId);
if (userId != null) {
// 移除会话相关的票号
Set<String> ticketNos = sessionTicketsMap.remove(sessionId);
if (ticketNos != null) {
// 从用户-票号映射中移除这些票号
Set<String> userTickets = userTicketsMap.get(userId);
if (userTickets != null) {
userTickets.removeAll(ticketNos);
// 如果用户没有票号了,移除用户
if (userTickets.isEmpty()) {
userTicketsMap.remove(userId);
}
}
// 从票号-用户映射中移除这些票号
ticketNos.forEach(ticketUserMap::remove);
}
log.info("用户会话移除成功 - 会话ID: {}, 用户: {}", sessionId, userId);
} else {
log.warn("移除不存在的会话 - 会话ID: {}", sessionId);
}
}
/**
* 移除特定票号的会话
* @param sessionId 会话ID
* @param ticketNo 票号
*/
public void removeTicketSession(String sessionId, String ticketNo) {
// 从会话-票号映射中移除
Set<String> sessionTickets = sessionTicketsMap.get(sessionId);
if (sessionTickets != null) {
sessionTickets.remove(ticketNo);
if (sessionTickets.isEmpty()) {
sessionTicketsMap.remove(sessionId);
}
}
// 从用户-票号映射中移除
String userId = sessionUserMap.get(sessionId);
if (userId != null) {
Set<String> userTickets = userTicketsMap.get(userId);
if (userTickets != null) {
userTickets.remove(ticketNo);
if (userTickets.isEmpty()) {
userTicketsMap.remove(userId);
}
}
}
// 从票号-用户映射中移除
ticketUserMap.remove(ticketNo);
log.info("票号会话移除成功 - 会话ID: {}, 票号: {}", sessionId, ticketNo);
}
/**
* 根据票号获取用户ID
*/
public String getUserIdByTicketNo(String ticketNo) {
return ticketUserMap.get(ticketNo);
}
/**
* 根据用户ID获取所有票号
*/
public Set<String> getTicketNosByUserId(String userId) {
return userTicketsMap.getOrDefault(userId, Collections.emptySet());
}
/**
* 根据用户ID获取所有会话ID
*/
public Set<String> getSessionIdsByUserId(String userId) {
Set<String> sessionIds = new HashSet<>();
sessionUserMap.forEach((sessionId, uid) -> {
if (uid.equals(userId)) {
sessionIds.add(sessionId);
}
});
return sessionIds;
}
/**
* 发送核销成功通知给指定票号对应的用户
*/
public void sendVerificationSuccess(String ticketNo, String message) {
String userId = getUserIdByTicketNo(ticketNo);
if (userId != null) {
String destination = "/topic/verification/" + userId;
WebSocketMessage wsMessage = WebSocketMessage.success(ticketNo, message);
messagingTemplate.convertAndSend(destination, wsMessage);
log.info("核销成功通知已发送 - 用户: {}, 票号: {}, 消息: {}", userId, ticketNo, message);
// 核销成功后,移除该票号的会话映射
removeTicketMappings(ticketNo);
} else {
log.warn("未找到对应的用户会话,无法发送核销通知 - 票号: {}", ticketNo);
}
}
/**
* 发送错误通知给指定票号对应的用户
*/
public void sendError(String ticketNo, String errorMessage) {
String userId = getUserIdByTicketNo(ticketNo);
if (userId != null) {
String destination = "/topic/errors/" + userId;
WebSocketMessage wsMessage = WebSocketMessage.error(ticketNo, errorMessage);
messagingTemplate.convertAndSend(destination, wsMessage);
log.info("错误通知已发送 - 用户: {}, 票号: {}, 错误: {}", userId, ticketNo, errorMessage);
}
}
/**
* 发送通知给指定用户的所有会话
*/
public void sendToUser(String userId, WebSocketMessage message) {
String destination = "/topic/user/" + userId;
messagingTemplate.convertAndSend(destination, message);
log.info("用户通知已发送 - 用户: {}, 类型: {}", userId, message.getType());
}
/**
* 移除票号的所有映射关系
*/
private void removeTicketMappings(String ticketNo) {
String userId = ticketUserMap.remove(ticketNo);
if (userId != null) {
// 从用户-票号映射中移除
Set<String> userTickets = userTicketsMap.get(userId);
if (userTickets != null) {
userTickets.remove(ticketNo);
if (userTickets.isEmpty()) {
userTicketsMap.remove(userId);
}
}
// 从所有会话的票号集合中移除
sessionTicketsMap.values().forEach(tickets -> tickets.remove(ticketNo));
log.info("票号映射关系已移除 - 票号: {}", ticketNo);
}
}
/**
* 获取当前会话统计信息(用于监控)
*/
public Map<String, Object> getSessionStats() {
Map<String, Object> stats = new HashMap<>();
stats.put("userCount", userTicketsMap.size());
stats.put("ticketCount", ticketUserMap.size());
stats.put("sessionCount", sessionUserMap.size());
stats.put("activeSessions", sessionTicketsMap.size());
// 计算平均每个用户的票号数
double avgTicketsPerUser = userTicketsMap.values().stream()
.mapToInt(Set::size)
.average()
.orElse(0.0);
stats.put("avgTicketsPerUser", avgTicketsPerUser);
return stats;
}
/**
* 清理过期会话(可定时调用)
*/
public void cleanupExpiredSessions() {
// 这里可以实现会话过期清理逻辑
// 比如移除长时间没有活动的会话
log.debug("清理过期会话 - 当前会话数: {}", sessionUserMap.size());
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment