
文章目录
一、背景
XNMS(Extended Network Management System,增强型网络管理系统)是一款远程监控和管理常规中转台的软件。中转台是系统的核心设备,所有业务都通过其进行中转。因此,只要对中转台进行监控,就能全面掌握系统的运行状况。而中转台通常部署室外,容易受到日晒雨淋等自然条件影响,造成设备损坏。为保证通讯系统正常运行,工作人员需要对中转台进行实时监控,发现中转台的异常问题,从而采取相关措施进行补救。
通过XNMS软件,工作人员可实时监控常规中转台的各项参数和告警情况,对异常问题进行排查;还可以查询或统计某时间段内中转台或终端的业务,从而全面了解常规系统的运行状况。
项目采用:Arco Design+java+mysql+springboot+vue3
二、页面
中转台RSSI
统计指定时间段内,各中转台接收终端各级别RSSI的次数及其占比。



三、代码
TransferDeskRSSI.vue
java
<template>
<layout_1 :auto-height="true">
<div class="--search-line">
<div>
<div class="key">{{ $t(queryButtonValue[7]) }} </div>
<div class="val">
<a-range-picker
style="width: 280px"
:allow-clear="false"
:disabled-date="disabledDate"
v-model="param.timeRange"
>
<template #suffix-icon>
<svg-loader :width="20" :height="20" name="clock"></svg-loader>
</template>
<template #separator>
<svg-loader
:width="16"
:height="16"
name="arrow-right"
></svg-loader>
</template>
</a-range-picker>
</div>
</div>
<div>
<div class="key">{{ $t(queryButtonValue[8]) }} </div>
<div class="val">
<a-tree-select
class="arco-tree-select --arco-select"
style="width: 230px"
:field-names="{
key: 'serialNo',
title: 'name',
children: 'children',
}"
:data="treeSelectNodeData"
:multiple="true"
:tree-checkable="true"
tree-checked-strategy="child"
:max-tag-count="1"
v-model:model-value="param.repeaterSNs"
>
</a-tree-select>
</div>
</div>
<a-button class="huge" @click="search" type="primary">
<template #icon>
<icon-search size="18" /> </template
>{{ $t(queryButtonValue[1]) }}
</a-button>
<a-button class="huge" @click="resetSearch">
<template #icon>
<svg-loader
:width="20"
:height="20"
name="reset"
></svg-loader> </template
>{{ $t(queryButtonValue[21]) }}
</a-button>
</div>
<div class="statistics-box">
<a-scrollbar
outer-style="height: 100%; width: calc(100%)"
style="height: 100%; overflow: scroll"
>
<div v-for="item in dataList" :key="item.id">
<div class="title">
{{ item.rptSN }}({{ item.alias }})
</div>
<div class="content" id="transfer_business_content_box">
<div class="content-item box-1 inline">
<div class="content-item-title">
{{ $t(queryColumnValue[92]) }}
</div>
<div
:id="`business_rssi_level_times_bar_${item.rptSN}`"
class="chart-1"
></div>
</div>
<div class="content-item box-2 inline">
<div class="content-item-title">
{{ $t(queryColumnValue[93]) }}
</div>
<div
:id="`business_rssi_pie_${item.rptSN}`"
class="chart-2"
></div>
<div class="chart-2-tooltip">
<div class="chart-2-tooltip-item">
<div
class="circle inline"
:style="{ background: color.level_1 }"
></div>
<div class="text inline">Good</div>
<div class="sp-line inline"></div>
<div class="percentage inline">
{{ item.level_1?.toFixed(2) }}%
</div>
</div>
<div class="chart-2-tooltip-item">
<div
class="circle inline"
:style="{ background: color.level_2 }"
></div>
<div class="text inline">Normal</div>
<div class="sp-line inline"></div>
<div class="percentage inline">
{{ item.level_2?.toFixed(2) }}%
</div>
</div>
<div class="chart-2-tooltip-item">
<div
class="circle inline"
:style="{ background: color.level_3 }"
></div>
<div class="text inline">Average</div>
<div class="sp-line inline"></div>
<div class="percentage inline">
{{ item.level_3?.toFixed(2) }}
</div>
</div>
<div class="chart-2-tooltip-item">
<div
class="circle inline"
:style="{ background: color.level_4 }"
></div>
<div class="text inline">Bad</div>
<div class="sp-line inline"></div>
<div class="percentage inline">
{{ item.level_4?.toFixed(2) }}%
</div>
</div>
<div class="chart-2-tooltip-item">
<div
class="circle inline"
:style="{ background: color.level_5 }"
></div>
<div class="text inline">Very Bad</div>
<div class="sp-line inline"></div>
<div class="percentage inline">
{{ item.level_5?.toFixed(2) }}%
</div>
</div>
</div>
</div>
</div>
</div>
</a-scrollbar>
</div>
</layout_1>
</template>
<script setup>
import Layout_1 from "@/views/pages/_common/layout_1.vue";
import * as moment from "moment/moment";
import * as echarts from "echarts";
import { nextTick, reactive, ref, inject } from "vue";
import { qryTransferNodeList } from "@/views/pages/topology/_request";
import {
getRssiColor,
getRssiList,
} from "@/views/pages/business/_request";
import { commonResponse } from "@/views/pages/_common";
import {
queryButtonValue,
queryColumnValue,
} from "@/views/pages/_common/enum";
const t = inject("t");
const param = reactive({
timeRange: [null, null],
repeaterSNs: [],
});
let reqParam = {
startTime: null,
endTime: null,
repeaterSNs: [],
};
const treeSelectNodeData = ref([]);
const getTransferNodeList = () => {
const principal = sessionStorage.getItem("principal");
if (principal) {
const principalObject = JSON.parse(principal);
qryTransferNodeList({ userName: principalObject.userName }).then(
(response) => {
treeSelectNodeData.value = [
{
serialNo: "-1",
name: t(queryButtonValue[22]),
children: response.data,
},
];
}
);
}
};
const dataList = ref([]);
const tableLoading = ref(false);
const getAllData = () => {
tableLoading.value = true;
getRssiList({
...reqParam,
})
.then((response) => {
tableLoading.value = false;
commonResponse({
response,
onSuccess: () => {
dataList.value = response.data;
nextTick(() => {
initCharts();
});
},
});
})
.catch((e) => (tableLoading.value = true));
};
const initCharts = () => {
const chartList = [];
dataList.value?.forEach((item) => {
const { Good, Normal, Average, Bad, Very_Bad } = item.datas;
const total = Good + Normal + Average + Bad + Very_Bad;
item.level_1 = total ? (Good / total) * 100 : 25;
item.level_2 = total ? (Normal / total) * 100 : 25;
item.level_3 = total ? (Average / total) * 100 : 25;
item.level_4 = total ? (Bad / total) * 100 : 25;
item.level_5 = total ? (Very_Bad / total) * 100 : 25;
chartList.push(initLevelTimesBar(item.rptSN, item.datas));
chartList.push(initBusinessTimePie(item.rptSN, item.datas));
});
window.removeEventListener("resize", () => {});
window.addEventListener("resize", () => {
chartList.forEach((item) => item.resize({ width: "auto", height: "auto" }));
});
};
const color = reactive({
level_1: "",
level_2: "",
level_3: "",
level_4: "",
level_5: "",
});
const getColor = async () => {
return new Promise((resolve, reject) => {
getRssiColor().then((response) => {
commonResponse({
response,
onSuccess: () => {
color.level_1 = response.data[0].color;
color.level_2 = response.data[1].color;
color.level_3 = response.data[2].color;
color.level_4 = response.data[3].color;
color.level_5 = response.data[4].color;
resolve();
},
});
});
});
};
const initLevelTimesBar = (id, data) => {
const chartDom = document.querySelector(
`#business_rssi_level_times_bar_${id}`
);
const chart = echarts.init(chartDom);
const option = {
tooltip: {
trigger: "axis",
},
legend: {
itemGap: 20,
textStyle: {
color: "#202B40",
fontFamily: "PingFang SC",
fontSize: 13,
fontWeight: 400,
lineHeight: 22,
},
},
grid: {
left: "3%",
right: "4%",
bottom: "3%",
containLabel: true,
},
toolbox: {
feature: {
saveAsImage: {},
},
},
xAxis: {
type: "category",
boundaryGap: [40, 40],
data: ["Good", "Normal", "Average", "Bad", "Very Bad"],
},
yAxis: {
type: "value",
},
series: [
{
type: "bar",
data: [
{ value: data.Good, itemStyle: { color: color.level_1 } },
{ value: data.Normal, itemStyle: { color: color.level_2 } },
{ value: data.Average, itemStyle: { color: color.level_3 } },
{ value: data.Bad, itemStyle: { color: color.level_4 } },
{ value: data.Very_Bad, itemStyle: { color: color.level_5 } },
],
barWidth: 40,
},
],
};
option && chart.setOption(option);
return chart;
};
const initBusinessTimePie = (id, data) => {
const chartDom = document.querySelector(`#business_rssi_pie_${id}`);
const chart = echarts.init(chartDom);
const option = {
color: [
color.level_1,
color.level_2,
color.level_3,
color.level_4,
color.level_5,
],
tooltip: {
trigger: "item",
formatter: "{b} : {c}",
},
series: [
{
type: "pie",
radius: ["50%", "80%"],
padAngle: 0.5,
label: {
show: true,
alignTo: "labelLine",
position: "outer",
color: "#192840",
fontSize: 12,
fontWeight: 400,
lineHeight: 22,
fontFamily: "PingFang SC",
formatter: "{d}%",
},
labelLine: {
show: true,
length: 0,
length2: 40,
},
labelLayout: {
align: "center",
verticalAlign: "bottom",
},
data: [
{ value: data.Good, name: "Good" },
{ value: data.Normal, name: "Normal" },
{ value: data.Average, name: "Average" },
{ value: data.Bad, name: "Bad" },
{ value: data.Very_Bad, name: "Very Bad" },
],
},
],
};
option && chart.setOption(option);
return chart;
};
const search = () => {
reqParam = {
...reqParam,
...param,
};
reqParam.startTime = moment(reqParam.timeRange[0]).format(
"YYYY-MM-DDT00:00:00.000"
);
reqParam.endTime = moment(reqParam.timeRange[1]).format(
"YYYY-MM-DDT23:59:59.999"
);
delete reqParam.timeRange;
getAllData();
};
const resetSearch = () => {
param.timeRange = [moment().add(-9, "days"), moment()];
param.repeaterSNs = [];
dataList.value = [];
};
const disabledDate = (date) => {
return date.getTime() > moment().format("x");
};
const init = async () => {
resetSearch();
await getColor();
getTransferNodeList();
};
init();
</script>
<style scoped lang="less">
.statistics-box {
box-sizing: border-box;
height: calc(100vh - 140px);
.title {
overflow: hidden;
color: #192840;
font-family: "PingFang SC";
font-size: 20px;
font-style: normal;
font-weight: 600;
line-height: 72px; /* 160% */
height: 72px;
}
:deep(.arco-scrollbar-track-direction-vertical) {
right: -20px;
}
.content {
width: 100%;
&-item {
position: relative;
box-sizing: border-box;
border: 1px solid #e5e7ec;
border-radius: 10px;
padding: 20px;
&.box-1 {
width: calc(100% - 454px);
.chart-1 {
height: 343px;
}
}
&.box-2 {
margin-left: 20px;
width: 434px;
height: 413px;
.chart-2 {
height: 240px;
}
}
&-title {
color: #192840;
font-family: "PingFang SC";
font-size: 18px;
font-style: normal;
font-weight: 600;
line-height: 28px; /* 155.556% */
}
.chart-2-tooltip {
position: absolute;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
left: 50%;
transform: translateX(-50%);
margin-top: 10px;
width: 330px;
&-item {
height: 22px;
min-width: 150px;
.circle {
margin-top: 6px;
border-radius: 5px;
height: 10px;
width: 10px;
}
.text {
margin-left: 8px;
min-width: 64px;
color: #202b40;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
.sp-line {
margin-top: 5px;
margin-left: 6px;
width: 1px;
height: 12px;
background-color: #dde4ed;
}
.percentage {
margin-left: 12px;
color: #7987a3;
font-family: "PingFang SC";
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 22px;
}
}
}
}
}
}
</style>
RssiStatisticController
java
package com.xnms.client.service.controller.data.statistic;
import com.xnms.client.service.ViewModel.StatRssiResult;
import com.xnms.client.service.controller.common.ResponseModel;
import com.xnms.client.service.view.naviagation.page.data.statistice.RssiStatisticUserControl;
import com.xnms.client.service.view.naviagation.page.data.statistice.StatisticSelectUserControl;
import com.xnms.data.contract.MultiResponse;
import com.xnms.data.contract.database.db.Pagination;
import com.xnms.data.contract.database.db.QueryConditionBase;
import com.xnms.data.contract.database.db.RssiLevelSetting;
import io.swagger.annotations.Api;
import io.swagger.v3.oas.annotations.Operation;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "业务统计 - 中转台RSSI")
@RestController
@RequestMapping("/api/rssi_statistic")
public class RssiStatisticController implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(RssiStatisticController.class);
@Autowired
private StatisticSelectUserControl statisticSelectUserControl;
@Autowired
private RssiStatisticUserControl rssiStatisticUserControl;
@Override
public void afterPropertiesSet() throws Exception {
}
@Operation(summary = "RSSI各级别次数,时长占比")
@PostMapping
public ResponseModel<List<StatRssiResult>> statisticRssi(@RequestBody QueryConditionBase queryConditionBase) {
logger.info("RssiStatisticController,statisticRssi:queryConditionBase = [{}]", queryConditionBase);
String msg = statisticSelectUserControl.verifyRssiStatisticQueryCondition(queryConditionBase, "Query_Operate_Exception");
if (!StringUtils.isBlank(msg)) {
return ResponseModel.ofError(msg);
}
MultiResponse<StatRssiResult> businessDetailDataResult = rssiStatisticUserControl.statisticRssi(queryConditionBase);
return ResponseModel.ofSuccess(businessDetailDataResult.getData(), Pagination.of(queryConditionBase.getCurrentPage(), queryConditionBase.getPageSize(), businessDetailDataResult.getTotalCount()));
}
@Operation(summary = "RSSI级别枚举(颜色在这里获取)")
@GetMapping(value = "/rssi_Level_setting")
public ResponseModel<List<RssiLevelSetting>> rssiLevelSettings() {
logger.info("RssiStatisticController,Rssi Start...");
MultiResponse<RssiLevelSetting> repeaterBusinessDetailDataResult = rssiStatisticUserControl.rssiLevelSettings();
return ResponseModel.ofSuccess(repeaterBusinessDetailDataResult.getData());
}
}
StacRssiDao
java
package com.xnms.data.service.dao;
import com.xnms.data.contract.database.db.StatRssi;
import com.xnms.data.service.entity.StacRssiEntity;
import java.util.Date;
import java.util.List;
public interface StacRssiDao {
/**
* 获取指定时间范围内的 StacRssi 实体列表
*
* @param fromTime 开始时间
* @param endTime 结束时间
* @return StacRssi 实体列表
*/
List<StacRssiEntity> getStacRssiEntityList(Date fromTime, Date endTime);
/**
* 获取所有 StacRssi 实体列表
*
* @return StacRssi 实体列表
*/
List<StacRssiEntity> getStacRssiEntityList();
/**
* 插入或更新 StacRssi 实体列表
*
* @param sqlList SQL 语句列表
*/
void insertOrUpdateStacRssiEntityList(List<String> sqlList);
/**
* 执行 StacRssi 操作
*
* @return 操作是否成功
*/
Boolean doStacRssi();
/**
* 获取 RSSI 统计数据
*
* @param startTime 开始时间
* @param endTime 结束时间
* @param repeaterSNs 重复器序列号列表
* @return RSSI 统计数据列表
*/
List<StatRssi> obtainRssiStatisticData(Date startTime, Date endTime, List<String> repeaterSNs);
/**
* 执行 StacRssi 操作并指定日期
*
* @param stacDate 指定日期
* @return 操作是否成功
*/
Boolean doStacRssi(Date stacDate, Date todayStart, Date tomorrowStart);
}
StacRssiDaoImpl
java
package com.xnms.data.service.dao.mysql.impl;
import com.xnms.data.contract.database.db.StatRssi;
import com.xnms.data.service.dao.StacRssiDao;
import com.xnms.data.service.entity.StacRssiEntity;
import com.xnms.data.service.util.DateUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
/**
*
*/
@Repository
public class StacRssiDaoImpl implements StacRssiDao {
private static final Logger logger = LoggerFactory.getLogger(StacRssiDaoImpl.class);
private static final SimpleDateFormat yyyyMMdd = new SimpleDateFormat("yyyy-MM-dd");
private static final SimpleDateFormat YMDHMS = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@PersistenceContext
private EntityManager entityManager;
private String getSql(String sqlBuilder) {
String sql = sqlBuilder;
if (sql.endsWith(";")) {
sql = sql.substring(0, sql.length() - 1);
}
sql = "SELECT UUID() as uuid, rbres.* from ( " + sql + " ) rbres";
return sql;
}
@Override
public List<StacRssiEntity> getStacRssiEntityList(Date fromTime, Date endTime) {
return null;
}
/**
* Retrieves a list of StacRssiEntity objects.
*
* @return a list of StacRssiEntity objects, or an empty list if no entities are found
*/
@Override
public List<StacRssiEntity> getStacRssiEntityList() {
return null;
}
/**
* Inserts or updates a list of SQL statements in the database.
*
* @param sqlList the list of SQL statements to be executed
*/
@Override
public void insertOrUpdateStacRssiEntityList(List<String> sqlList) {
try {
entityManager.getTransaction().begin();
for (String sql : sqlList) {
entityManager.createNativeQuery(sql).executeUpdate();
}
entityManager.getTransaction().commit();
} catch (Exception ex) {
if (entityManager.getTransaction().isActive()) {
entityManager.getTransaction().rollback();
}
throw ex;
}
}
@Transactional
@Override
public Boolean doStacRssi() {
List<String> sqlList = new ArrayList<>();
try {
for (int i = 0; i < 2; i++) {
String dateStr = DateUtil.addDaysToCurrentDate(i - 1); // 假设 DateUtil 提供的功能
String deleteSql = String.format("DELETE FROM stac_rssi_day WHERE date='%s';", dateStr);
// 假设你有一个 DateUtil.CovertStartTimeToTableName 方法
String tableName = DateUtil.convertStartTimeToTableName(yyyyMMdd.parse(dateStr));
String insertSql = String.format(
"INSERT INTO stac_rssi_day (rptsn, date, count, rssi) " +
"SELECT serial_no AS rptsn, DATE_FORMAT(starttime, '%%Y-%%m-%%d') AS date, " +
"count(*) AS count, rssi " +
"FROM %s " +
"WHERE DATE_FORMAT(starttime, '%%Y-%%m-%%d') = '%s' " +
"GROUP BY serial_no, date, rssi;",
tableName, dateStr
);
sqlList.add(deleteSql);
sqlList.add(insertSql);
}
// 执行 SQL 语句
if (!sqlList.isEmpty()) {
for (String sql : sqlList) {
Query query = entityManager.createNativeQuery(sql);
query.executeUpdate(); // 执行更新操作
}
}
return true;
} catch (Exception ex) {
logger.error("<StacRssiDaoImpl doStacRssi> Error: {}, SQL: {}", ex.getMessage(), sqlList.toString(), ex);
return false;
}
}
@Override
public List<StatRssi> obtainRssiStatisticData(Date startTime, Date endTime, List<String> repeaterSNs) {
List<StatRssi> rssiStatisticRecords = new ArrayList<>();
try {
// 构建 SQL 查询字符串
String sql = "SELECT rptsn, date, count, rssi FROM stac_rssi_day WHERE rptsn IN (:repeaterSNs) AND date >= :startDate AND date <= :endDate AND rssi != -200";
Query query = entityManager.createNativeQuery(getSql(sql), StatRssi.class);
// 设置参数
query.setParameter("startDate", yyyyMMdd.format(startTime));
query.setParameter("endDate", yyyyMMdd.format(endTime));
query.setParameter("repeaterSNs", repeaterSNs);
// 执行查询并获取结果
rssiStatisticRecords = query.getResultList();
} catch (Exception ex) {
// 日志记录
logger.error("[StacRssiDaoImpl] ObtainRssiStatisticData: {}", ex.getMessage(), ex);
}
return rssiStatisticRecords;
}
/**
* Main method that serves as the entry point of the application.
* This method attempts to perform a division by zero, which will result in an ArithmeticException.
*
* @param args command line arguments passed to the application
*/
public static void main(String[] args) {
int i = 1 / 0;
System.out.println(i);
}
@Transactional
@Override
public Boolean doStacRssi(Date stacDate, Date todayStart, Date tomorrowStart) {
List<String> sqlList = new ArrayList<>();
try {
// Create SQL list
String stacDateStr = yyyyMMdd.format(stacDate);
String deleteSql = String.format("DELETE FROM stac_rssi_day WHERE date = '%s';",
stacDateStr);
String tableName = DateUtil.convertStartTimeToTableName(stacDate);
String insertSql = String.format(
"INSERT INTO stac_rssi_day (rptsn, date, count, rssi) " +
"SELECT serial_no AS rptsn, DATE_FORMAT(starttime, '%%Y-%%m-%%d') AS date, " +
"count(*) AS count, rssi " +
"FROM %s " +
"WHERE STARTTIME between '%s' and '%s' " +
"GROUP BY serial_no, date, rssi;",
tableName, YMDHMS.format(todayStart), YMDHMS.format(tomorrowStart)
);
sqlList.add(deleteSql);
sqlList.add(insertSql);
// 执行 SQL 语句
if (!sqlList.isEmpty()) {
for (String sql : sqlList) {
Query query = entityManager.createNativeQuery(sql);
query.executeUpdate(); // 执行更新操作
}
}
return true;
} catch (Exception ex) {
logger.error("<StacRssiDaoImpl doStacRssi> Error: {}", ex.getMessage(), ex);
return false;
}
}
}