Merge branch 'dev'

新增服务监控功能。
master
wangsiyuan 2023-12-09 11:25:40 +08:00
commit db949ace34
17 changed files with 472 additions and 3 deletions

View File

@ -35,3 +35,19 @@ CREATE TABLE api_setting(
INSERT INTO api_setting(id,phone_number,access_token,msgtype,touser,agentid,enable_duplicate_check,duplicate_check_interval) INSERT INTO api_setting(id,phone_number,access_token,msgtype,touser,agentid,enable_duplicate_check,duplicate_check_interval)
VALUES (1,'18281561650','gKGCDSgWV82XbU0H','textcard','@all',1000002,1,1800); VALUES (1,'18281561650','gKGCDSgWV82XbU0H','textcard','@all',1000002,1,1800);
CREATE TABLE device_info (
id INT PRIMARY KEY AUTO_INCREMENT,
is_Monitored INT,
time_out_period BIGINT,
device_brand VARCHAR(255),
android_version VARCHAR(255),
sn VARCHAR(255),
device_model VARCHAR(255),
last_online_time BIGINT,
status INT
);
INSERT INTO device_info (is_monitored, time_out_period, device_brand, android_version, sn, device_model, last_online_time, status) VALUES
(1, 600, 'google', 'Android 10', UNIX_TIMESTAMP(), 1),

View File

@ -3,8 +3,10 @@ package com.kimgo.wepush;
import org.mybatis.spring.annotation.MapperScan; import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication @SpringBootApplication
@EnableScheduling
@MapperScan("com.kimgo.wepush.mapper") @MapperScan("com.kimgo.wepush.mapper")
public class WePushApplication { public class WePushApplication {

View File

@ -0,0 +1,32 @@
package com.kimgo.wepush.controller;
import com.kimgo.wepush.model.CallInfo;
import com.kimgo.wepush.model.DeviceInfo;
import com.kimgo.wepush.response.ServerResponseEntity;
import com.kimgo.wepush.service.HeartBeatService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HeartbeatController {
@Autowired
private HeartBeatService heartBeatService;
@PostMapping("/heartbeat")
public ServerResponseEntity receiveHeartbeat(@RequestHeader("accessToken") String accessToken,
@RequestBody @Valid DeviceInfo deviceInfo) {
// 处理心跳请求
// 更新客户端的“最后活跃时间”
if(accessToken == null){
return ServerResponseEntity.fail("accessToken cannot be empty.");
}
if (deviceInfo == null || deviceInfo.hasInvalidFields()){
return ServerResponseEntity.fail("json body value error.");
}
return heartBeatService.handleHeartbeatSignal(accessToken,deviceInfo);
}
}

View File

@ -2,6 +2,7 @@ package com.kimgo.wepush.mapper;
import com.kimgo.wepush.model.ApiSetting; import com.kimgo.wepush.model.ApiSetting;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/** /**
* @author wangsiyuan * @author wangsiyuan
@ -9,6 +10,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
* @createDate 2023-12-05 01:25:35 * @createDate 2023-12-05 01:25:35
* @Entity com.kimgo.wepush.model.ApiSetting * @Entity com.kimgo.wepush.model.ApiSetting
*/ */
@Mapper
public interface ApiSettingMapper extends BaseMapper<ApiSetting> { public interface ApiSettingMapper extends BaseMapper<ApiSetting> {
} }

View File

@ -0,0 +1,10 @@
package com.kimgo.wepush.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kimgo.wepush.model.ApiSetting;
import com.kimgo.wepush.model.DeviceInfoDAO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DeviceInfoDAOMapper extends BaseMapper<DeviceInfoDAO> {
}

View File

@ -2,6 +2,8 @@ package com.kimgo.wepush.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kimgo.wepush.model.QyWeChatAppInfo; import com.kimgo.wepush.model.QyWeChatAppInfo;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface QyWeChatAppInfoMapper extends BaseMapper<QyWeChatAppInfo> { public interface QyWeChatAppInfoMapper extends BaseMapper<QyWeChatAppInfo> {
} }

View File

@ -4,5 +4,6 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.kimgo.wepush.model.QyWeChatURL; import com.kimgo.wepush.model.QyWeChatURL;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface QyWeChatURLMapper extends BaseMapper<QyWeChatURL> { public interface QyWeChatURLMapper extends BaseMapper<QyWeChatURL> {
} }

View File

@ -0,0 +1,24 @@
package com.kimgo.wepush.model;
import lombok.Data;
@Data
public class DeviceInfo {
private String androidVersion;
private String deviceBrand;
private String SN;
private String deviceModel;
public boolean hasInvalidFields() {
return isNullOrInvalid(androidVersion) || isNullOrInvalid(SN) || isNullOrInvalid(deviceModel) || isNullOrInvalid(deviceBrand);
}
/**
* null"null"
*
* @param value
* @return null"null" true false
*/
private boolean isNullOrInvalid(String value) {
return value == null || value.equals("null") || value.isEmpty();
}
}

View File

@ -0,0 +1,23 @@
package com.kimgo.wepush.model;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("device_info")
public class DeviceInfoDAO {
@TableId(type = IdType.AUTO)
private Integer id;
private int isMonitored;
private long timeOutPeriod;
private String deviceBrand;
private String androidVersion;
@TableField("sn")
private String SN;
private String deviceModel;
private long lastOnlineTime;
private int status;
}

View File

@ -0,0 +1,60 @@
package com.kimgo.wepush.request;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.kimgo.wepush.model.TextCardMessage;
import com.kimgo.wepush.response.QyWeChatSendMessageApiResponse;
import com.kimgo.wepush.service.QyWeChatURLService;
import com.kimgo.wepush.service.TokenService;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.RequestBody;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
public class Request {
private final Logger logger = LoggerFactory.getLogger(Request.class);
public <T> QyWeChatSendMessageApiResponse okhttpRequest(String url,String accessToken,T object){
OkHttpClient client = new OkHttpClient();
// 使用Jackson进行序列化
ObjectMapper objectMapper = new ObjectMapper();
String jsonBody = null;
try {
jsonBody = objectMapper.writeValueAsString(object);
logger.info("jsonBody: {}",jsonBody);
} catch (JsonProcessingException e) {
logger.error("JSON processing error", e);
return null;
}
// 构建请求体
RequestBody body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), jsonBody);
okhttp3.Request request = new okhttp3.Request.Builder().url(url).post(body).build();
try (Response response = client.newCall(request).execute()) {
String responseBody = response.body().string();
logger.info("request sendMessage api ResponseBody: {}", responseBody);
ObjectMapper objectMapper1 = new ObjectMapper();
QyWeChatSendMessageApiResponse apiResponse = objectMapper1.readValue(responseBody, QyWeChatSendMessageApiResponse.class);
if (apiResponse.getErrcode() == 0) {
logger.debug("Request qyWeChat Success.");
return apiResponse;
} else if (apiResponse.getErrcode() == 42001 || apiResponse.getErrcode() == 40014) {
logger.info("Access token expired. Refreshing token...");
// 一个方法来刷新accessToken
return apiResponse;
} else {
// 处理其他错误情况
logger.error("Error: {}", apiResponse.getErrmsg());
return null;
}
} catch (IOException e) {
logger.error("OkHttp request error", e);
return null;
}
}
}

View File

@ -62,8 +62,9 @@ public class ServerResponseEntity <T> implements Serializable {
public static <T> ServerResponseEntity<T> success(){ public static <T> ServerResponseEntity<T> success(){
ServerResponseEntity<T> serverResponseEntity = new ServerResponseEntity<>(); ServerResponseEntity<T> serverResponseEntity = new ServerResponseEntity<>();
serverResponseEntity.setCode(ResponseEnum.OK.value()); serverResponseEntity.setCode(ResponseEnum.OK.value());
serverResponseEntity.getMsg(); serverResponseEntity.setMsg(ResponseEnum.OK.getMsg());
serverResponseEntity.setTimestamp(Instant.now().getEpochSecond()); serverResponseEntity.setTimestamp(Instant.now().getEpochSecond());
serverResponseEntity.setData(null);
return serverResponseEntity; return serverResponseEntity;
} }

View File

@ -2,7 +2,6 @@ package com.kimgo.wepush.service;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.kimgo.wepush.common.PhoneNumberValidation;
import com.kimgo.wepush.response.QyWeChatSendMessageApiResponse; import com.kimgo.wepush.response.QyWeChatSendMessageApiResponse;
import com.kimgo.wepush.model.CallInfo; import com.kimgo.wepush.model.CallInfo;
import com.kimgo.wepush.model.TextCardMessage; import com.kimgo.wepush.model.TextCardMessage;

View File

@ -0,0 +1,119 @@
package com.kimgo.wepush.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.kimgo.wepush.mapper.DeviceInfoDAOMapper;
import com.kimgo.wepush.model.DeviceInfo;
import com.kimgo.wepush.model.DeviceInfoDAO;
import com.kimgo.wepush.response.ServerResponseEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class HeartBeatService {
private final Logger logger = LoggerFactory.getLogger(HeartBeatService.class);
private final long TIME_OUT_PERIOD = 600;
@Autowired
TokenService tokenService;
@Autowired
private DeviceInfoDAOMapper deviceInfoDAOMapper;
public ServerResponseEntity handleHeartbeatSignal(String accessToken,DeviceInfo deviceInfo) {
String correctAccessToken = tokenService.getApiAccessToken();
logger.info("accessToken: {} correctAccessToken: {}",accessToken,correctAccessToken);
if (!correctAccessToken.equals(accessToken)){
return ServerResponseEntity.fail("Invalid accessToken");
}
DeviceInfoDAO deviceInfoDAO = getDeviceInfoBySN(deviceInfo.getSN());
if (deviceInfoDAO == null) {
// 设备信息不存在,可能是无效的设备
boolean isSuccess = addDeviceInfo(deviceInfo);
if (isSuccess){
return ServerResponseEntity.success();
} else {
logger.info("add to mysql error");
}
}
try {
updateClientStatus(deviceInfo);
return ServerResponseEntity.success();
} catch (Exception e) {
logger.error("update client status error.");
// 处理更新状态时的异常
return ServerResponseEntity.fail("Error updating client status");
}
}
public void updateClientStatus(DeviceInfo deviceInfo) {
long currentTimeMillis = System.currentTimeMillis();
// 更新该客户端的最后活跃时间
UpdateWrapper<DeviceInfoDAO> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("sn", deviceInfo.getSN());
updateWrapper.set("last_online_time", currentTimeMillis); // 设置新的 accessToken
int result = deviceInfoDAOMapper.update(null, updateWrapper);
if (result > 0) {
logger.debug("Update successful");
} else {
logger.warn("Update failed: No rows affected");
}
}
public boolean addDeviceInfo(DeviceInfo deviceInfo) {
try {
DeviceInfoDAO deviceInfoDAO = convertToDeviceInfoDAO(deviceInfo);
int result = deviceInfoDAOMapper.insert(deviceInfoDAO);
logger.info("result: {}",result);
return result > 0;
} catch (Exception e) {
logger.error("Error adding device info", e);
return false;
}
}
/**
* androidId DeviceInfo
*
* @param SN SN
* @return androidId DeviceInfoDAO null
*/
public DeviceInfoDAO getDeviceInfoBySN(String SN) {
QueryWrapper<DeviceInfoDAO> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("sn", SN);
try {
DeviceInfoDAO deviceInfoDAO = deviceInfoDAOMapper.selectOne(queryWrapper);
if (deviceInfoDAO != null) {
logger.info("Data is queried in the database based on the device id");
return deviceInfoDAO;
}
} catch (Exception e) {
logger.error("Error querying device info by android ID", e);
}
return null;
}
/**
* DeviceInfo DeviceInfoDAO
*
* @param deviceInfo DeviceInfo
* @return DeviceInfoDAO
*/
private DeviceInfoDAO convertToDeviceInfoDAO(DeviceInfo deviceInfo) {
// 这里需要实现将 DeviceInfo 转换为 DeviceInfoDAO 的逻辑
// 示例代码(根据实际字段调整):
DeviceInfoDAO deviceInfoDAO = new DeviceInfoDAO();
deviceInfoDAO.setIsMonitored(1);
deviceInfoDAO.setDeviceBrand(deviceInfo.getDeviceBrand());
deviceInfoDAO.setSN(deviceInfo.getSN());
deviceInfoDAO.setDeviceModel(deviceInfo.getDeviceModel());
deviceInfoDAO.setAndroidVersion(deviceInfo.getAndroidVersion());
deviceInfoDAO.setTimeOutPeriod(TIME_OUT_PERIOD);
deviceInfoDAO.setLastOnlineTime(System.currentTimeMillis());
deviceInfoDAO.setStatus(1);
return deviceInfoDAO;
}
}

View File

@ -0,0 +1,131 @@
package com.kimgo.wepush.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.kimgo.wepush.mapper.DeviceInfoDAOMapper;
import com.kimgo.wepush.model.DeviceInfoDAO;
import com.kimgo.wepush.model.TextCardMessage;
import com.kimgo.wepush.request.Request;
import com.kimgo.wepush.response.QyWeChatSendMessageApiResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.List;
@Service
public class MonitorService {
@Autowired
private ApiSettingService apiSettingService;
@Autowired
private QyWeChatURLService qyWeChatURLService;
@Autowired
private TokenService tokenService;
private final Logger logger = LoggerFactory.getLogger(MonitorService.class);
@Autowired
private DeviceInfoDAOMapper deviceInfoDAOMapper;
@Scheduled(fixedDelay = 480000)
public void monitorOnlineDevices() {
List<DeviceInfoDAO> devices = getAllDevices();
if (devices.isEmpty()) {
// 没有找到数据的处理逻辑
logger.info("no data was queried.");
}
for (DeviceInfoDAO device : devices) {
try {
if (device.getIsMonitored() != 1){
logger.info("device is not monitored ,device SN: {}",device.getSN());
continue;
}
long currentTimeMillis = System.currentTimeMillis();
if (currentTimeMillis - device.getLastOnlineTime() > device.getTimeOutPeriod()) {
updateStatus(device, 0); // 设备离线
sendNotification(device);
} else {
updateStatus(device, 1); // 设备在线
}
} catch (Exception e) {
logger.error("Error processing device with SN " + device.getSN(), e);
}
}
}
/**
* DeviceInfoDAOid
*
* @return id
*/
private List<DeviceInfoDAO> getAllDevices() {
try {
// 查询所有DeviceInfoDAO对象
List<DeviceInfoDAO> deviceInfoDAOs = deviceInfoDAOMapper.selectList(new QueryWrapper<>());
// 提取并返回所有对象的id
return deviceInfoDAOs;
} catch (Exception e) {
logger.error("Error querying all device info IDs", e);
}
return Collections.emptyList();
}
private void sendNotification(DeviceInfoDAO deviceInfoDAO){
Request request = new Request();
String accessToken = tokenService.getAccessToken();
String url = qyWeChatURLService.getSendTextCardMessageUrl() + "?access_token=" + accessToken;
QyWeChatSendMessageApiResponse qyWeChatSendMessageApiResponse = request.okhttpRequest(url,accessToken,setTextCardMessage(deviceInfoDAO));
if (qyWeChatSendMessageApiResponse ==null){
logger.info("Send Notification Fail");
}
if (qyWeChatSendMessageApiResponse != null && qyWeChatSendMessageApiResponse.getErrcode() == 0) {
logger.info("Send Notification Success");
}
}
public TextCardMessage setTextCardMessage(DeviceInfoDAO deviceInfoDAO) {
TextCardMessage textCardMessage = new TextCardMessage();
textCardMessage.setTouser(apiSettingService.getApiSetting().getTouser());
textCardMessage.setMsgtype(apiSettingService.getApiSetting().getMsgtype());
textCardMessage.setAgentid(apiSettingService.getApiSetting().getAgentid());
textCardMessage.setEnable_duplicate_check(0);
TextCardMessage.TextCard textCard = new TextCardMessage.TextCard();
textCard.setTitle("设备掉线通知");
String formattedCurrentTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
textCard.setDescription("<div class='gray'>设备信息</div>" +
"<div class='highlight'>品牌: " + deviceInfoDAO.getDeviceBrand() + "</div>" +
"<div class='highlight'>设备型号: " + deviceInfoDAO.getDeviceModel() + "</div>" +
"<div class='highlight'>安卓版本: " + deviceInfoDAO.getAndroidVersion() + "</div>" +
"<div class='highlight'>SN: " + deviceInfoDAO.getSN() + "</div>" +
"<div class='highlight'>监测到掉线时间: " + formattedCurrentTime + "</div>" +
"检测到设备已掉线,请及时查看设备状态.");
textCard.setUrl("https://kimgo.cn");
// 将TextCard对象设置到TextCardMessage中
textCardMessage.setTextcard(textCard);
logger.info("TextCardMessage: {}", textCardMessage);
return textCardMessage;
}
private void updateStatus(DeviceInfoDAO device, int status) {
try {
UpdateWrapper<DeviceInfoDAO> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("SN", device.getSN());
updateWrapper.set("status", status);
int result = deviceInfoDAOMapper.update(null, updateWrapper);
if (result > 0) {
logger.debug("Update successful for SN: " + device.getSN());
} else {
logger.warn("Update failed: No rows affected for SN: " + device.getSN());
}
} catch (Exception e) {
logger.error("Error updating status for SN: " + device.getSN(), e);
}
}
}

View File

@ -18,7 +18,6 @@ public class QyWeChatURLService {
private QyWeChatURLMapper qyWeChatURLMapper; private QyWeChatURLMapper qyWeChatURLMapper;
@Autowired @Autowired
private UserConfig userConfig; private UserConfig userConfig;
private String urlName;
private String sendTextCardMessageUrl; private String sendTextCardMessageUrl;
private String qyWechatGetTokenUrl; private String qyWechatGetTokenUrl;

View File

@ -0,0 +1,31 @@
package com.kimgo.wepush.service;
import com.kimgo.wepush.model.DeviceInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class HeartBeatServiceTest {
@Autowired
HeartBeatService heartBeatService;
@Test
void updateClientStatus() {
DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.setDeviceModel("Model B");
deviceInfo.setSerialNumber("SN1234567891");
deviceInfo.setAndroidId("android_id_2");
heartBeatService.updateClientStatus(deviceInfo);
}
@Test
void addDeviceInfo() {
DeviceInfo deviceInfo = new DeviceInfo();
deviceInfo.setDeviceModel("Model C");
deviceInfo.setSerialNumber("SN1234567899");
deviceInfo.setAndroidId("dadwdwfcev");
heartBeatService.addDeviceInfo(deviceInfo);
}
}

View File

@ -0,0 +1,17 @@
package com.kimgo.wepush.service;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class MonitorServiceTest {
@Autowired
private MonitorService monitorService;
@Test
void monitorOnlineDevices() {
monitorService.monitorOnlineDevices();
}
}