共享点单一般发生去餐厅共同扫桌面二维码,或者手机点单,发起人分享订单给别人,别人通过链接进入点单页面。
自己琢磨了下共享点单的一套流程,然后使用springboot+websocket+redis简单实现了一段拼单逻辑。
以下为主要代码:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-websocketartifactId>
dependency>
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket 配置类
* pom.xml,springboot 2.x 自带websocket依赖,不需要带版本号
*
* org.springframework.boot
* spring-boot-starter-websocket
*
* add qy
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
package com.qiuyu.demo.service;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.qiuyu.demo.utils.redis.RedisUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
@ServerEndpoint("/ws/shareBill/{orderId}/{userId}")
// @ServerEndpoint("/wsserver/{userId}")
@Component
public class WebSocketServer {
private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
/**静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。*/
private static int onlineTableCount = 0;
private static ConcurrentHashMap<String, HashMap<String,WebSocketServer>> orderToUsersMap = new ConcurrentHashMap<>();
/**与某个客户端的连接会话,需要通过它来给客户端发送数据*/
private Session session;
/**接收userId*/
private String userId="";
private String orderId="";
private static final String SHARE_BILL_KEY = "SHARE_BILL_KEY_";
/**
* 连接建立成功调用的方法*/
@OnOpen
public void onOpen(Session session,@PathParam("orderId") String orderId,@PathParam("userId") String userId) {
this.session = session;
this.userId=userId;
this.orderId=orderId;
// 获取拼单号是否存在
HashMap<String,WebSocketServer> webSocketMap;
if (orderToUsersMap.containsKey(orderId)){
webSocketMap = orderToUsersMap.get(orderId);
if (webSocketMap.containsKey(userId)){
webSocketMap.remove(userId);
webSocketMap.put(userId,this);
} else {
webSocketMap.put(userId,this);
}
orderToUsersMap.put(orderId,webSocketMap);
// 将订单对应的JSON 发送给对应userId,可以从redis取出来
try {
sendMessage(getMessageFromRedisByOrder(orderId));
}
catch (Exception e){
e.printStackTrace();
}
}else{
webSocketMap = new HashMap<>();
webSocketMap.put(userId,this);
orderToUsersMap.put(orderId,webSocketMap);
//加入set中
addOnlineCount();
//在线数加1
}
log.info("用户ID:{},加入当前订单{},该订单拼单人数{},总拼单数为:{}" ,userId,orderId,webSocketMap.size(),getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if(orderToUsersMap.containsKey(orderId)){
HashMap<String,WebSocketServer> webSocketMap = orderToUsersMap.get(orderId);
if (webSocketMap.size()==0){
// 无参与人,移除订单
orderToUsersMap.remove(orderId);
subOnlineCount();
log.info("订单{}移除,总拼单数为:{}" ,orderId, getOnlineCount());
}else {
if (webSocketMap.containsKey(userId)){
webSocketMap.remove(userId);
if (webSocketMap.size()==0){
// 无参与人,移除订单
orderToUsersMap.remove(orderId);
subOnlineCount();
String key = SHARE_BILL_KEY+orderId;
RedisUtils.remove(key);
log.info("订单{}移除,总拼单数为:{}" ,orderId, getOnlineCount());
}
log.info("用户ID:{},退出当前订单{},该订单拼单人数{},总拼单数为:{}" ,userId,orderId,webSocketMap.size(),getOnlineCount());
}
}
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("用户消息ID:"+userId+",报文:"+message);
//可以群发消息
//消息保存到数据库、redis
if(StringUtils.isNotBlank(message)){
try {
// 存储报文到redis
sendMessageToRedis(orderId,message);
//解析发送的报文
JSONObject jsonObject = JSON.parseObject(message);
// 已选商品同步至其他点单人
HashMap<String,WebSocketServer> webSocketMap = orderToUsersMap.get(orderId);
for (Map.Entry entry : webSocketMap.entrySet()){
WebSocketServer webSocketServer = (WebSocketServer)entry.getValue();
webSocketServer.sendMessage(jsonObject.toJSONString());
}
}catch (Exception e){
e.printStackTrace();
}
}
}
/**
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误ID:"+this.userId+",原因:"+error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
private void sendMessageToRedis(String orderId,String message){
String key = SHARE_BILL_KEY+orderId;
if (RedisUtils.exists(key)){
RedisUtils.remove(key);
RedisUtils.set(key,message,60*15L);
}
RedisUtils.set(key,message,60*15L);
}
private String getMessageFromRedisByOrder(String orderId){
String key = SHARE_BILL_KEY+orderId;
if (RedisUtils.exists(key)){
return RedisUtils.get(key).toString();
}
return "";
}
/**
* 发送自定义消息
* */
public static void sendInfo(String message,@PathParam("orderId") String orderId,@PathParam("userId") String userId) throws IOException {
log.info("发送消息到ID:"+userId+",报文:"+message);
if(StringUtils.isNotBlank(orderId) && orderToUsersMap.get(orderId).containsKey(userId)){
orderToUsersMap.get(orderId).get(userId).sendMessage(message);
}else{
log.error("用户ID"+userId+",不在线!");
}
}
public static synchronized int getOnlineCount() {
return onlineTableCount;
}
public static synchronized void addOnlineCount() {
WebSocketServer.onlineTableCount++;
}
public static synchronized void subOnlineCount() {
WebSocketServer.onlineTableCount--;
}
}
DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>websocket通讯title>
head>
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.js">script>
<script>
var socket;
function openSocket() {
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else{
console.log("您的浏览器支持WebSocket");
// 实现化WebSocket对象,指定要连接的服务器地址与端口 建立连接
// 等同于socket = new WebSocket("ws://localhost:8888/xxxx/im/25");
// var socketUrl="${request.contextPath}/im/"+$("#userId").val();
var socketUrl="http://localhost:8080/ws/shareBill/"+$("#orderId").val()+"/"+$("#userId").val();
socketUrl=socketUrl.replace("https","ws").replace("http","ws");
console.log(socketUrl);
if(socket!=null){
socket.close();
socket=null;
}
socket = new WebSocket(socketUrl);
//打开事件
socket.onopen = function() {
console.log("websocket已打开");
//socket.send("这是来自客户端的消息" + location.href + new Date());
};
//获得消息事件
socket.onmessage = function(msg) {
console.log(msg.data);
var json = JSON.parse(msg.data);
console.log("JSON.parse()",json);
document.getElementById("contentText").value = json.contentText;
//发现消息进入 开始处理前端触发逻辑
};
//关闭事件
socket.onclose = function() {
console.log("websocket已关闭");
};
//发生了错误事件
socket.onerror = function() {
console.log("websocket发生了错误");
}
}
}
function sendMessage() {
if(typeof(WebSocket) == "undefined") {
console.log("您的浏览器不支持WebSocket");
}else {
console.log("您的浏览器支持WebSocket");
console.log('{"orderId":"'+$("#orderId").val()+'","contentText":"'+$("#contentText").val()+'"}');
socket.send('{"orderId":"'+$("#orderId").val()+'","contentText":"'+$("#contentText").val()+'"}');
}
}
function createOrderId() {
document.getElementById("orderId").value = "20210323162129128WM6542";
}
script>
<body>
<p>【用户ID】:<div><input id="userId" name="userId" type="text" value="wx123wdgr23icowe02c">div>
<p>【订单ID】:<div><input id="orderId" name="orderId" type="text" value=""><button onclick="createOrderId()">创建订单(默认写死的)button>div>
<p>【已选商品】:<div><input id="contentText" name="contentText" type="text" value="">div>
<p>【1、操作】:<div><button onclick="openSocket()">发起拼单/加入拼单button>div>
<p>【2、操作】:<div><button onclick="sendMessage()">点单操作(+ -)button>div>
body>
html>
package com.qiuyu.demo.resource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class WebSocketController {
@GetMapping("/index")
public String re(){
return "sharedOrder";
}
}
1、创建订单,这里默认一笔订单号了。
2、发起人可以先选一部分商品,在发起拼单,也可以直接发起。
3、添加商品
这里是模拟,直接简单地将商品当成字符串处理保存到redis中。
4、其他用户加入拼单
大家可以拉取项目启动尝试一下,去了解下运行过程。希望能给大家带来一些解决拼单问题的思路。有问题留言吧