feature/simcards_checker #2

Merged
admin merged 5 commits from feature/simcards_checker into develop 2025-06-03 08:29:08 +00:00
18 changed files with 1188 additions and 173 deletions

View File

@ -12,4 +12,8 @@ public interface GnssDeviceMapper extends MPJBaseMapper<GnssDevice> {
@Update({"update gnssdevices set syn=false where group_id=#{group_id}"}) @Update({"update gnssdevices set syn=false where group_id=#{group_id}"})
int setSynFlagByGroupId(int group_id); int setSynFlagByGroupId(int group_id);
@Update({"update gnssdevices set iccid=#{iccid} where deviceid=#{deviceId}"})
int updateIccidByDeviceId(String deviceId, String iccid);
} }

View File

@ -0,0 +1,59 @@
package com.imdroid.secapi.dto;
import com.alibaba.excel.annotation.ExcelProperty;
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;
import java.util.Date;
@Data
@TableName("traffic_cards")
public class TrafficCard {
public static final int STATUS_UNKNOWN = -1; // 未知
public static final int STATUS_WAIT_ACTIVE = 1; // 待激活
public static final int STATUS_ACTIVATED = 2; // 已激活
public static final int STATUS_SUSPENDED = 3; // 停机
public static final int STATUS_CANCELLED = 4; // 注销
public static final int STATUS_IN_STOCK = 5; // 库存
public static final int STATUS_TESTABLE = 6; // 可测试
public static final int STATUS_INVALID = 7; // 失效
public static final int STATUS_NOT_EXIST = 99; // 号码不存在
// 查询状态常量定义
public static final int QUERY_STATUS_NORMAL = 0; // 正常状态
public static final int QUERY_STATUS_NOT_CURRENT_VENDOR = 1; // 非当前卡商
public static final int QUERY_STATUS_OTHER_ERROR = 2; // 其他错误
@TableId(type = IdType.AUTO)
@ExcelProperty("ID")
private Integer id;
@ExcelProperty("ICCID")
private String iccid;
@ExcelProperty("物联卡号码")
private String msisdn;
@ExcelProperty("状态")
private Integer status;
@ExcelProperty("剩余流量(MB)")
private Integer remaining;
@ExcelProperty("总流量(MB)")
private Integer total;
@ExcelProperty("已用流量(MB)")
private Integer used;
@TableField("update_time")
@ExcelProperty("更新时间")
private Date updateTime;
@TableField("query_status")
@ExcelProperty("查询状态")
private Integer queryStatus;
}

View File

@ -0,0 +1,37 @@
package com.imdroid.secapi.dto;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@Mapper
public interface TrafficCardMapper extends BaseMapper<TrafficCard> {
@Select("select * from traffic_cards where iccid = #{iccid} limit 1")
TrafficCard findByIccid(String iccid);
@Update("UPDATE traffic_cards SET " +
"update_time = #{updateTime}, " +
"msisdn = #{msisdn}, " +
"status = #{status}, " +
"remaining = #{remaining}, " +
"total = #{total}, " +
"used = #{used} " +
"WHERE iccid = #{iccid}")
int updateCardInfo(TrafficCard trafficCard);
@Update("UPDATE traffic_cards SET query_status = #{queryStatus} WHERE iccid = #{iccid}")
int updateQueryStatus(String iccid,int queryStatus);
@Update("UPDATE traffic_cards SET " +
"update_time = #{updateTime}, " +
"remaining = #{remaining}, " +
"total = #{total}, " +
"used = #{used} " +
"WHERE iccid = #{iccid} AND iccid IS NOT NULL AND iccid != ''")
int updateCardTrafficInfo(TrafficCard trafficCard);
}

View File

@ -0,0 +1,27 @@
package com.imdroid.secapi.dto;
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;
import java.util.Date;
@Data
@TableName("traffic_device_mappings")
public class TrafficDeviceMapping {
@TableId(type = IdType.AUTO)
private Integer id;
private String deviceid;
private String iccid;
@TableField("start_time")
private Date startTime;
@TableField("end_time")
private Date endTime; // 结束使用时间NULL表示当前正在使用
}

View File

@ -0,0 +1,24 @@
package com.imdroid.secapi.dto;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
@Mapper
public interface TrafficDeviceMappingMapper extends BaseMapper<TrafficDeviceMapping> {
@Select("SELECT * FROM traffic_device_mappings WHERE deviceid = #{deviceId} AND end_time IS NULL LIMIT 1")
TrafficDeviceMapping findActiveByDeviceId(String deviceId);
@Select("SELECT * FROM traffic_device_mappings WHERE iccid = #{iccid} AND end_time IS NULL LIMIT 1")
TrafficDeviceMapping findActiveByIccid(String iccid);
@Select("SELECT * FROM traffic_device_mappings WHERE deviceid = #{deviceId} ORDER BY start_time DESC")
List<TrafficDeviceMapping> findHistoryByDeviceId(String deviceId);
@Update("UPDATE traffic_device_mappings SET end_time = NOW() WHERE id = #{id}")
int endMapping(Integer id);
}

View File

@ -0,0 +1,26 @@
package com.imdroid.secapi.dto;
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;
import java.util.Date;
@Data
@TableName("traffic_records")
public class TrafficRecord {
@TableId(type = IdType.AUTO)
private Integer id;
private String iccid;
@TableField("record_time")
private Date recordTime;
private Integer remaining; // 剩余流量(MB×1000)
private Integer used; // 已用流量(MB×1000)
private Integer total; // 总流量(MB×1000)
}

View File

@ -0,0 +1,10 @@
package com.imdroid.secapi.dto;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface TrafficRecordMapper extends BaseMapper<TrafficRecord> {
}

View File

@ -4,9 +4,7 @@ import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.imdroid.secapi.dto.GnssStatusJoin; import com.imdroid.secapi.dto.*;
import com.imdroid.secapi.dto.SimCard;
import com.imdroid.secapi.dto.SimCardsMapper;
import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPost;
@ -20,7 +18,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.*; import java.util.*;
@ -35,7 +32,10 @@ public class SimCardQueryServiceImpl implements SimCardQueryService{
private String KEY; private String KEY;
@Autowired @Autowired
SimCardsMapper simCardsMapper; TrafficCardMapper trafficCardMapper;
@Autowired
GnssDeviceMapper gnssDeviceMapper;
@Override @Override
public BaseResponse<CardInfoData> queryCardInfo(GnssStatusJoin device) { public BaseResponse<CardInfoData> queryCardInfo(GnssStatusJoin device) {
@ -59,20 +59,53 @@ public class SimCardQueryServiceImpl implements SimCardQueryService{
private <T> BaseResponse<T> executeQuery(GnssStatusJoin device, String path, Class<T> responseType) { private <T> BaseResponse<T> executeQuery(GnssStatusJoin device, String path, Class<T> responseType) {
try { try {
BaseResponse<T> response = queryByParams(device.getIccid(), path, responseType);
/*
系统中存在一些奇怪的卡
DTU发送AT指令查询 ICCID 返回的值比如是89861124224084565106但实际是 8986112422408456510B
从而导致卡商无法查询到该卡正确的方法是舍弃最后一位比如 8986112422408456510
所以当出现 " 无效的卡号 " 错误的时候应该尝试去舍弃最后一位 这样才是正确的 ICCID 号码
*/
if (shouldTryTruncatedIccid(response) && device.getIccid() != null && device.getIccid().length() > 1) {
String truncatedIccid = device.getIccid().substring(0, device.getIccid().length() - 1);
BaseResponse<T> retryResponse = queryByParams(truncatedIccid, path, responseType);
if (retryResponse != null && retryResponse.getStatus() == 1) {
updateGnssDeviceIccid(device, truncatedIccid);
device.setIccid(truncatedIccid);
return retryResponse;
}
}
return response;
} catch (Exception e) {
logger.error("查询失败: 设备={}, 错误={}", device.getDeviceid(), e.getMessage());
return null;
}
}
private <T> BaseResponse<T> queryByParams(String iccid, String path, Class<T> responseType) throws Exception {
Map<String, String> params = buildQueryParams(iccid);
String response = sendHttpPost(path, params);
logger.debug("查询响应: ICCID={}, 响应={}", iccid, response);
return parseResponse(response, responseType);
}
private Map<String, String> buildQueryParams(String iccid) {
Map<String, String> params = new HashMap<>(); Map<String, String> params = new HashMap<>();
params.put("username", USERNAME); params.put("username", USERNAME);
params.put("key", KEY); params.put("key", KEY);
params.put("card", device.getIccid()); params.put("card", iccid);
params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000)); params.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000));
params.put("signature", calculateSignature(params));
return params;
}
String signature = calculateSignature(params); private <T> BaseResponse<T> parseResponse(String response, Class<T> responseType) throws Exception {
params.put("signature", signature);
logger.info("Request params: {}", params);
String response = sendHttpPost(path, params);
logger.info("查询响应: 设备={}, ICCID={}, 响应={}",
device.getDeviceid(), device.getIccid(), response);
ObjectMapper mapper = new ObjectMapper(); ObjectMapper mapper = new ObjectMapper();
mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true); mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
@ -93,11 +126,13 @@ public class SimCardQueryServiceImpl implements SimCardQueryService{
JavaType type = mapper.getTypeFactory().constructParametricType(BaseResponse.class, responseType); JavaType type = mapper.getTypeFactory().constructParametricType(BaseResponse.class, responseType);
return mapper.readValue(response, type); return mapper.readValue(response, type);
} catch (Exception e) {
logger.error("查询失败: 设备={}, 错误={}", device.getDeviceid(), e.getMessage());
return null;
} }
private boolean shouldTryTruncatedIccid(BaseResponse<?> response) {
return response != null &&
(response.getStatus() == 0 || response.getStatus() == -6) &&
response.getMessage() != null &&
response.getMessage().contains("无效的卡号");
} }
private String calculateSignature(Map<String, String> params) { private String calculateSignature(Map<String, String> params) {
@ -147,30 +182,34 @@ public class SimCardQueryServiceImpl implements SimCardQueryService{
return device.getIccid() != null && !device.getIccid().trim().isEmpty(); return device.getIccid() != null && !device.getIccid().trim().isEmpty();
} }
public SimCard CreateOrUpdateSimCard(GnssStatusJoin device) { public TrafficCard createOrUpdateTrafficCard(GnssStatusJoin device) {
SimCard simCard = simCardsMapper.queryByDeviceId(device.getDeviceid()); TrafficCard card = trafficCardMapper.selectById(device.getIccid());
if (simCard == null) { if (card == null) {
simCard = createNewSimCard(device); card = new TrafficCard();
card.setIccid(device.getIccid());
card.setStatus(-1);
card.setUpdateTime(new Date());
trafficCardMapper.insert(card);
} }
return simCard; return card;
}
public SimCard createNewSimCard(GnssStatusJoin device) {
SimCard newCard = new SimCard();
newCard.setDeviceid(device.getDeviceid());
newCard.setUpdatetime(new Date());
newCard.setIccid(device.getIccid());
newCard.setStatus(-1);
newCard.setMsisdn("");
newCard.setRemaining(BigDecimal.ZERO);
newCard.setUsed(BigDecimal.ZERO);
newCard.setTotal(BigDecimal.ZERO);
simCardsMapper.insert(newCard);
return newCard;
} }
public boolean isValidResponse(BaseResponse<?> response) { public boolean isValidResponse(BaseResponse<?> response) {
return response != null && response.getStatus() == 1; return response != null && response.getStatus() == 1;
} }
private void updateGnssDeviceIccid(GnssStatusJoin device, String newIccid) {
try {
String originalIccid = device.getIccid();
GnssDevice gnssDevice = gnssDeviceMapper.queryByDeviceId(device.getDeviceid());
if (gnssDevice != null) {
gnssDeviceMapper.updateIccidByDeviceId(device.getDeviceid(), newIccid);
logger.debug("更新设备ICCID: 设备={}, 原ICCID={}, 新ICCID={}",
device.getDeviceid(), originalIccid, newIccid);
}
} catch (Exception e) {
logger.error("更新设备ICCID失败: 设备={}, 错误={}", device.getDeviceid(), e.getMessage());
}
}
} }

View File

@ -0,0 +1,104 @@
package com.imdroid.beidou_ehm.simcard;
import com.imdroid.secapi.dto.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
@Service
public class TrafficCardService {
@Autowired
private TrafficCardMapper trafficCardMapper;
@Autowired
private TrafficDeviceMappingMapper mappingMapper;
@Autowired
private GnssDeviceMapper deviceMapper;
@Transactional
public boolean checkAndHandleICCIDChanges(GnssStatusJoin device) {
if (device == null || device.getIccid() == null || device.getIccid().trim().isEmpty()) {
return false;
}
String currentIccid = device.getIccid().trim();
// 查询当前设备的活跃映射记录
TrafficDeviceMapping activeMapping = mappingMapper.findActiveByDeviceId(device.getDeviceid());
if (activeMapping == null) {
// 第一次为设备创建映射
createNewMapping(device.getDeviceid(), currentIccid);
return true;
} else if (!currentIccid.equals(activeMapping.getIccid())) {
// ICCID变更关闭旧映射创建新映射
closeExistingMapping(activeMapping);
createNewMapping(device.getDeviceid(), currentIccid);
return true;
}
return false;
}
public TrafficCard getOrCreateTrafficCard(String iccid) {
TrafficCard card = trafficCardMapper.findByIccid(iccid);
if (card == null) {
card = new TrafficCard();
card.setIccid(iccid);
card.setMsisdn("");
card.setStatus(TrafficCard.STATUS_UNKNOWN);
card.setRemaining(0);
card.setTotal(0);
card.setUsed(0);
card.setUpdateTime(new Date());
card.setQueryStatus(TrafficCard.QUERY_STATUS_NORMAL);
trafficCardMapper.insert(card);
}
return card;
}
@Transactional
public void updateDeviceSimMapping(String deviceId, String iccid) {
getOrCreateTrafficCard(iccid);
// 查询现有映射
TrafficDeviceMapping activeMapping = mappingMapper.findActiveByDeviceId(deviceId);
if (activeMapping != null) {
// 如果ICCID变化关闭现有映射并创建新映射
if (!iccid.equals(activeMapping.getIccid())) {
closeExistingMapping(activeMapping);
createNewMapping(deviceId, iccid);
}
} else {
// 没有现有映射创建新映射
createNewMapping(deviceId, iccid);
}
}
public void markCardAsNotCurrentVendor(String iccid) {
trafficCardMapper.updateQueryStatus(iccid, TrafficCard.QUERY_STATUS_NOT_CURRENT_VENDOR);
}
public void markCardAsQueryFailed(String iccid) {
trafficCardMapper.updateQueryStatus(iccid, TrafficCard.QUERY_STATUS_OTHER_ERROR);
}
private void createNewMapping(String deviceId, String iccid) {
TrafficDeviceMapping newMapping = new TrafficDeviceMapping();
newMapping.setDeviceid(deviceId);
newMapping.setIccid(iccid);
newMapping.setStartTime(new Date());
newMapping.setEndTime(null);
mappingMapper.insert(newMapping);
}
private void closeExistingMapping(TrafficDeviceMapping mapping) {
mappingMapper.endMapping(mapping.getId());
}
}

View File

@ -1,6 +1,5 @@
package com.imdroid.beidou_ehm.task; package com.imdroid.beidou_ehm.task;
import com.alibaba.excel.util.StringUtils;
import com.imdroid.beidou_ehm.service.WarningService; import com.imdroid.beidou_ehm.service.WarningService;
import com.imdroid.beidou_ehm.simcard.*; import com.imdroid.beidou_ehm.simcard.*;
import com.imdroid.common.util.ThreadManager; import com.imdroid.common.util.ThreadManager;
@ -13,8 +12,6 @@ import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled; import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*; import java.util.*;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -29,24 +26,33 @@ public class SimStatusChecker {
// 2. 每两小时任务 // 2. 每两小时任务
// a. QueryGprs 检查 SIM 流量 // a. QueryGprs 检查 SIM 流量
final Logger logger = LoggerFactory.getLogger(SimStatusChecker.class); final Logger logger = LoggerFactory.getLogger(SimStatusChecker.class);
@Autowired @Autowired
private GnssStatusMapper gnssStatusMapper; private GnssStatusMapper gnssStatusMapper;
@Autowired @Autowired
private SimCardsMapper simCardsMapper; private GnssDeviceMapper deviceMapper;
@Autowired
private TrafficCardMapper trafficCardMapper;
@Autowired
private TrafficRecordMapper trafficRecordMapper;
@Autowired
private WarningService warningService;
@Autowired
private TrafficCardService trafficCardService;
@Autowired @Autowired
private SimCardQueryServiceImpl simCardQueryServiceImpl; private SimCardQueryServiceImpl simCardQueryServiceImpl;
@Autowired
WarningService warningService;
// 每小时执行一次状态检查调度 // 每小时执行一次状态检查调度
@Scheduled(cron = "0 0 * * * ?") @Scheduled(cron = "0 0 * * * ?")
//@Scheduled(cron = "0 */10 * * * ?")
private void scheduleSimCardStatusCheck() { private void scheduleSimCardStatusCheck() {
List<GnssStatusJoin> onlineDevices = gnssStatusMapper.queryOnline(); List<GnssStatusJoin> onlineDevices = gnssStatusMapper.queryOnline();
logger.debug("当前在线设备数量: {}", onlineDevices.size()); // logger.debug("当前在线设备数量: {}", onlineDevices.size());
for (GnssStatusJoin onlineDevice : onlineDevices) { for (GnssStatusJoin onlineDevice : onlineDevices) {
int delay = Math.abs(onlineDevice.getDeviceid().hashCode() % 3600 ); int delay = Math.abs(onlineDevice.getDeviceid().hashCode() % 3600 );
//logger.debug("- 设备: {}, SIM状态查询延迟执行: {}秒", onlineDevice.getDeviceid(), delay); //logger.debug("- 设备: {}, SIM状态查询延迟执行: {}秒", onlineDevice.getDeviceid(), delay);
@ -79,13 +85,23 @@ public class SimStatusChecker {
private void checkDeviceSimCardStatus(GnssStatusJoin device) { private void checkDeviceSimCardStatus(GnssStatusJoin device) {
try { try {
// 不允许尚未从自检得到 ICCID 的设备参加 SIM 卡状态查询
if (!simCardQueryServiceImpl.hasValidIccid(device)) { if (!simCardQueryServiceImpl.hasValidIccid(device)) {
return; return;
} }
// SimCards 表中没有数据就初始化这其实说明它在自检中刚获得属于自己的 ICCID boolean iccidChanged = trafficCardService.checkAndHandleICCIDChanges(device);
SimCard simCard = simCardQueryServiceImpl.CreateOrUpdateSimCard(device); if (iccidChanged) {
updateSimCardInfo(device, simCard); logger.info("设备 {} ICCID变更已处理: {}", device.getDeviceid(), device.getIccid());
}
TrafficCard trafficCard = trafficCardService.getOrCreateTrafficCard(device.getIccid());
// 如果 MSISDN 为空说明是首次创建的记录要先去查询基本信息
if (trafficCard.getMsisdn() == null || trafficCard.getMsisdn().isEmpty()) {
updateTrafficCardBasicInfoFromAPI(device, trafficCard);
}
// 查询SIM卡状态
updateTrafficCardStatusFromAPI(device, trafficCard);
} catch (Exception e) { } catch (Exception e) {
logger.error("设备{}状态查询失败: ", device.getDeviceid(), e); logger.error("设备{}状态查询失败: ", device.getDeviceid(), e);
@ -94,42 +110,63 @@ public class SimStatusChecker {
private void checkDeviceSimCardTraffic(GnssStatusJoin device) { private void checkDeviceSimCardTraffic(GnssStatusJoin device) {
try { try {
// 不允许尚未从自检得到 ICCID 的设备参加 SIM 卡状态查询
if (!simCardQueryServiceImpl.hasValidIccid(device)) { if (!simCardQueryServiceImpl.hasValidIccid(device)) {
return; return;
} }
// SimCards 表中没有数据就初始化这说明它在自检中获得属于自己的 ICCID
SimCard simCard = simCardQueryServiceImpl.CreateOrUpdateSimCard(device); TrafficCard trafficCard = trafficCardService.getOrCreateTrafficCard(device.getIccid());
// 如果该卡状态不是已激活而是其他状态那么不运行它参与 SIM 卡流量检测
if(simCard.getStatus() != SimCard.STATUS_ACTIVATED){ // 检查查询状态如果已标记为非当前卡商或查询失败则跳过
if (trafficCard.getQueryStatus() != null &&
trafficCard.getQueryStatus() != TrafficCard.QUERY_STATUS_NORMAL) {
logger.debug("设备 {} 的SIM卡 {} 查询状态为 {},跳过流量查询",
device.getDeviceid(),
device.getIccid(),
trafficCard.getQueryStatus());
return; return;
} }
updateSimCardTrafficFromAPI(device, simCard);
// 如果该卡状态不是已激活跳过流量查询
if(trafficCard.getStatus() != TrafficCard.STATUS_ACTIVATED) {
logger.debug("设备 {} 的SIM卡 {} 状态为 {},跳过流量查询",
device.getDeviceid(),
device.getIccid(),
trafficCard.getStatus());
return;
}
// 查询流量信息
try {
updateTrafficCardTrafficFromAPI(device, trafficCard);
} catch (Exception e) {
// 如果查询失败标记为查询失败状态
if (e.getMessage() != null && e.getMessage().contains("不属于当前卡商")) {
trafficCardService.markCardAsNotCurrentVendor(device.getIccid());
} else {
trafficCardService.markCardAsQueryFailed(device.getIccid());
}
throw e;
}
} catch (Exception e) { } catch (Exception e) {
logger.error("设备{}查询失败: ", device.getDeviceid(), e); logger.error("设备{}流量查询失败: ", device.getDeviceid(), e);
} }
} }
private void updateSimCardInfo(GnssStatusJoin device, SimCard simCard) throws Exception { private void updateTrafficCardBasicInfoFromAPI(GnssStatusJoin device, TrafficCard trafficCard) {
// 自检中只是获取保存了设备的 ICCID 所有如果判断如果没有 MSISDN先更新基本信息
if (StringUtils.isBlank(simCard.getMsisdn())) {
updateSimCardBasicInfoFromAPI(device, simCard);
}
// 更新状态
updateSimCardStatusFromAPI(device, simCard);
}
private void updateSimCardBasicInfoFromAPI(GnssStatusJoin device, SimCard simCard) {
try { try {
BaseResponse<CardInfoData> response = simCardQueryServiceImpl.queryCardInfo(device); BaseResponse<CardInfoData> response = simCardQueryServiceImpl.queryCardInfo(device);
if (!simCardQueryServiceImpl.isValidResponse(response)) { if (response == null || response.getStatus() != 1 || response.getData() == null) {
logger.warn("设备 {} 的SIM卡基本信息查询失败: {}",
device.getDeviceid(),
response != null ? response.getMessage() : "无响应");
return; return;
} }
CardInfoData info = response.getData(); CardInfoData info = response.getData();
simCard.setUpdatetime(new Date()); trafficCard.setUpdateTime(new Date());
simCard.setMsisdn(info.getMsisdn()); trafficCard.setMsisdn(info.getMsisdn());
simCardsMapper.updateSimCardInfo(simCard); trafficCardMapper.updateCardInfo(trafficCard);
logger.debug("更新SIM卡基本信息 - imsi: {}, msisdn: {}, iccid: {}", logger.debug("更新SIM卡基本信息 - imsi: {}, msisdn: {}, iccid: {}",
info.getImsi(), info.getMsisdn(), info.getIccid()); info.getImsi(), info.getMsisdn(), info.getIccid());
@ -138,31 +175,38 @@ public class SimStatusChecker {
throw e; throw e;
} }
} }
private void updateSimCardStatusFromAPI(GnssStatusJoin device, SimCard simCard) {
private void updateTrafficCardStatusFromAPI(GnssStatusJoin device, TrafficCard trafficCard) {
try { try {
BaseResponse<CardStatusData> response = simCardQueryServiceImpl.queryCardStatus(device); BaseResponse<CardStatusData> response = simCardQueryServiceImpl.queryCardStatus(device);
if (!simCardQueryServiceImpl.isValidResponse(response)) { if (response == null || response.getStatus() != 1 || response.getData() == null) {
logger.warn("设备 {} 的SIM卡状态查询失败: {}",
device.getDeviceid(),
response != null ? response.getMessage() : "无响应");
return; return;
} }
CardStatusData status = response.getData();
simCard.setUpdatetime(new Date());
simCard.setStatus(status.getStatusCode());
simCardsMapper.updateCardStatusInfo(simCard);
checkSimCardStatus(device, simCard); CardStatusData status = response.getData();
trafficCard.setUpdateTime(new Date());
trafficCard.setStatus(status.getStatusCode());
trafficCardMapper.updateCardInfo(trafficCard);
checkTrafficCardStatus(device, trafficCard);
logger.debug("更新SIM卡状态 - Code: {}, 描述: {}", logger.debug("更新SIM卡状态 - Code: {}, 描述: {}",
status.getStatusCode(), status.getStatusDesc()); status.getStatusCode(), status.getStatusDesc());
} catch (Exception e) { } catch (Exception e) {
logger.error("更新设备{}的SIM卡状态失败: ", device.getDeviceid(), e); logger.error("更新设备{}的SIM卡状态失败: ", device.getDeviceid(), e);
// throw e;
} }
} }
private void updateSimCardTrafficFromAPI(GnssStatusJoin device, SimCard simCard) { private void updateTrafficCardTrafficFromAPI(GnssStatusJoin device, TrafficCard trafficCard) {
try { try {
BaseResponse<GprsData> response = simCardQueryServiceImpl.queryGprs(device); BaseResponse<GprsData> response = simCardQueryServiceImpl.queryGprs(device);
if (!simCardQueryServiceImpl.isValidResponse(response)) { if (response == null || response.getStatus() != 1 || response.getData() == null) {
logger.warn("设备 {} 的SIM卡流量查询失败: {}",
device.getDeviceid(),
response != null ? response.getMessage() : "无响应");
return; return;
} }
@ -173,43 +217,55 @@ public class SimStatusChecker {
return; return;
} }
simCard.setUpdatetime(new Date()); trafficCard.setUpdateTime(new Date());
simCard.setRemaining(BigDecimal.valueOf(usage.getLeft())); // 将浮点数转换为整数存储MB值×1000
simCard.setUsed(BigDecimal.valueOf(usage.getUsed())); trafficCard.setRemaining((int)(usage.getLeft() * 1000));
simCard.setTotal(BigDecimal.valueOf(usage.getTotal())); trafficCard.setUsed((int)(usage.getUsed() * 1000));
simCardsMapper.updateCardTrafficInfo(simCard); trafficCard.setTotal((int)(usage.getTotal() * 1000));
checkSimCardTraffic(device, simCard); trafficCardMapper.updateCardTrafficInfo(trafficCard);
TrafficRecord record = new TrafficRecord();
record.setIccid(trafficCard.getIccid());
record.setRecordTime(new Date());
record.setRemaining(trafficCard.getRemaining());
record.setTotal(trafficCard.getTotal());
record.setUsed(trafficCard.getUsed());
trafficRecordMapper.insert(record);
checkTrafficCardTraffic(device, trafficCard);
logger.debug("更新流量信息成功 - deviceId: {}, 剩余: {}MB, 总量: {}MB, 已用: {}MB", logger.debug("更新流量信息成功 - deviceId: {}, 剩余: {}MB, 总量: {}MB, 已用: {}MB",
device.getIccid(), device.getIccid(),
simCard.getRemaining(), trafficCard.getRemaining() / 1000.0,
simCard.getTotal(), trafficCard.getTotal() / 1000.0,
simCard.getUsed()); trafficCard.getUsed() / 1000.0);
} catch (Exception e) { } catch (Exception e) {
logger.error("设备{}更新SIM卡流量失败: ", device.getDeviceid(), e); logger.error("设备{}更新SIM卡流量失败: ", device.getDeviceid(), e);
//throw e; throw e;
} }
} }
public void checkSimCardTraffic(GnssStatusJoin device, SimCard simCard) { public void checkTrafficCardTraffic(GnssStatusJoin device, TrafficCard trafficCard) {
GnssStatus status = gnssStatusMapper.getByDeviceId(device.getDeviceid()); GnssStatus status = gnssStatusMapper.getByDeviceId(device.getDeviceid());
if (status == null) return; if (status == null) return;
boolean isUpdated = false; boolean isUpdated = false;
BigDecimal usedPercentage = simCard.getUsed() // 计算流量使用百分比
.divide(simCard.getTotal(), 4, RoundingMode.HALF_UP) int usedPercentage = 0;
.multiply(BigDecimal.valueOf(100)); if (trafficCard.getTotal() > 0) {
usedPercentage = (int)((trafficCard.getUsed() * 100.0) / trafficCard.getTotal());
}
// 检查流量使用情况 // 检查流量使用情况
if (warningService.check(status, WarningCfg.TYPE_SIM_LOW_TRAFFIC, if (warningService.check(status, WarningCfg.TYPE_SIM_LOW_TRAFFIC,
WarningCfg.TYPE_NAME_SIM_LOW_TRAFFIC, WarningCfg.TYPE_NAME_SIM_LOW_TRAFFIC,
false, // 大于等于流量门限值那么就报警 false, // 大于等于流量门限值那么就报警
usedPercentage.intValue(), usedPercentage,
null, null,
String.format("流量已使用 %.2f%%", usedPercentage.doubleValue()))) { String.format("流量已使用 %d%%", usedPercentage))) {
isUpdated = true; isUpdated = true;
} }
@ -219,27 +275,26 @@ public class SimStatusChecker {
} }
} }
// 检查SIM卡状态 public void checkTrafficCardStatus(GnssStatusJoin device, TrafficCard trafficCard) {
public void checkSimCardStatus(GnssStatusJoin device, SimCard simCard) {
GnssStatus status = gnssStatusMapper.getByDeviceId(device.getDeviceid()); GnssStatus status = gnssStatusMapper.getByDeviceId(device.getDeviceid());
if (status == null) return; if (status == null) return;
boolean isUpdated = false; boolean isUpdated = false;
// 检查SIM卡状态是否异常停机注销失效 // 检查SIM卡状态是否异常停机注销失效
if (simCard.getStatus() == SimCard.STATUS_SUSPENDED || if (trafficCard.getStatus() == TrafficCard.STATUS_SUSPENDED ||
simCard.getStatus() == SimCard.STATUS_CANCELLED || trafficCard.getStatus() == TrafficCard.STATUS_CANCELLED ||
simCard.getStatus() == SimCard.STATUS_INVALID) { trafficCard.getStatus() == TrafficCard.STATUS_INVALID) {
String statusDesc; String statusDesc;
switch(simCard.getStatus()) { switch(trafficCard.getStatus()) {
case SimCard.STATUS_SUSPENDED: case TrafficCard.STATUS_SUSPENDED:
statusDesc = "停机"; statusDesc = "停机";
break; break;
case SimCard.STATUS_CANCELLED: case TrafficCard.STATUS_CANCELLED:
statusDesc = "注销"; statusDesc = "注销";
break; break;
case SimCard.STATUS_INVALID: case TrafficCard.STATUS_INVALID:
statusDesc = "失效"; statusDesc = "失效";
break; break;
default: default:
@ -248,12 +303,13 @@ public class SimStatusChecker {
if (warningService.check(status, WarningCfg.TYPE_SIM_STATUS_ABNORMAL, if (warningService.check(status, WarningCfg.TYPE_SIM_STATUS_ABNORMAL,
WarningCfg.TYPE_NAME_SIM_STATUS_ABNORMAL, false, WarningCfg.TYPE_NAME_SIM_STATUS_ABNORMAL, false,
simCard.getStatus(), null, trafficCard.getStatus(),
"SIM卡状态: " + statusDesc)) { null,
"SIM卡状态异常: " + statusDesc)) {
isUpdated = true; isUpdated = true;
} }
} else if (simCard.getStatus() == SimCard.STATUS_ACTIVATED) { } else {
// 状态正常已激活清除告警 // 清除状态异常告警
if ((status.getWarningcode() & WarningCfg.TYPE_SIM_STATUS_ABNORMAL) != 0) { if ((status.getWarningcode() & WarningCfg.TYPE_SIM_STATUS_ABNORMAL) != 0) {
warningService.clearWarning(status, WarningCfg.TYPE_SIM_STATUS_ABNORMAL); warningService.clearWarning(status, WarningCfg.TYPE_SIM_STATUS_ABNORMAL);
isUpdated = true; isUpdated = true;
@ -265,5 +321,4 @@ public class SimStatusChecker {
gnssStatusMapper.updateById(status); gnssStatusMapper.updateById(status);
} }
} }
} }

View File

@ -18,4 +18,8 @@ app.format.date = yyyy-MM-dd
app.format.time = HH:mm:ss app.format.time = HH:mm:ss
app.format.datetime = yyyy-MM-dd HH:mm:ss app.format.datetime = yyyy-MM-dd HH:mm:ss
sim.url = http://120.78.169.220:8089
sim.username = gzyzdz
sim.key = 632629d1269a202c9d49a574623e4e4c
mybatis-plus.configuration.map-underscore-to-camel-case=false mybatis-plus.configuration.map-underscore-to-camel-case=false

View File

@ -30,6 +30,8 @@ public class APIController extends BasicController{
DeviceCacheCmdMapper cacheCmdMapper; DeviceCacheCmdMapper cacheCmdMapper;
@Autowired @Autowired
WarningMsgMapper warningMsgMapper; WarningMsgMapper warningMsgMapper;
@Autowired
GnssStatusMapper gnssStatusMapper;
/****** config ack *******/ /****** config ack *******/
@PostMapping(value = "/api/config_ack") @PostMapping(value = "/api/config_ack")
@ -151,6 +153,7 @@ public class APIController extends BasicController{
// 保存 // 保存
saveMsg(deviceId, tenantId,0xD312, uploadCmd, true); saveMsg(deviceId, tenantId,0xD312, uploadCmd, true);
return null; return null;
} }
@ -189,10 +192,8 @@ public class APIController extends BasicController{
// 保存 // 保存
saveMsg(deviceId, tenantId,cacheCmd.getMsgtype(), cacheCmd.getCmd(), true); saveMsg(deviceId, tenantId,cacheCmd.getMsgtype(), cacheCmd.getCmd(), true);
} }
// 设备上线后检查是否需要重新查询更新 ICCID
// 检查iccid
checkAndAskICCID(device); checkAndAskICCID(device);
return null; return null;
} }
@ -245,7 +246,19 @@ public class APIController extends BasicController{
} }
void checkAndAskICCID(GnssDevice device) { void checkAndAskICCID(GnssDevice device) {
if(device.getIccid() == null || device.getIccid().trim().isEmpty()) { GnssStatus status = gnssStatusMapper.getByDeviceId(device.getDeviceid());
// 1. 检查设备是否有 ICCID记录 初始化
// 2. 检查设备是否从离线恢复 防止更换 SIM
// 3. 如果两种情况都不满足则不查询 ICCID
boolean isDeviceReconnecting = status != null && status.getState() == GnssStatus.STATE_OFFLINE;
boolean isIccidEmpty = device.getIccid() == null || device.getIccid().trim().isEmpty();
if (!isDeviceReconnecting && !isIccidEmpty) {
return;
}
String sendCmd = "AT+ICCID"; String sendCmd = "AT+ICCID";
int msgType = 0xD310 + 10; // DTU int msgType = 0xD310 + 10; // DTU
short len = (short) (sendCmd.length() + 5); short len = (short) (sendCmd.length() + 5);
@ -254,7 +267,6 @@ public class APIController extends BasicController{
"01" + HexUtil.String2HexString(sendCmd); "01" + HexUtil.String2HexString(sendCmd);
rtcmClient.config(device.getDeviceid(), sendCmd); rtcmClient.config(device.getDeviceid(), sendCmd);
} }
}
void updateICCID(GnssDevice device, String dtuAck) { void updateICCID(GnssDevice device, String dtuAck) {
// 只检查 "ICCID:" 的十六进制部分 // 只检查 "ICCID:" 的十六进制部分
if(!dtuAck.contains("49434349443a")){ if(!dtuAck.contains("49434349443a")){
@ -262,7 +274,6 @@ public class APIController extends BasicController{
} }
String content = HexUtil.HexString2String(dtuAck); String content = HexUtil.HexString2String(dtuAck);
if(content.contains("+ICCID:")){ if(content.contains("+ICCID:")){
//System.out.println(content);
String iccid = content.substring(content.indexOf("+ICCID:") + 8).trim(); String iccid = content.substring(content.indexOf("+ICCID:") + 8).trim();
iccid = iccid.split("\r\n")[0].trim(); iccid = iccid.split("\r\n")[0].trim();
device.setIccid(iccid); device.setIccid(iccid);

View File

@ -34,7 +34,16 @@ public class SimCardController extends BasicController {
private String KEY; private String KEY;
@Autowired @Autowired
private SimCardsMapper simCardsMapper; private GnssDeviceMapper gnssDeviceMapper;
@Autowired
private TrafficCardMapper trafficCardMapper;
@Autowired
private TrafficDeviceMappingMapper trafficDeviceMappingMapper;
@Autowired
private TrafficRecordMapper trafficRecordMapper;
@RequestMapping("/page/sim_status") @RequestMapping("/page/sim_status")
public String simStatus(Model m, HttpSession session) { public String simStatus(Model m, HttpSession session) {
@ -43,6 +52,20 @@ public class SimCardController extends BasicController {
return "/page/sim_status"; return "/page/sim_status";
} }
@RequestMapping("/page/sim_traffic_records")
public String simTrafficRecords(Model m, HttpSession session) {
initModel(m, session);
return "/page/sim_traffic_records";
}
@RequestMapping("/page/sim_device_mapping")
public String simDeviceMapping(Model m, HttpSession session) {
initModel(m, session);
return "/page/sim_device_mapping";
}
@RequestMapping("/sim/list") @RequestMapping("/sim/list")
@ResponseBody @ResponseBody
public JSONObject list(HttpSession session, public JSONObject list(HttpSession session,
@ -51,37 +74,98 @@ public class SimCardController extends BasicController {
String searchType, String searchType,
String searchContent, String searchContent,
Integer status) { Integer status) {
Page<SimCard> pageable = new Page<>(page, limit);
QueryWrapper<SimCard> queryWrapper = new QueryWrapper<>();
QueryWrapper<GnssDevice> deviceQueryWrapper = new QueryWrapper<>();
if (!StringUtils.isEmpty(searchContent)) { if (!StringUtils.isEmpty(searchContent)) {
switch(searchType) { switch(searchType) {
case "deviceId": case "deviceId":
queryWrapper.like("deviceid", searchContent.trim()); deviceQueryWrapper.like("deviceid", searchContent.trim());
break; break;
case "iccid": case "iccid":
queryWrapper.like("iccid", searchContent.trim()); deviceQueryWrapper.like("iccid", searchContent.trim());
break; break;
case "simNumber": case "simNumber":
queryWrapper.like("msisdn", searchContent.trim()); // 通过SIM号查找对应的ICCID然后查询设备
TrafficCard cardByMsisdn = trafficCardMapper.selectOne(
new QueryWrapper<TrafficCard>().like("msisdn", searchContent.trim())
);
if (cardByMsisdn != null) {
deviceQueryWrapper.eq("iccid", cardByMsisdn.getIccid());
} else {
deviceQueryWrapper.eq("iccid", "");
}
break; break;
} }
} }
// 只查询有ICCID的设备
deviceQueryWrapper.isNotNull("iccid");
deviceQueryWrapper.ne("iccid", "");
if (status != null) { if (status != null) {
queryWrapper.eq("status", status); List<TrafficCard> cardsWithStatus = trafficCardMapper.selectList(
new QueryWrapper<TrafficCard>().eq("status", status)
);
if (cardsWithStatus.isEmpty()) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 0);
jsonObject.put("msg", "");
jsonObject.put("count", 0);
jsonObject.put("data", new ArrayList<>());
return jsonObject;
} }
queryWrapper.orderByDesc("updatetime"); List<String> iccids = new ArrayList<>();
IPage<SimCard> cs = simCardsMapper.selectPage(pageable, queryWrapper); for (TrafficCard card : cardsWithStatus) {
iccids.add(card.getIccid());
}
deviceQueryWrapper.in("iccid", iccids);
}
deviceQueryWrapper.orderByDesc("deviceid");
Page<GnssDevice> pageable = new Page<>(page, limit);
IPage<GnssDevice> devices = gnssDeviceMapper.selectPage(pageable, deviceQueryWrapper);
List<Map<String, Object>> enrichedData = new ArrayList<>();
for (GnssDevice device : devices.getRecords()) {
TrafficCard trafficCard = null;
if (device.getIccid() != null && !device.getIccid().trim().isEmpty()) {
trafficCard = trafficCardMapper.findByIccid(device.getIccid());
}
Map<String, Object> deviceData = new HashMap<>();
deviceData.put("id", device.getId());
deviceData.put("deviceid", device.getDeviceid());
deviceData.put("iccid", device.getIccid());
if (trafficCard != null) {
deviceData.put("msisdn", trafficCard.getMsisdn());
deviceData.put("status", trafficCard.getStatus());
deviceData.put("remaining", trafficCard.getRemaining());
deviceData.put("total", trafficCard.getTotal());
deviceData.put("used", trafficCard.getUsed());
deviceData.put("updateTime", trafficCard.getUpdateTime());
deviceData.put("queryStatus", trafficCard.getQueryStatus());
} else {
deviceData.put("msisdn", "-");
deviceData.put("status", TrafficCard.STATUS_UNKNOWN);
deviceData.put("remaining", 0);
deviceData.put("total", 0);
deviceData.put("used", 0);
deviceData.put("updateTime", null);
deviceData.put("queryStatus", TrafficCard.QUERY_STATUS_NORMAL);
}
enrichedData.add(deviceData);
}
JSONObject jsonObject = new JSONObject(); JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 0); jsonObject.put("code", 0);
jsonObject.put("msg", ""); jsonObject.put("msg", "");
jsonObject.put("count", cs.getTotal()); jsonObject.put("count", devices.getTotal());
jsonObject.put("data", cs.getRecords()); jsonObject.put("data", enrichedData);
System.out.println(jsonObject.toString());
return jsonObject; return jsonObject;
} }
@ -114,11 +198,11 @@ public class SimCardController extends BasicController {
if (content.trim().isEmpty()) { if (content.trim().isEmpty()) {
throw new IllegalArgumentException("设备ID不能为空"); throw new IllegalArgumentException("设备ID不能为空");
} }
SimCard simCard = simCardsMapper.queryByDeviceId(content.trim()); GnssDevice device = gnssDeviceMapper.queryByDeviceId(content.trim());
if (simCard == null) { if (device == null || device.getIccid() == null || device.getIccid().trim().isEmpty()) {
throw new IllegalArgumentException("未找到该设备ID对应的SIM卡信息"); throw new IllegalArgumentException("未找到该设备ID或设备没有ICCID信息");
} }
params.put("card", simCard.getIccid()); params.put("card", device.getIccid());
break; break;
case "simNumber": case "simNumber":
params.put("card", content); params.put("card", content);
@ -176,6 +260,166 @@ public class SimCardController extends BasicController {
} }
} }
@RequestMapping("/sim/traffic-records")
@ResponseBody
public JSONObject getTrafficRecords(HttpSession session,
int page,
int limit,
String searchType,
String searchContent) {
try {
Page<TrafficRecord> pageable = new Page<>(page, limit);
QueryWrapper<TrafficRecord> queryWrapper = new QueryWrapper<>();
if (!StringUtils.isEmpty(searchContent)) {
switch(searchType) {
case "deviceId":
GnssDevice device = gnssDeviceMapper.queryByDeviceId(searchContent.trim());
if (device != null && device.getIccid() != null && !device.getIccid().trim().isEmpty()) {
queryWrapper.eq("iccid", device.getIccid());
} else {
queryWrapper.eq("iccid", "");
}
break;
case "iccid":
queryWrapper.like("iccid", searchContent.trim());
break;
case "simNumber":
TrafficCard cardByMsisdn = trafficCardMapper.selectOne(
new QueryWrapper<TrafficCard>().like("msisdn", searchContent.trim())
);
if (cardByMsisdn != null) {
queryWrapper.eq("iccid", cardByMsisdn.getIccid());
} else {
queryWrapper.eq("iccid", "");
}
break;
}
}
queryWrapper.orderByDesc("record_time");
IPage<TrafficRecord> records = trafficRecordMapper.selectPage(pageable, queryWrapper);
List<Map<String, Object>> enrichedData = new ArrayList<>();
for (TrafficRecord record : records.getRecords()) {
Map<String, Object> recordData = new HashMap<>();
recordData.put("id", record.getId());
recordData.put("iccid", record.getIccid());
recordData.put("recordTime", record.getRecordTime());
recordData.put("remaining", record.getRemaining());
recordData.put("total", record.getTotal());
recordData.put("used", record.getUsed());
GnssDevice device = gnssDeviceMapper.selectOne(
new QueryWrapper<GnssDevice>().eq("iccid", record.getIccid())
);
if (device != null) {
recordData.put("deviceid", device.getDeviceid());
} else {
recordData.put("deviceid", "无绑定设备");
}
TrafficCard trafficCard = trafficCardMapper.findByIccid(record.getIccid());
if (trafficCard != null) {
recordData.put("msisdn", trafficCard.getMsisdn());
} else {
recordData.put("msisdn", "-");
}
enrichedData.add(recordData);
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 0);
jsonObject.put("msg", "");
jsonObject.put("count", records.getTotal());
jsonObject.put("data", enrichedData);
return jsonObject;
} catch (Exception e) {
JSONObject errorResponse = new JSONObject();
errorResponse.put("code", 1);
errorResponse.put("msg", "查询失败: " + e.getMessage());
errorResponse.put("count", 0);
errorResponse.put("data", new ArrayList<>());
return errorResponse;
}
}
@RequestMapping("/sim/device-mapping")
@ResponseBody
public JSONObject getDeviceMapping(HttpSession session,
int page,
int limit,
String searchType,
String searchContent) {
try {
Page<TrafficDeviceMapping> pageable = new Page<>(page, limit);
QueryWrapper<TrafficDeviceMapping> queryWrapper = new QueryWrapper<>();
if (!StringUtils.isEmpty(searchContent)) {
switch(searchType) {
case "deviceId":
queryWrapper.like("deviceid", searchContent.trim());
break;
case "iccid":
queryWrapper.like("iccid", searchContent.trim());
break;
case "simNumber":
TrafficCard cardByMsisdn = trafficCardMapper.selectOne(
new QueryWrapper<TrafficCard>().like("msisdn", searchContent.trim())
);
if (cardByMsisdn != null) {
queryWrapper.eq("iccid", cardByMsisdn.getIccid());
} else {
queryWrapper.eq("iccid", "");
}
break;
}
}
queryWrapper.orderByDesc("start_time");
IPage<TrafficDeviceMapping> mappings = trafficDeviceMappingMapper.selectPage(pageable, queryWrapper);
List<Map<String, Object>> enrichedData = new ArrayList<>();
for (TrafficDeviceMapping mapping : mappings.getRecords()) {
Map<String, Object> mappingData = new HashMap<>();
mappingData.put("id", mapping.getId());
mappingData.put("deviceId", mapping.getDeviceid());
mappingData.put("iccid", mapping.getIccid());
mappingData.put("startTime", mapping.getStartTime());
mappingData.put("endTime", mapping.getEndTime());
mappingData.put("isActive", mapping.getEndTime() == null);
// 查找对应的流量卡信息
TrafficCard trafficCard = trafficCardMapper.findByIccid(mapping.getIccid());
if (trafficCard != null) {
mappingData.put("msisdn", trafficCard.getMsisdn());
} else {
mappingData.put("msisdn", "-");
}
enrichedData.add(mappingData);
}
JSONObject jsonObject = new JSONObject();
jsonObject.put("code", 0);
jsonObject.put("msg", "");
jsonObject.put("count", mappings.getTotal());
jsonObject.put("data", enrichedData);
return jsonObject;
} catch (Exception e) {
JSONObject errorResponse = new JSONObject();
errorResponse.put("code", 1);
errorResponse.put("msg", "查询失败: " + e.getMessage());
errorResponse.put("count", 0);
errorResponse.put("data", new ArrayList<>());
return errorResponse;
}
}
private String calculateSignature(Map<String, String> params) { private String calculateSignature(Map<String, String> params) {
try { try {
List<String> paramList = new ArrayList<>(); List<String> paramList = new ArrayList<>();

View File

@ -396,3 +396,52 @@ CREATE TABLE IF NOT EXISTS `ehmconfig` (
`calcstathours` int DEFAULT NULL COMMENT '数据分析周期', `calcstathours` int DEFAULT NULL COMMENT '数据分析周期',
PRIMARY KEY (`updatetime`) PRIMARY KEY (`updatetime`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/***
*/
CREATE TABLE IF NOT EXISTS `traffic_cards` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,唯一',
`iccid` VARCHAR(20) NOT NULL COMMENT 'ICCID号唯一标识SIM卡',
`msisdn` VARCHAR(20) NOT NULL COMMENT '物联卡号码',
`status` INT DEFAULT -1 COMMENT 'SIM卡状态-1:未知, 1:待激活, 2:已激活, 3:停机, 4:注销, 5:库存, 6:可测试, 7:失效, 99:号码不存在)',
`remaining` INT DEFAULT 0 COMMENT '剩余流量单位MB为避免小数存储值为实际值的1000倍',
`total` INT DEFAULT 0 COMMENT '总流量单位MB同上',
`used` INT DEFAULT 0 COMMENT '已用流量单位MB同上',
`update_time` DATETIME DEFAULT NULL COMMENT '最后更新时间',
`query_status` TINYINT DEFAULT 0 COMMENT 'SIM卡查询状态0=正常1=非当前卡商2=其他错误',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_iccid` (`iccid`),
INDEX `idx_status` (`status`),
INDEX `idx_query_status` (`query_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流量卡信息表';
/***
-SIM卡映射表
*/
CREATE TABLE IF NOT EXISTS `traffic_device_mappings` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,唯一',
`deviceid` VARCHAR(20) NOT NULL COMMENT '设备ID',
`iccid` VARCHAR(20) NOT NULL COMMENT 'SIM卡ICCID',
`start_time` DATETIME NOT NULL COMMENT '开始使用时间',
`end_time` DATETIME DEFAULT NULL COMMENT '结束使用时间NULL表示当前正在使用',
PRIMARY KEY (`id`),
KEY `idx_deviceid` (`deviceid`),
KEY `idx_iccid` (`iccid`),
KEY `idx_start_time` (`start_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备-SIM卡映射表';
/***
使
*/
CREATE TABLE IF NOT EXISTS `traffic_records` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键,自增,唯一',
`iccid` VARCHAR(20) NOT NULL COMMENT 'SIM卡ICCID',
`record_time` DATETIME NOT NULL COMMENT '记录时间',
`remaining` INT DEFAULT NULL COMMENT '剩余流量单位MB为避免小数存储值为实际值的1000倍',
`used` INT DEFAULT NULL COMMENT '已用流量单位MB为避免小数存储值为实际值的1000倍',
`total` INT DEFAULT NULL COMMENT '总流量单位MB为避免小数存储值为实际值的1000倍',
PRIMARY KEY (`id`),
KEY `idx_iccid` (`iccid`),
KEY `idx_record_time` (`record_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='流量使用记录表';

View File

@ -130,7 +130,19 @@
"target": "_self" "target": "_self"
}, },
{ {
"title": "卡信息查询", "title": "流量使用记录",
"href": "page/sim_traffic_records",
"icon": "fa fa-minus",
"target": "_self"
},
{
"title": "设备映射记录",
"href": "page/sim_device_mapping",
"icon": "fa fa-minus",
"target": "_self"
},
{
"title": "卡信息聚合查询",
"href": "page/sim_traffic_query", "href": "page/sim_traffic_query",
"icon": "fa fa-minus", "icon": "fa fa-minus",
"target": "_self" "target": "_self"

View File

@ -0,0 +1,164 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>设备SIM卡映射记录</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="../lib/layui-v2.6.3/css/layui.css" media="all">
<link rel="stylesheet" href="../css/public.css" media="all">
</head>
<body>
<div class="layuimini-container">
<div class="layuimini-main">
<fieldset class="table-search-fieldset">
<legend>搜索信息</legend>
<div style="margin: 10px 10px 10px 10px">
<form class="layui-form layui-form-pane" action="" id="searchForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">查询类型</label>
<div class="layui-input-inline">
<select name="searchType" lay-verify="required">
<option value="deviceId">设备号</option>
<option value="iccid">ICCID</option>
<option value="simNumber">SIM卡号</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">搜索内容</label>
<div class="layui-input-inline">
<input type="text" name="searchContent" id="searchInput" autocomplete="off" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="searchSubmit"><i class="layui-icon">&#xe615;</i> 搜 索</button>
</div>
</div>
</form>
</div>
</fieldset>
<div class="layui-tab layui-tab-card" lay-filter="data-tab">
<ul class="layui-tab-title">
<li class="layui-this">设备映射记录</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<table class="layui-hide" id="deviceMappingTable" lay-filter="deviceMappingFilter"></table>
</div>
</div>
</div>
</div>
</div>
<script type="text/html" id="statusTpl">
{{# if(d.isActive){ }}
<span class="layui-badge layui-bg-green">使用中</span>
{{# } else { }}
<span class="layui-badge">已结束</span>
{{# } }}
</script>
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
<script th:inline="javascript">
layui.use(['form', 'table','layer','element'], function () {
var table = layui.table
,form = layui.form
,layer = layui.layer
,element = layui.element;
var data_cols = [
{field: 'deviceId', title: '设备号'},
{field: 'iccid', title: 'ICCID'},
{field: 'msisdn', title: 'SIM卡号'},
{field: 'startTime', title: '开始时间', templet: "<div>{{layui.util.toDateString(d.startTime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'endTime', title: '结束时间', templet: function(d) {
return d.endTime ? layui.util.toDateString(d.endTime, 'yyyy-MM-dd HH:mm:ss') : '-';
}},
{field: 'isActive', title: '状态', templet: '#statusTpl'},
{field: 'duration', title: '使用时长', templet: function(d) {
if (!d.startTime) return '-';
var start = new Date(d.startTime);
var end = d.endTime ? new Date(d.endTime) : new Date();
var diffMs = end - start;
if (diffMs < 0) return '-';
var days = Math.floor(diffMs / (1000 * 60 * 60 * 24));
var hours = Math.floor((diffMs % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
if (days > 0) {
return days + '天' + hours + '小时';
} else if (hours > 0) {
return hours + '小时';
} else {
var minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60));
return minutes + '分钟';
}
}}
];
form.render();
table.render({
elem: '#deviceMappingTable',
url: '/sim/device-mapping',
defaultToolbar: ['filter'],
cols: [
data_cols
],
limits: [20, 50, 100, 200, 300],
limit: 20,
page: true,
skin: 'line',
done: function(res) {
if(res.code !== 0) {
layer.msg(res.msg || '查询失败', {icon: 2});
}
}
});
form.verify({
searchContent: function(value, item) {
var type = $('select[name="searchType"]').val();
if(!value) {
return '请输入搜索内容';
}
if(type === 'iccid' && !/^\d{19,20}$/.test(value)) {
return 'ICCID必须是19-20位数字';
}
if(type === 'simNumber' && !/^\d{11,13}$/.test(value)) {
return 'SIM卡号必须是11-13位数字';
}
}
});
form.on('submit(searchSubmit)', function(data){
var loadIndex = layer.load(1);
table.reload('deviceMappingTable', {
page: {
curr: 1
}
,where: {
searchType: data.field.searchType,
searchContent: data.field.searchContent
}
,done: function(res) {
layer.close(loadIndex);
if(res.code !== 0) {
layer.msg(res.msg || '查询失败', {icon: 2});
} else if(res.count === 0) {
layer.msg('未找到符合条件的数据', {icon: 0});
}
}
});
return false;
});
});
</script>
</body>
</html>

View File

@ -110,9 +110,9 @@
function updateFlowChart(simData) { function updateFlowChart(simData) {
initEcharts(); initEcharts();
var used = parseFloat(simData.used) || 0; var used = simData.used ? (simData.used / 1000) : 0;
var remaining = parseFloat(simData.remaining) || 0; var remaining = simData.remaining ? (simData.remaining / 1000) : 0;
var total = parseFloat(simData.total) || (used + remaining); var total = simData.total ? (simData.total / 1000) : (used + remaining);
var option = { var option = {
title: { title: {
@ -191,11 +191,17 @@
{field: 'deviceid', title: '设备号'}, {field: 'deviceid', title: '设备号'},
{field: 'iccid', title: 'ICCID'}, {field: 'iccid', title: 'ICCID'},
{field: 'msisdn', title: 'SIM 卡号'}, {field: 'msisdn', title: 'SIM 卡号'},
{field: 'updatetime', title: '更新时间',templet: "<div>{{layui.util.toDateString(d.updatetime, 'yyyy-MM-dd HH:mm:ss')}}</div>"}, {field: 'updateTime', title: '更新时间',templet: "<div>{{layui.util.toDateString(d.updateTime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'status', title: '状态',templet: '#statusTpl'}, {field: 'status', title: '状态',templet: '#statusTpl'},
{field: 'remaining', title: '剩余流量(MB)'}, {field: 'remaining', title: '剩余流量(MB)', templet: function(d) {
{field: 'used', title: '已使用流量(MB)'}, return d.remaining ? (d.remaining / 1000).toFixed(2) : '0';
{field: 'total', title: '总流量(MB)'} }},
{field: 'used', title: '已使用流量(MB)', templet: function(d) {
return d.used ? (d.used / 1000).toFixed(2) : '0';
}},
{field: 'total', title: '总流量(MB)', templet: function(d) {
return d.total ? (d.total / 1000).toFixed(2) : '0';
}}
]; ];

View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SIM卡流量使用记录</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<link rel="stylesheet" href="../lib/layui-v2.6.3/css/layui.css" media="all">
<link rel="stylesheet" href="../css/public.css" media="all">
</head>
<body>
<div class="layuimini-container">
<div class="layuimini-main">
<fieldset class="table-search-fieldset">
<legend>搜索信息</legend>
<div style="margin: 10px 10px 10px 10px">
<form class="layui-form layui-form-pane" action="" id="searchForm">
<div class="layui-form-item">
<div class="layui-inline">
<label class="layui-form-label">查询类型</label>
<div class="layui-input-inline">
<select name="searchType" lay-verify="required">
<option value="deviceId">设备号</option>
<option value="iccid">ICCID</option>
<option value="simNumber">SIM卡号</option>
</select>
</div>
</div>
<div class="layui-inline">
<label class="layui-form-label">搜索内容</label>
<div class="layui-input-inline">
<input type="text" name="searchContent" id="searchInput" autocomplete="off" class="layui-input">
</div>
</div>
</div>
<div class="layui-form-item">
<div class="layui-inline">
<button type="submit" class="layui-btn layui-btn-primary" lay-submit lay-filter="searchSubmit"><i class="layui-icon">&#xe615;</i> 搜 索</button>
</div>
</div>
</form>
</div>
</fieldset>
<div class="layui-tab layui-tab-card" lay-filter="data-tab">
<ul class="layui-tab-title">
<li class="layui-this">流量使用记录</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<table class="layui-hide" id="trafficRecordsTable" lay-filter="trafficRecordsFilter"></table>
</div>
</div>
</div>
</div>
</div>
<script src="../lib/layui-v2.6.3/layui.js" charset="utf-8"></script>
<script th:inline="javascript">
layui.use(['form', 'table','layer','element'], function () {
var table = layui.table
,form = layui.form
,layer = layui.layer
,element = layui.element;
var data_cols = [
{field: 'deviceid', title: '设备号'},
{field: 'iccid', title: 'ICCID'},
{field: 'msisdn', title: 'SIM卡号'},
{field: 'recordTime', title: '记录时间', templet: "<div>{{layui.util.toDateString(d.recordTime, 'yyyy-MM-dd HH:mm:ss')}}</div>"},
{field: 'remaining', title: '剩余流量(MB)', templet: function(d) {
return d.remaining ? (d.remaining / 1000).toFixed(2) : '0';
}},
{field: 'used', title: '已使用流量(MB)', templet: function(d) {
return d.used ? (d.used / 1000).toFixed(2) : '0';
}},
{field: 'total', title: '总流量(MB)', templet: function(d) {
return d.total ? (d.total / 1000).toFixed(2) : '0';
}}
];
form.render();
table.render({
elem: '#trafficRecordsTable',
url: '/sim/traffic-records',
defaultToolbar: ['filter'],
cols: [
data_cols
],
limits: [20, 50, 100, 200, 300],
limit: 20,
page: true,
skin: 'line',
done: function(res) {
if(res.code !== 0) {
layer.msg(res.msg || '查询失败', {icon: 2});
}
}
});
form.verify({
searchContent: function(value, item) {
var type = $('select[name="searchType"]').val();
if(!value) {
return '请输入搜索内容';
}
if(type === 'iccid' && !/^\d{19,20}$/.test(value)) {
return 'ICCID必须是19-20位数字';
}
if(type === 'simNumber' && !/^\d{11,13}$/.test(value)) {
return 'SIM卡号必须是11-13位数字';
}
}
});
form.on('submit(searchSubmit)', function(data){
var loadIndex = layer.load(1);
table.reload('trafficRecordsTable', {
page: {
curr: 1
}
,where: {
searchType: data.field.searchType,
searchContent: data.field.searchContent
}
,done: function(res) {
layer.close(loadIndex);
if(res.code !== 0) {
layer.msg(res.msg || '查询失败', {icon: 2});
} else if(res.count === 0) {
layer.msg('未找到符合条件的数据', {icon: 0});
}
}
});
return false;
});
});
</script>
</body>
</html>