背景
公司最近在各个会议室都安防了一块屏显示会议预约系统,购买的别家公司的会议预约app软件安装,现在不维护了,会议预约出了问题,于是就研发了一套简易的,集成企业微信会议预约功能的产品。
满足:
- 企业微信预约后在会议室门口屏上显示当前是否预订信息,以及下个预订信息
- 方便大家快速确认该会议室是否被预订
技术要求:
3. 熟悉企业微信会议预订通讯协议(http)
4. 掌握前端技术,html,css,js即可
5. 熟悉spingboot, websocket
整体架构流程
前端:屏端显示一个本地网页,间隔1分钟更新一次会议信息
后端:websocket监听,有请求会议信息,调用企业微信接口获取数据并返回前端
技术名词解释
都是常用技术,大家直接百度
技术细节
- 前端展示,上核心部分代码
(function flexible(window, document) {
function resetFontSize() {
const size = (document.documentElement.clientWidth / 1280) * 37.5;
document.documentElement.style.fontSize = size + 'px';
}
// reset root font size on page show or resize
window.addEventListener('pageshow', resetFontSize);
window.addEventListener('resize', resetFontSize);
})(window, document);
let roomName = "会议室2";
let wsurl = "ws://192.xxx/webSocket?room=";
let ws;
let heartCheck = {
timeout: 1000,//1s
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
this.start();
},
start: function(){
let self = this;
this.timeoutObj = setTimeout(function(){
ws.send(roomName);
self.serverTimeoutObj = setTimeout(function(){
ws.close();//如果onclose会执行reconnect,我们执行ws.close()就行了.如果直接执行reconnect 会触发onclose导致重连两次
}, self.timeout)
}, this.timeout)
},
}
function getRoomInfo() {
if("WebSocket" in window){
if(ws == null || ws.readyState != ws.OPEN){
ws = new WebSocket(wsurl+roomName);
ws.onopen = function(){
//当WebSocket创建成功时,触发onopen事件
heartCheck.reset();
}
ws.onmessage = function(e){
//当客户端收到服务端发来的消息时,触发onmessage事件,参数e.data包含server传递过来的数据
//parseAndExeData(e.data);
//heartCheck.reset();
console.log("onmessage",new Date(),e.data)
let {current,next,status} = JSON.parse(e.data);
if(roomName){
document.getElementById("roomName").innerHTML=roomName
}
if(status){
document.getElementById("status").innerHTML=status
document.getElementById("statusImg").style.display = "inline";
if(status == '空闲'){
document.getElementById("roomName").style.fontSize="145px";
document.getElementById("roomName").style.fontWeight="bold";
document.getElementById("roomName").style.top="70px";
document.getElementById("roomName").style.left="355px";
document.getElementById("meetingBg").style.background = "url(./img/free.png) -490px -6px no-repeat";
document.getElementById("statusImg").src = "./img/metting3.png";
document.getElementById("status").style.color="rgba(60, 248, 174, 1)";
document.getElementById("summary").innerHTML=""
document.getElementById("meetingTime").innerHTML=""
document.getElementById("meetingTimeImg").style.display = "none";
document.getElementById("organizer").innerHTML=""
document.getElementById("organizerImg").style.display = "none";
document.getElementById("attendNames").innerHTML=""
document.getElementById("attendNamesImg").style.display = "none";
}else if(status == '会议中'){
document.getElementById("roomName").style.fontSize="48px";
document.getElementById("roomName").style.fontWeight="Normal";
document.getElementById("roomName").style.top="21px";
document.getElementById("roomName").style.left="546px";
document.getElementById("meetingBg").style.background = "url(./img/meeting.png) -490px -6px no-repeat";
document.getElementById("statusImg").src = "./img/metting2.png";
document.getElementById("status").style.color="rgba(255, 88, 82, 1)";
}
if(current){
if(current.summary){
document.getElementById("summary").innerHTML=current.summary
}
if(current.time){
document.getElementById("meetingTime").innerHTML=current.time
document.getElementById("meetingTimeImg").style.display = "inline";
}
if(current.organizer){
document.getElementById("organizer").innerHTML=current.organizer
document.getElementById("organizerImg").style.display = "inline";
}
if(current.attendNames){
document.getElementById("attendNames").innerHTML=current.attendNames
document.getElementById("attendNamesImg").style.display = "inline";
}
}
}
if(next) {
if(next.summary){
document.getElementById("nextSummary").innerHTML=next.summary.slice(0, 9)
document.getElementById("nextShow").style.display = "flex";
}else{
document.getElementById("nextSummary").innerHTML=""
document.getElementById("nextShow").style.display = "none";
}
if(next.time){
document.getElementById("nextTime").innerHTML=next.time
}else{
document.getElementById("nextTime").innerHTML=""
}
if(next.organizer){
document.getElementById("nextOrganizer").innerHTML=next.organizer
}else{
document.getElementById("nextOrganizer").innerHTML=""
}
}
}
ws.onclose = function(e){
console.log("onclose")
//当客户端收到服务端发送的关闭连接请求时,触发onclose事件
ws = null;
setTimeout(function () {getRoomInfo();}, 60000);
}
ws.onerror = function(e){
console.log("onerror")
//如果出现连接、处理、接收、发送数据失败的时候触发onerror事件
ws.close();
}
}
}
}
function showTime() {
let now = new Date();
let h = now.getHours();
let m = now.getMinutes();
let showHM = h + ":" + (m > 9 ? m : "0" + m);
document.getElementById("curTime").innerHTML = showHM;
setTimeout("showTime()", 1000);
}
//此处需要延时1秒执行
setTimeout(function () {getRoomInfo();}, 1000);
//刷新当前时间
setTimeout("showTime()", 1000);
会议室可以通过企业微信获取,我这边用名称,更好处理
- 后端代码,首先需要新建一个spring boot项目,这里直接上代码
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
@ServerEndpoint(value = "/webSocket")
@Component
@Slf4j
public class WebSocketServer
{
/**
* 连接建立成功调用的方法
*
* @param session 可选的参数。session为与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
@OnOpen
public void onOpen(Session session) {
// System.out.println("enter onOpen**********");
//心跳等待时间1分钟
session.setMaxIdleTimeout(60000);
Map<String, List<String>> requestParameterMap = session.getRequestParameterMap();
List<String> roomList = requestParameterMap.get("room");
if (null != roomList)
{
String room = roomList.get(0);
// System.out.println("room="+room);
MySocket mySocket = new MySocket();
mySocket.setSocketId(Thread.currentThread().getId());
mySocket.setRoom(room);
mySocket.setSession(session);
}
}
private void sendRoomMsg(Session session, String room)
{
/**
* 1. 根据会议室名称从企业微信调用会议室预约信息
* 2. 根据网页展现需求构建需要的json数据
*/
WeiXinTemplate weiXinTemplate = (WeiXinTemplate)SpringUtil.getBean("weiXinTemplate");
String scheduleJson = weiXinTemplate.buildMeetingroomBookingInfo(room);
// System.out.println("sendRoomMsg="+scheduleJson);
sendMessage(scheduleJson, session);
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose()
{
// System.out.println("onClose*******");
}
/**
* 收到客户端消息后调用的方法
*
* @param room 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String room, Session session)
{
// System.out.println("onMessage*******");
sendRoomMsg(session, room);
}
/**
* 发生错误时调用
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error)
{
// System.out.println("onError="+error);
try
{
if (session.isOpen())
{
session.close();
}
}
catch (IOException e)
{
e.printStackTrace();
log.error(e.getMessage(), e);
}
finally
{
}
}
/**
* 这个方法与上面几个方法不一样。没有用注解,是根据自己需要添加的方法。
*
* @param message
* @throws IOException
*/
public static void sendMessage(String message, Session session)
{
if (null != session && session.isOpen()) {
session.getAsyncRemote().sendText(message);
}
}
private Object getFieldInstance(Object obj, String fieldPath)
{
String[] fields = fieldPath.split("#");
for (String field : fields)
{
obj = getField(obj, obj.getClass(), field);
if (obj == null)
{
return null;
}
}
return obj;
}
private Object getField(Object obj, Class<?> clazz, String fieldName)
{
for (; clazz != Object.class; clazz = clazz.getSuperclass())
{
try
{
Field field;
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(obj);
}
catch (Exception e)
{
}
}
return null;
}
}
以下贴出关键代码,其他业务根据情况调整
/**
* 构造前端需要的会议室预定信息
* @param room
* @return
*/
public String buildMeetingroomBookingInfo(String room)
{
String accessToken = getAccessTokenFromRoom(WeiXinApiConstant.CORP_ID,WeiXinApiConstant.MEETINGROOM_CORPSECRET);
// #1.查询会议室,并构造名称和id映射关系
Map<String, Integer> roomMap = new HashMap<>();
String roomlistStr = getMeetingroomList(accessToken);
// #2.根据meetingroom_id查询会议室的预定信息
JSONArray schedule_id_list = getMeetingroomBookingInfo(meetingroom_id,accessToken);
//#3 此处是业务处理,主要获取当天所有预订,按时间升序排列,取前2个,查看第一个时间是否在当前时间内,若是,就是会议中状态
// #4 构建会议室属性
JSONObject roomJson = new JSONObject();
roomJson.put("status", status);
roomJson.put("current",currentRoom);
roomJson.put("next",nextRoom);
return roomJson.toJSONString();
}
| 方法名 | 企业微信接口 |
|---|---|
| getAccessTokenFromRoom | https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=ID&corpsecret=SECRET |
| getMeetingroomList | https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/list?access_token=ACCESS_TOKEN |
| getMeetingroomBookingInfo | https://qyapi.weixin.qq.com/cgi-bin/oa/meetingroom/get_booking_info?access_token=ACCESS_TOKEN |
小结
1. 企业微信token获取,避免token过期和频繁调用,默认有效期是7200秒,可以用redis做缓存
2.获取的会议预订信息一定要重新按时间排序,这里我是按时间升序重新排序计算
3. 企业微信接口给的时间戳是秒为单位
4. cropID和会议室应用的secret要注意,不是从一个地方获取,不清楚的参看 企业微信扫码登录业务实现


1043

被折叠的 条评论
为什么被折叠?



