
文章目录
一、背景
XNMS(Extended Network Management System,增强型网络管理系统)是一款远程监控和管理常规中转台的软件。中转台是系统的核心设备,所有业务都通过其进行中转。因此,只要对中转台进行监控,就能全面掌握系统的运行状况。而中转台通常部署室外,容易受到日晒雨淋等自然条件影响,造成设备损坏。为保证通讯系统正常运行,工作人员需要对中转台进行实时监控,发现中转台的异常问题,从而采取相关措施进行补救。
通过XNMS软件,工作人员可实时监控常规中转台的各项参数和告警情况,对异常问题进行排查;还可以查询或统计某时间段内中转台或终端的业务,从而全面了解常规系统的运行状况。
项目采用:Arco Design+java+mysql+springboot+vue3
二、页面
终端业务
统计和导出指定时间段内,所有终端的业务信息,包括次数、总时长及其占比。终端业务类型包括语音业务、GNSS业务、短消息业务、注册服务业务和其他业务。
- 按表统计:选择需要统计的日期和业务类型,单击"统计",生成统计列表。该列表显示所有终端的业务信息。
- 按图统计:生成统计列表后,单击列表中的某个终端, 报表下方左侧折线图显示该终端的语音业务信息;右侧折线图显示该终端的数据业务信息,包括GNSS业务、短消息业务、注册服务业务和其他业务。如下图所示。
备注:您可将鼠标移动至图中某个时间点查看终端当天执行该业务的次数。



三、代码
TerminalBusiness.vue
java
<template>
<a-spin
:size="80"
:loading="uploadeLoading"
:tip="loadingTip"
style="width: 100%; height: 100%"
>
<layout_1>
<a-scrollbar style="height: 800px; overflow: auto">
<div class="--search-line">
<div>
<div class="key">{{ $t(queryButtonValue[7]) }} </div>
<div class="val">
<a-range-picker
style="width: 280px"
:allow-clear="false"
v-model="param.timeRange"
:disabled-date="disabledDate"
>
<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[10]) }} </div>
<div class="val select-input">
<!-- <a-select v-model="param.businessTypes" :multiple="true" :max-tag-count="1">
<a-option v-for="(val, key) in statBusinessType" :label="val" :value="key" :key="key"></a-option>
</a-select> -->
<a-tree-select
class="arco-tree-select --arco-select"
:data="treeData"
v-model="param.businessTypes"
:tree-checkable="true"
:tree-check-strictly="false"
tree-checked-strategy="child"
:max-tag-count="1"
/>
</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
>
<a-dropdown @select="handleSelect">
<a-button class="huge" :disabled="!tableData?.length">
<template #icon> <icon-export size="18" /> </template>
{{ $t(queryButtonValue[3]) }}
</a-button>
<template #content>
<a-doption value="excel">Excel</a-doption>
<a-doption value="pdf">PDF</a-doption>
<a-doption value="png">PNG</a-doption>
<a-doption value="jpeg">JPEG</a-doption>
</template>
</a-dropdown>
</div>
<div class="table-line" ref="exportContentRef">
<a-table
:data="tableData"
:loading="tableLoading"
:bordered="{ headerCell: true }"
:pagination="false"
:scroll="{ x: '100%', y: 550 }"
@row-click="handleRowClick"
>
<template #empty>
<div style="text-align: center">{{ $t("NoData") }}</div>
</template>
<template #columns>
<a-table-column
:title="$t(queryColumnValue[1])"
dataIndex="index"
:tooltip="true"
></a-table-column>
<a-table-column
:title="$t(queryColumnValue[79])"
dataIndex="terminalID"
:tooltip="true"
></a-table-column>
<a-table-column
:title="$t(queryColumnValue[61])"
v-if="param.businessTypes.includes(1)"
>
<a-table-column
:title="$t(queryColumnValue[66])"
dataIndex="radioTotalCount"
/>
<a-table-column
:title="$t(queryColumnValue[67])"
dataIndex="radioTotalTime"
>
<template #cell="{ record }">
{{ formatTime(record.radioTotalTime) }}
</template>
</a-table-column>
<a-table-column
:title="$t(queryColumnValue[68])"
dataIndex="radioAverageTime"
>
<template #cell="{ record }">
{{ formatTime(record.radioAverageTime) }}
</template>
</a-table-column>
</a-table-column>
<a-table-column
:title="$t(queryColumnValue[62])"
v-if="param.businessTypes.includes(2)"
>
<a-table-column
:title="$t(queryColumnValue[80])"
dataIndex="messageTotalCount"
/>
<a-table-column
:title="$t(queryColumnValue[81])"
dataIndex="messageTotalTime"
>
<template #cell="{ record }">
{{ formatTime(record.messageTotalTime) }}
</template>
</a-table-column>
</a-table-column>
<a-table-column
:title="$t(queryColumnValue[63])"
v-if="param.businessTypes.includes(3)"
>
<a-table-column
:title="$t(queryColumnValue[69])"
dataIndex="gpsTotalCount"
/>
<a-table-column
:title="$t(queryColumnValue[70])"
dataIndex="gpsTotalTime"
>
<template #cell="{ record }">
{{ formatTime(record.gpsTotalTime) }}
</template>
</a-table-column>
</a-table-column>
<a-table-column
:title="$t(queryColumnValue[64])"
v-if="param.businessTypes.includes(4)"
>
<a-table-column
:title="$t(queryColumnValue[71])"
dataIndex="registTotalCount"
/>
<a-table-column
:title="$t(queryColumnValue[72])"
dataIndex="registTotalTime"
>
<template #cell="{ record }">
{{ formatTime(record.registTotalTime) }}
</template>
</a-table-column>
</a-table-column>
<a-table-column
:title="$t(queryColumnValue[65])"
v-if="param.businessTypes.includes(5)"
>
<a-table-column
:title="$t(queryColumnValue[73])"
dataIndex="otherTotalCount"
/>
<a-table-column
:title="$t(queryColumnValue[74])"
dataIndex="otherTotalTime"
>
<template #cell="{ record }">
{{ formatTime(record.otherTotalTime) }}
</template>
</a-table-column>
</a-table-column>
<a-table-column
:title="$t(queryColumnValue[78])"
dataIndex="proportion"
:tooltip="true"
>
<template #cell="{ record }">
{{ `${(record.proportion * 100).toFixed(2)}%` }}
</template>
</a-table-column>
</template>
</a-table>
<div class="--table-pager">
<a-config-provider :locale="arcoLang">
<a-pagination
:total="page.total"
v-model:current="page.currentPage"
v-model:page-size="page.pageSize"
@change="getTableData"
@page-size-change="getTableData"
show-total
show-page-size
show-jumper
/></a-config-provider>
</div>
<div v-if="showRecordChart" class="chart_box">
<div class="title">{{ detailTitle }}</div>
<div class="content" id="terminal_business_content_box">
<div class="content-item box-1 inline">
<div class="content-item-title">
{{ $t(queryColumnValue[116]) }}
</div>
<div
:id="`voice_business_by_date_${recordInfo.terminalID}`"
class="chart-1"
></div>
</div>
<div class="content-item box-2 inline">
<div class="content-item-title">
{{ $t(queryColumnValue[117]) }}
</div>
<div
:id="`data_business_by_date_${recordInfo.terminalID}`"
class="chart-2"
></div>
<div class="content-item-icon">
<div
v-for="(item, index) in filteredbizTypesTitlesItems"
:key="index"
:id="`content-item-icon-container${index}`"
:class="{
'content-item-icon-container': true,
'content-item-icon-greyed-out-0': index === 0 && g0,
'content-item-icon-greyed-out-1': index === 1 && g1,
'content-item-icon-greyed-out-2': index === 2 && g2,
'content-item-icon-greyed-out-3': index === 3 && g3,
}"
@click="
toggleLine($t(queryColumnValue[item.enumValueIndex]), index)
"
>
<div
class="content-item-icon-container-inline"
:style="{ backgroundColor: item.color }"
></div>
<div class="content-item-icon-container-text">
{{ $t(queryColumnValue[item.enumValueIndex]) }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</a-scrollbar>
</layout_1>
</a-spin>
</template>
<script setup>
import {
commonResponse,
formatDateToYyyyMMddHHmmss,
formatTime,
} from "@/views/pages/_common";
import {
queryButtonValue,
queryColumnValue,
queryName,
statBusinessType,
} from "@/views/pages/_common/enum";
import Layout_1 from "@/views/pages/_common/layout_1.vue";
import {
exportTerminalBusinessDetailCountByDayBizType,
exportTerminalBusinessStat,
getTerminalBusinessStatList,
} from "@/views/pages/business/_request";
import * as echarts from "echarts";
import html2canvas from "html2canvas";
import JsPDF from "jspdf";
import * as moment from "moment";
import { computed, inject, reactive, ref, watch } from "vue";
const arcoLang = inject("arcoLang");
const t = inject("t");
const exportType = ref('');
const uploadeLoading = ref(false);
const param = reactive({
timeRange: [null, null],
businessTypes: [],
});
let reqParam = {
startTime: null,
endTime: null,
businessTypes: [],
};
const page = reactive({
currentPage: 1,
pageSize: 10,
total: 0,
});
const treeData = [
{
title: t(statBusinessType[0]), // 根节点 (全选项)
value: 0,
key: 0,
children: Object.keys(statBusinessType)
.slice(1)
.map((key) => ({
title: t(statBusinessType[key]),
value: Number(key),
key: Number(key),
})),
},
];
// 监听 param.businessTypes 的变化
watch(
() => param.businessTypes,
(newValue) => {
if (newValue.includes(0)) {
// 如果用户选择了全选(即选择了 0),将其转换为 1, 2, 3, 4, 5 的数组
param.businessTypes = [1, 2, 3, 4, 5];
}
}
);
const showRecordChart = ref(false);
const tableLoading = ref(false);
const exportContentRef = ref(null);
const tableData = ref([]);
const getTableData = () => {
tableLoading.value = true;
getTerminalBusinessStatList({
...reqParam,
currentPage: page.currentPage,
pageSize: page.pageSize,
})
.then((response) => {
tableLoading.value = false;
commonResponse({
response,
onSuccess: () => {
tableData.value = response.data;
page.total = response.pagination.totalRecords;
},
});
})
.catch((e) => (tableLoading.value = true));
};
const disabledDate = (date) => {
return date.getTime() > moment().format("x");
};
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;
getTableData();
};
const resetSearch = () => {
param.timeRange = [moment().add(-9, "days"), moment()];
param.businessTypes = [];
page.currentPage = 1;
page.total = 0;
tableData.value = [];
showRecordChart.value = false;
};
const loadingTip = computed(() => {
if (!exportType.value) return t('ExportStatus_Exporting');
return `${exportType.value} ${t('Outputting')}`;
});
const handleSelect = (t) => {
exportType.value = t;
uploadeLoading.value = true;
exportFormat(t);
};
// 导出功能
const exportTable = async () => {
tableLoading.value = true;
try {
const response = await exportTerminalBusinessStat({
...reqParam,
});
tableLoading.value = false;
// 创建 Blob 对象
const blob = new Blob([response], {
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
});
const url = window.URL.createObjectURL(blob);
// 创建一个临时的 <a> 标签来触发下载
const link = document.createElement("a");
link.href = url;
link.download =
t(queryName["RadioService"]) + formatDateToYyyyMMddHHmmss(new Date()); // 设置文件保存名称
link.click();
// 清理 URL 对象
window.URL.revokeObjectURL(url);
} catch (error) {
tableLoading.value = true;
console.error("导出.excel图片失败:", error);
} finally {
setTimeout(() => uploadeLoading.value = false, 3000);
}
};
// 处理不同格式的导出请求
const exportFormat = (format) => {
const title =
t(queryName["RadioService"]) + formatDateToYyyyMMddHHmmss(new Date());
if (format === "excel") {
exportTable();
} else if (format === "pdf") {
exportPdf(exportContentRef.value, title);
} else if (format === "png") {
exportPng(exportContentRef.value, title);
} else if (format === "jpeg") {
exportJpeg(exportContentRef.value, title);
}
};
// 导出 PDF
const exportPdf = async (el, title) => {
// 获取元素的实际高度
const contentHeight = el.scrollHeight;
const contentWidth = el.scrollWidth;
const getDynamicScale = (element) => {
try {
if (!element || !element.getBoundingClientRect) {
console.warn('getDynamicScale: 元素无效或不存在');
return 1;
}
const rect = element.getBoundingClientRect();
const area = rect.width * rect.height;
if (isNaN(area) || area <= 0) {
console.warn('getDynamicScale: 计算出的面积无效', { width: rect.width, height: rect.height, area });
return 1;
}
console.log('area', area, 'width:', rect.width, 'height:', rect.height);
const areaMB = area / 1000000;
if (!isFinite(areaMB)) {
console.warn('getDynamicScale: areaMB无效', areaMB);
return 1;
}
if (areaMB > 20) {
return 0.5;
} else if (areaMB > 15) {
return 0.75;
} else if (areaMB > 13) {
return 0.8;
} else if (areaMB > 10) {
return 0.9;
} else if (areaMB > 5) {
return 0.95;
} else if (areaMB > 4) {
return 1;
} else if (areaMB > 3) {
return 1.25;
} else if (areaMB > 2.5) {
return 1.5;
} else if (areaMB > 2) {
return 1.5;
} else {
return 2;
}
} catch (error) {
console.error('getDynamicScale 发生错误:', error);
return 1;
}
};
const scale = getDynamicScale(el);
try {
const canvas = await html2canvas(el, {
allowTaint: true,
taintTest: false,
useCORS: true,
logging: false,
imageTimeout: 0,
scale: scale,
scrollX: 0,
scrollY: 0,
width: contentWidth,
height: contentHeight,
});
let pageHeight = (contentWidth / 592.28) * 841.89;
let leftHeight = contentHeight;
let position = 0;
let imgWidth = 592.28;
let imgHeight = (592.28 / contentWidth) * contentHeight;
let pageData = canvas.toDataURL("image/jpeg", 1.0);
let PDF = new JsPDF("", "pt", "a4");
if (leftHeight < pageHeight) {
PDF.addImage(pageData, "JPEG", 0, 0, imgWidth, imgHeight);
} else {
while (leftHeight > 0) {
PDF.addImage(pageData, "JPEG", 0, position, imgWidth, imgHeight);
leftHeight -= pageHeight;
position -= imgHeight;
if (leftHeight > 0) {
PDF.addPage();
}
}
}
PDF.save(title + ".pdf");
} catch (error) {
console.error("导出.pdf图片失败:", error);
} finally {
setTimeout(() => uploadeLoading.value = false, 3000);
}
};
// 导出 PNG
const exportPng = async (el, title) => {
const contentHeight = el.scrollHeight;
const contentWidth = el.scrollWidth;
try {
const canvas = await html2canvas(el, {
allowTaint: true,
taintTest: false,
useCORS: true,
logging: false,
imageTimeout: 0,
scale: 2,
scrollX: 0,
scrollY: 0,
width: contentWidth,
height: contentHeight,
});
let url = canvas.toDataURL("image/png");
let a = document.createElement("a");
a.href = url;
a.download = title + ".png";
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (error) {
console.error("导出.png图片失败:", error);
} finally {
setTimeout(() => uploadeLoading.value = false, 3000);
}
};
// 导出 JPEG
const exportJpeg = async (el, title) => {
const contentHeight = el.scrollHeight;
const contentWidth = el.scrollWidth;
try {
const canvas = await html2canvas(el, {
allowTaint: true,
taintTest: false,
useCORS: true,
logging: false,
imageTimeout: 0,
scale: 2,
scrollX: 0,
scrollY: 0,
width: contentWidth,
height: contentHeight,
});
let url = canvas.toDataURL("image/jpeg", 0.9);
let a = document.createElement("a");
a.href = url;
a.download = title + ".jpg";
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
} catch (error) {
console.error("导出.jpg图片失败:", error);
} finally {
setTimeout(() => uploadeLoading.value = false, 3000);
}
};
let detailCountParam = {
businessTypes: [],
startTime: null,
endTime: null,
radioIDs: [],
};
let detailCountData = {};
const getDetailCountByDayBizType = (record) => {
exportTerminalBusinessDetailCountByDayBizType({
...detailCountParam,
currentPage: page.currentPage,
pageSize: page.pageSize,
}).then((response) => {
commonResponse({
response,
onSuccess: () => {
detailCountData = response.data;
renderChartVoice(detailCountData, record);
renderChartOtherData(detailCountData, record);
},
});
});
};
const detailTitle = ref("");
const recordInfo = ref({});
//终端业务列标题枚举的索引
const bizTypesTitles = [
{ bizTypeValue: 1, enumValueIndex: 61, color: "#2FA5FB" },
{ bizTypeValue: 3, enumValueIndex: 63, color: "#7D51BC" },
{ bizTypeValue: 2, enumValueIndex: 62, color: "#2987B6" },
{ bizTypeValue: 4, enumValueIndex: 64, color: "#7A7CED" },
{ bizTypeValue: 5, enumValueIndex: 65, color: "#435088" },
];
const filteredbizTypesTitlesItems = computed(() => bizTypesTitles.slice(1));
const handleRowClick = (record) => {
showRecordChart.value = true;
detailTitle.value = t(queryColumnValue[115]) + `(Radio-${record.terminalID})`;
recordInfo.value = record;
detailCountParam.businessTypes = reqParam.businessTypes;
detailCountParam.startTime = reqParam.startTime;
detailCountParam.endTime = reqParam.endTime;
detailCountParam.radioIDs = [record.terminalID];
getDetailCountByDayBizType(record);
};
const renderChartVoice = (detailCountData, record) => {
const x = Object.keys(detailCountData).sort();
const y_1 = [];
x.forEach((key) => {
let nestedMap = detailCountData[key];
if (
nestedMap[bizTypesTitles[0].bizTypeValue] !== undefined &&
nestedMap[bizTypesTitles[0].bizTypeValue] !== null
) {
y_1.push(nestedMap[bizTypesTitles[0].bizTypeValue]);
}
});
const chartDomVoice = document.querySelector(
`#voice_business_by_date_${record.terminalID}`
);
const chart = echarts.init(chartDomVoice);
const option = {
color: ["#2FA5FB"],
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross", // 鼠标滑过时显示交叉线
},
formatter: function (params) {
// 获取日期(x 轴的值)
const date = params[0].axisValue;
// 获取 y 轴的次数(即每个系列的值)
const count = params[0].value;
// 返回格式化后的内容
return `${date}<br>次数: ${count}`;
},
},
legend: {
itemGap: 20,
textStyle: {
color: "#202B40",
fontFamily: "PingFang SC",
fontSize: 13,
fontWeight: 400,
lineHeight: 22,
},
},
grid: {
left: "3%",
right: "10%",
bottom: "6%",
containLabel: true,
},
toolbox: {
feature: {
saveAsImage: {},
},
},
xAxis: {
type: "category",
boundaryGap: false,
data: x,
triggerEvent: true,
},
yAxis: {
type: "value",
name: t(queryColumnValue[118]),
nameTextStyle: {
color: "#778091",
fontFamily: "PingFang SC",
fontSize: 12,
fontWeight: 400,
lineHeight: 12,
align: "left",
},
minInterval: 1,
},
series: [
{
type: "line",
data: y_1,
symbol: "circle",
symbolSize: 5,
connectNulls: true,
},
],
};
option && chart.setOption(option);
};
let globalChartDomOtherData;
const renderChartOtherData = (detailCountData, record) => {
const x = Object.keys(detailCountData).sort();
const y_1 = [];
for (let i = 1; i < bizTypesTitles.length; i++) {
const y_2 = [];
x.forEach((key) => {
let nestedMap = detailCountData[key];
y_2.push(nestedMap[bizTypesTitles[i].bizTypeValue]);
});
y_1.push({ y_2: y_2, bizTypesTitles: bizTypesTitles[i] });
}
const chartDomOtherData = document.querySelector(
`#data_business_by_date_${record.terminalID}`
);
const chart = echarts.init(chartDomOtherData);
globalChartDomOtherData = chart;
const series = [];
for (let i = 0; i < y_1.length; i++) {
series.push({
name: t(queryColumnValue[y_1[i].bizTypesTitles.enumValueIndex]),
type: "line",
areaStyle: {},
data: y_1[i].y_2,
symbol: "circle",
symbolSize: 5,
connectNulls: true,
lineStyle: {
opacity: 0.7, // 透明度
width: 2,
color: y_1[i].bizTypesTitles.color,
emphasis: {
color: y_1[i].bizTypesTitles.color,
},
},
});
}
const option = {
color: ["#7D51BC", "#2987B6", "#7A7CED", "#435088"],
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross", // 鼠标滑过时显示交叉线
},
},
legend: {
show: false,
itemGap: 20,
textStyle: {
color: "#202B40",
fontFamily: "PingFang SC",
fontSize: 13,
fontWeight: 400,
lineHeight: 22,
},
},
grid: {
left: "6%",
right: "10%",
bottom: "8%",
containLabel: true,
},
toolbox: {
feature: {
saveAsImage: {},
},
},
xAxis: {
type: "category",
boundaryGap: false,
data: x,
triggerEvent: true,
},
yAxis: {
type: "value",
name: t(queryColumnValue[118]),
nameTextStyle: {
color: "#778091",
fontFamily: "PingFang SC",
fontSize: 12,
fontWeight: 400,
lineHeight: 12,
align: "left",
},
minInterval: 1,
},
series: series,
};
option && chart.setOption(option);
};
const g0 = ref(false);
const g1 = ref(false);
const g2 = ref(false);
const g3 = ref(false);
const toggleLine = (name, index) => {
if (index == 0) {
g0.value = !g0.value;
}
if (index == 1) {
g1.value = !g1.value;
}
if (index == 2) {
g2.value = !g2.value;
}
if (index == 3) {
g3.value = !g3.value;
}
const option = globalChartDomOtherData.getOption();
if (Object.prototype.hasOwnProperty.call(option.legend[0].selected, name)) {
option.legend[0].selected[name] = !option.legend[0].selected[name];
} else {
option.legend[0].selected[name] = false;
}
globalChartDomOtherData.setOption(option, true);
};
const init = () => {
resetSearch();
};
init();
</script>
<style scoped lang="less">
.table-line {
box-sizing: border-box;
margin-top: 20px;
height: calc(100% - 60px);
}
.select-input {
width: 160px;
}
.title {
margin-top: 20px;
margin-left: 40px;
margin-top: 10px;
padding-left: 10px;
}
.content {
width: 100%;
margin-top: 20px;
&-item {
position: relative;
box-sizing: border-box;
border: 1px solid #e5e7ec;
border-radius: 10px;
padding: 20px;
&.box-1 {
width: 45%;
margin-left: 40px;
margin-right: 40px;
height: 420px;
.chart-1 {
height: 345px;
}
}
&.box-2 {
margin-left: 30px;
width: 45%;
height: 420px;
.chart-2 {
height: 345px;
}
}
&-title {
color: #192840;
font-family: "PingFang SC";
font-size: 18px;
font-style: normal;
font-weight: 600;
line-height: 28px; /* 155.556% */
text-align: center;
}
&-icon {
display: flex;
justify-content: center;
&-greyed-out-0 {
opacity: 0.5; /* 置灰效果 */
}
&-greyed-out-1 {
opacity: 0.5; /* 置灰效果 */
}
&-greyed-out-2 {
opacity: 0.5; /* 置灰效果 */
}
&-greyed-out-3 {
opacity: 0.5; /* 置灰效果 */
}
&-container {
display: flex;
cursor: pointer;
/* 中间的所有子元素 */
&:not(:first-child) {
margin-left: 40px;
}
&-inline {
width: 28px;
height: 15px;
border-radius: 5px;
margin-right: 5px;
}
&-text {
color: #192840;
font-family: "PingFang SC";
font-size: 12px;
font-style: normal;
line-height: 15px;
text-align: center;
}
}
}
}
}
</style>
RadioStatisticController
java
package com.xnms.client.service.controller.data.statistic;
import com.xnms.client.service.controller.common.ResponseModel;
import com.xnms.client.service.view.naviagation.page.data.statistice.RadioStatisticUserControl;
import com.xnms.client.service.view.naviagation.page.data.statistice.StatisticSelectUserControl;
import com.xnms.data.contract.MultiResponse;
import com.xnms.data.contract.client.RadioTeleTrafficStatisticTable;
import com.xnms.data.contract.database.db.Pagination;
import com.xnms.data.contract.database.db.QueryConditionBase;
import com.xnms.data.contract.database.db.StatUserTraffic;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Map;
@Api(tags = "业务统计 - 终端业务")
@RestController
@RequestMapping("/api/radio_statistic")
public class RadioStatisticController implements InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(RadioStatisticController.class);
@Autowired
private StatisticSelectUserControl statisticSelectUserControl;
@Autowired
private RadioStatisticUserControl radioStatisticUserControl;
@Override
public void afterPropertiesSet() throws Exception {
}
@Operation(summary = "统计列表")
@PostMapping
public ResponseModel<List<RadioTeleTrafficStatisticTable>> statisticRadio(@RequestBody QueryConditionBase queryConditionBase) {
logger.info("RadioStatisticController,statisticRadio:queryConditionBase = [{}]", queryConditionBase);
String msg = statisticSelectUserControl.verifyStatisticQueryCondition(queryConditionBase, "Query_Operate_Exception");
if (!StringUtils.isBlank(msg)) {
return ResponseModel.ofError(msg);
}
MultiResponse<RadioTeleTrafficStatisticTable> radioTeleTrafficStatisticTableMultiResponse = radioStatisticUserControl.statisticRadio(queryConditionBase);
return ResponseModel.ofSuccess(radioTeleTrafficStatisticTableMultiResponse.getData(), Pagination.of(queryConditionBase.getCurrentPage(), queryConditionBase.getPageSize(), radioTeleTrafficStatisticTableMultiResponse.getTotalCount()));
}
@Operation(summary = "导出")
@PostMapping(value = "/excel")
public ResponseModel<String> exportExcelFile(@RequestBody QueryConditionBase queryConditionBase) {
logger.info("RadioStatisticController,exportExcelFile:queryConditionBase = [{}]", queryConditionBase);
String filePath = radioStatisticUserControl.doFile(queryConditionBase, radioStatisticUserControl.exportToExcel(queryConditionBase));
return ResponseModel.ofSuccess(filePath);
}
@Operation(summary = "导出,返回流")
@PostMapping(value = "/excel_stream")
public void exportExcelStream(@RequestBody QueryConditionBase queryConditionBase, HttpServletResponse httpServletResponse) throws IOException {
logger.info("RadioStatisticController,exportExcelStream:queryConditionBase = [{}]", queryConditionBase);
radioStatisticUserControl.doStream(queryConditionBase, radioStatisticUserControl.exportToExcel(queryConditionBase), httpServletResponse);
}
@Operation(summary = "详情")
@PostMapping("/detail")
public ResponseModel<List<StatUserTraffic>> statisticRadioDetail(@RequestBody QueryConditionBase queryConditionBase) {
logger.info("RadioStatisticController,statisticRadioDetail:queryConditionBase = [{}]", queryConditionBase);
List<StatUserTraffic> radioTeleTrafficStatisticTableMultiResponse = radioStatisticUserControl.statisticRadioDetail(queryConditionBase);
return ResponseModel.ofSuccess(radioTeleTrafficStatisticTableMultiResponse, Pagination.of(queryConditionBase.getCurrentPage(), queryConditionBase.getPageSize(), radioTeleTrafficStatisticTableMultiResponse.size()));
}
@Operation(summary = "详情分业务统计次数")
@PostMapping("/detail_count")
public ResponseModel<Map<String, Map<Integer, Long>>> statisticRadioDetailCountByDayBizType(@RequestBody QueryConditionBase queryConditionBase) {
logger.info("RadioStatisticController,statisticRadioDetail:queryConditionBase = [{}]", queryConditionBase);
List<StatUserTraffic> radioTeleTrafficStatisticTableMultiResponse = radioStatisticUserControl.statisticRadioDetail(queryConditionBase);
Map<String, Map<Integer, Long>> stringMapMap = radioStatisticUserControl.calculateCountByBizType(radioTeleTrafficStatisticTableMultiResponse,queryConditionBase);
return ResponseModel.ofSuccess(stringMapMap, Pagination.of(queryConditionBase.getCurrentPage(), queryConditionBase.getPageSize(), radioTeleTrafficStatisticTableMultiResponse.size()));
}
}
StacUserTrafficDaoImpl
java
package com.xnms.data.service.dao.mysql.impl;
import com.xnms.data.contract.client.RadioTeleTrafficStatisticTable;
import com.xnms.data.contract.database.db.QueryConditionBase;
import com.xnms.data.contract.database.db.StatUserTraffic;
import com.xnms.data.service.dao.StacUserTrafficDao;
import com.xnms.data.service.entity.StacUserTrafficEntity;
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.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Repository
public class StacUserTrafficDaoImpl implements StacUserTrafficDao {
private static final Logger logger = LoggerFactory.getLogger(StacUserTrafficDaoImpl.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<StacUserTrafficEntity> getStacUserTrafficEntityList(Date fromTime, Date endTime) {
List<StacUserTrafficEntity> stacUserTrafficEntityList = new ArrayList<>();
String sql = "SELECT * FROM stac_usertraffic_day sp WHERE sp.Date >= :fromTime AND sp.Date <= :endTime";
try {
Query query = entityManager.createNativeQuery(getSql(sql), StacUserTrafficEntity.class);
query.setParameter("fromTime", yyyyMMdd.format(fromTime));
query.setParameter("endTime", yyyyMMdd.format(endTime));
List<StacUserTrafficEntity> resultList = query.getResultList();
stacUserTrafficEntityList.addAll(resultList);
} catch (Exception ex) {
// 日志记录
logger.error("<StacUserTrafficDaoImpl getStacUserTrafficEntityList> Error: {}, SQL: {}", ex.getMessage(), sql, ex);
}
return stacUserTrafficEntityList;
}
@Override
public List<StacUserTrafficEntity> getStacUserTrafficEntityList() {
return null;
}
@Override
public void insertOrUpdateStacUserTrafficEntityList(List<String> sqlList) {
}
// 计算日期方法
private Date getDate(int daysOffset) {
return new Date(System.currentTimeMillis() + (daysOffset * 24L * 60L * 60L * 1000L));
}
@Transactional
@Override
public Boolean doStacUserTraffic() {
List<String> sqlList = new ArrayList<>();
try {
for (int i = 0; i < 2; i++) {
String date = yyyyMMdd.format(getDate(i - 1));
String tableName = DateUtil.convertStartTimeToTableName(getDate(i - 1));
// DELETE SQL
String deleteSql = String.format("DELETE FROM stac_usertraffic_day WHERE date='%s';", date);
sqlList.add(deleteSql);
// Calling Insert SQL
String callingInsertSql = String.format(
"INSERT INTO stac_usertraffic_day (radioId, date, count, durationTime, biztype, calling, groupId, calltype) " +
"SELECT senderid AS radioId, " +
"DATE_FORMAT(starttime, '%%Y-%%m-%%d') AS date, " +
"COUNT(*) AS count, " +
"SUM(duration) AS durationTime, " +
"type AS bizType, " +
"1 AS calling, -1 AS groupId, " +
"calltype AS calltype " +
"FROM %s " +
"WHERE (status = 0 OR status = 6 OR status = 11) " +
"AND DATE_FORMAT(starttime, '%%Y-%%m-%%d') = '%s' " +
"GROUP BY senderid, DATE_FORMAT(starttime, '%%Y-%%m-%%d'), type, calltype;",
tableName, date
);
sqlList.add(callingInsertSql);
// Called Insert SQL
String calledInsertSql = String.format(
"INSERT INTO stac_usertraffic_day (radioId, date, count, durationTime, biztype, calling, groupId, calltype) " +
"SELECT " +
"(CASE WHEN calltype IN (1, 2, 3, 5, 7, 12) THEN -1 ELSE targetid END) AS radioId, " +
"DATE_FORMAT(starttime, '%%Y-%%m-%%d') AS date, " +
"COUNT(*) AS count, " +
"SUM(duration) AS durationTime, " +
"type AS bizType, " +
"0 AS calling, " +
"(CASE WHEN calltype IN (1, 2, 3, 5, 7, 12) THEN targetid ELSE -1 END) AS groupId, " +
"calltype AS calltype " +
"FROM %s " +
"WHERE (status = 0 OR status = 6 OR status = 11) " +
"AND (type != 3 AND type != 4) " +
"AND DATE_FORMAT(starttime, '%%Y-%%m-%%d') = '%s' " +
"GROUP BY targetid, DATE_FORMAT(starttime, '%%Y-%%m-%%d'), type, calltype;",
tableName, date
);
sqlList.add(calledInsertSql);
}
// 批量执行 SQL
if (!sqlList.isEmpty()) {
for (String sql : sqlList) {
Query query = entityManager.createNativeQuery(sql);
query.executeUpdate();
}
}
return true;
} catch (Exception ex) {
// 错误日志记录
logger.error("StacUserTrafficDaoImpl类,doStacUserTraffic方法:SQL:{}", sqlList.toString(), ex);
return false;
}
}
@Override
public List<StatUserTraffic> getRadioIdFromPrivateCallStatistics() {
return null;
}
@Override
public List<StatUserTraffic> obtainRadioTeleTrafficDataDetail(QueryConditionBase queryCondition) {
List<StatUserTraffic> radioTeleTrafficStatisticRecords = new ArrayList<>();
// 检查查询条件是否有效
if (queryCondition == null || queryCondition.getStartTime() == null || queryCondition.getEndTime() == null ||
queryCondition.getBusinessTypes() == null || queryCondition.getBusinessTypes().isEmpty() ||
queryCondition.getRadioIDs() == null || queryCondition.getRadioIDs().isEmpty()) {
logger.debug("[StacUserTrafficDao] obtainRadioTeleTrafficDataDetail: QueryCondition Invalid.");
return radioTeleTrafficStatisticRecords;
}
// 处理业务类型(如果业务类型是 5,则加上 6 和 7)
List<Integer> bizTypes = new ArrayList<>();
for (Integer businessType : queryCondition.getBusinessTypes()) {
if (businessType != 5) {
bizTypes.add(businessType);
} else {
bizTypes.add(5);
bizTypes.add(6);
bizTypes.add(7);
}
}
// 构建 SQL 查询语句
try {
String sql = "SELECT radioId, date, count, durationTime durationtime, biztype, calling " +
"FROM stac_usertraffic_day " +
"WHERE biztype IN (:bizTypes) " +
"AND radioId IN (:radioIds) " +
"AND date >= :startDate " +
"AND date <= :endDate " +
"AND radioId > 0";
// 创建原生 SQL 查询并设置参数
Query query = entityManager.createNativeQuery(getSql(sql), StatUserTraffic.class);
query.setParameter("bizTypes", bizTypes);
query.setParameter("radioIds", queryCondition.getRadioIDs());
query.setParameter("startDate", yyyyMMdd.format(queryCondition.getStartTime()));
query.setParameter("endDate", yyyyMMdd.format(queryCondition.getEndTime()));
// 执行查询
List<StatUserTraffic> resultList = query.getResultList();
// 将查询结果加入到返回列表中
if (resultList != null) {
radioTeleTrafficStatisticRecords.addAll(resultList);
}
} catch (Exception ex) {
logger.error("[StacUserTrafficDao] obtainRadioTeleTrafficDataDetail: " + ex.getMessage(), ex);
}
return radioTeleTrafficStatisticRecords;
}
@Override
public int obtainRadioTeleTrafficCount(QueryConditionBase queryCondition) {
int totalCount = 0;
// Validate the input
if (queryCondition == null || queryCondition.getStartTime() == null || queryCondition.getEndTime() == null || queryCondition.getBusinessTypes() == null || queryCondition.getBusinessTypes().isEmpty()) {
logger.debug("[StacUserTrafficDaoImpl] obtainRadioTeleTrafficCount : QueryCondition Invalid.");
return totalCount;
}
List<Integer> bizTypes = new ArrayList<>();
for (Integer businessType : queryCondition.getBusinessTypes()) {
if (businessType != 5) {
bizTypes.add(businessType);
} else {
bizTypes.add(5);
bizTypes.add(6);
bizTypes.add(7);
}
}
try {
String startDateStr = yyyyMMdd.format(queryCondition.getStartTime());
String endDateStr = yyyyMMdd.format(queryCondition.getEndTime());
// Construct the SQL query
String dmlSql = "SELECT count(1) cnt FROM " +
"( SELECT radioId FROM stac_usertraffic_day " +
"WHERE biztype IN (:bizTypes) AND radioid > 0 " +
"AND `DATE` >= :startDate AND `DATE` <= :endDate " +
"GROUP BY radioId ) a";
// Create the native query with parameters
Query query = entityManager.createNativeQuery(dmlSql);
query.setParameter("bizTypes", bizTypes);
query.setParameter("startDate", startDateStr);
query.setParameter("endDate", endDateStr);
// Execute the query and retrieve the result
Object returnValue = query.getSingleResult();
if (returnValue != null) {
totalCount = Integer.parseInt(returnValue.toString());
}
} catch (Exception ex) {
logger.error("[StacUserTrafficDaoImpl] obtainRadioTeleTrafficCount : " + ex.getMessage(), ex);
}
return totalCount;
}
@Override
public List<RadioTeleTrafficStatisticTable> obtainRadioTeleTrafficData(QueryConditionBase queryCondition, int pageIndex, int pageSize) {
List<RadioTeleTrafficStatisticTable> radioTeleTrafficStatisticRecords = new ArrayList<>();
// Validate input
if (queryCondition == null || queryCondition.getStartTime() == null || queryCondition.getEndTime() == null || queryCondition.getBusinessTypes() == null || queryCondition.getBusinessTypes().isEmpty()) {
logger.debug("[StacUserTrafficDaoImpl] obtainRadioTeleTrafficData: QueryCondition Invalid.");
return radioTeleTrafficStatisticRecords;
}
List<Integer> bizTypes = new ArrayList<>();
for (Integer businessType : queryCondition.getBusinessTypes()) {
if (businessType != 5) {
bizTypes.add(businessType);
} else {
bizTypes.add(5);
bizTypes.add(6);
bizTypes.add(7);
}
}
try {
// Calculate offset
int offset = (pageIndex - 1) * pageSize;
;
// Prepare date format
String startDateStr = yyyyMMdd.format(queryCondition.getStartTime());
String endDateStr = yyyyMMdd.format(queryCondition.getEndTime());
// Construct SQL query
String dmlSql = "SELECT IFNULL(voice_duration_time / voice_count, 0) voice_average_time, a.* FROM (" +
"SELECT radioId radio_id, SUM(durationTime) totaltime, " +
"SUM(CASE biztype WHEN '1' THEN count ELSE 0 END) voice_count, " +
"SUM(CASE biztype WHEN '1' THEN durationTime ELSE 0 END) voice_duration_time, " +
"SUM(CASE biztype WHEN '2' THEN count ELSE 0 END) message_count, " +
"SUM(CASE biztype WHEN '2' THEN durationTime ELSE 0 END) message_duration_time, " +
"SUM(CASE biztype WHEN '3' THEN count ELSE 0 END) gps_count, " +
"SUM(CASE biztype WHEN '3' THEN durationTime ELSE 0 END) gps_duration_time, " +
"SUM(CASE biztype WHEN '4' THEN count ELSE 0 END) register_count, " +
"SUM(CASE biztype WHEN '4' THEN durationTime ELSE 0 END) register_duration_time, " +
"SUM(CASE WHEN biztype IN ('5', '6', '7') THEN count ELSE 0 END) other_count, " +
"SUM(CASE WHEN biztype IN ('5', '6', '7') THEN durationTime ELSE 0 END) other_duration_time " +
"FROM stac_usertraffic_day " +
"WHERE biztype IN (:bizTypes) AND radioid > 0 AND `DATE` >= :startDate AND `DATE` <= :endDate " +
"GROUP BY radioId) a " +
"ORDER BY a.totaltime DESC " +
"LIMIT :offset, :size";
// Create native query with parameters
Query query = entityManager.createNativeQuery(getSql(dmlSql), RadioTeleTrafficStatisticTable.class);
query.setParameter("bizTypes", bizTypes);
query.setParameter("startDate", startDateStr);
query.setParameter("endDate", endDateStr);
query.setParameter("offset", offset);
query.setParameter("size", pageSize);
// Execute query and get result
radioTeleTrafficStatisticRecords = query.getResultList();
BigDecimal terminalsTotalTime = obtainRadioTeleTrafficTotalTime(queryCondition);
int startIndex = (pageIndex - 1) * pageSize;
int i = 0;
for (RadioTeleTrafficStatisticTable item : radioTeleTrafficStatisticRecords) {
item.setIndex(startIndex + i + 1);
item.setProportion(item.getTotalTime().divide(terminalsTotalTime, 4, BigDecimal.ROUND_HALF_UP));
i++;
}
} catch (Exception ex) {
logger.error("[StacUserTrafficDaoImpl] obtainRadioTeleTrafficData: " + ex.getMessage(), ex);
}
return radioTeleTrafficStatisticRecords;
}
public BigDecimal obtainRadioTeleTrafficTotalTime(QueryConditionBase queryCondition) {
BigDecimal totalTime = BigDecimal.valueOf(Long.MAX_VALUE); // 默认返回最大值
// 参数校验
if (queryCondition == null || queryCondition.getStartTime() == null || queryCondition.getEndTime() == null
|| queryCondition.getBusinessTypes() == null || queryCondition.getBusinessTypes().isEmpty()) {
logger.debug("[StacUserTrafficDaoImpl] obtainRadioTeleTrafficTotalTime: QueryCondition Invalid.");
return totalTime;
}
try {
String startDateStr = yyyyMMdd.format(queryCondition.getStartTime());
String endDateStr = yyyyMMdd.format(queryCondition.getEndTime());
// 构建 SQL 查询语句
String sql = "SELECT IFNULL(SUM(durationTime), 0) AS totalTime FROM stac_usertraffic_day " +
"WHERE biztype IN (:bizTypes) AND radioid > 0 AND DATE >= :startDate AND DATE <= :endDate";
// 创建查询对象
Query query = entityManager.createNativeQuery(sql);
query.setParameter("bizTypes", queryCondition.getBusinessTypes());
query.setParameter("startDate", startDateStr);
query.setParameter("endDate", endDateStr);
// 执行查询并获取结果
Object result = query.getSingleResult();
// 获取查询结果中的 totalTime
if (result != null) {
totalTime = new BigDecimal(result.toString());
}
// 如果 totalTime 为零,设置为最大值
if (totalTime.compareTo(BigDecimal.ZERO) == 0) {
totalTime = BigDecimal.valueOf(Long.MAX_VALUE);
}
} catch (Exception ex) {
logger.error("[StacUserTrafficDaoImpl] obtainRadioTeleTrafficTotalTime: " + ex.getMessage(), ex);
}
return totalTime;
}
@Transactional
@Override
public Boolean doStacUserTraffic(Date stacDate, Date todayStart, Date tomorrowStart) {
List<String> sqlList = new ArrayList<>();
try {
String date = yyyyMMdd.format(stacDate);
String tableName = DateUtil.convertStartTimeToTableName(stacDate);
// DELETE SQL
String deleteSql = String.format("DELETE FROM stac_usertraffic_day WHERE date='%s';", date);
sqlList.add(deleteSql);
// Calling Insert SQL
String callingInsertSql = String.format(
"INSERT INTO stac_usertraffic_day (radioId, date, count, durationTime, biztype, calling, groupId, calltype) " +
"SELECT senderid AS radioId, " +
"DATE_FORMAT(starttime, '%%Y-%%m-%%d') AS date, " +
"COUNT(*) AS count, " +
"SUM(duration) AS durationTime, " +
"type AS bizType, " +
"1 AS calling, -1 AS groupId, " +
"calltype AS calltype " +
"FROM %s " +
"WHERE status in (0,6,11) " +
"AND STARTTIME between '%s' and '%s' " +
"GROUP BY senderid, date, type, calltype;",
tableName, YMDHMS.format(todayStart), YMDHMS.format(tomorrowStart)
);
sqlList.add(callingInsertSql);
// Called Insert SQL
String calledInsertSql = String.format(
"INSERT INTO stac_usertraffic_day (radioId, date, count, durationTime, biztype, calling, groupId, calltype) " +
"SELECT " +
"(CASE WHEN calltype IN (1, 2, 3, 5, 7, 12) THEN -1 ELSE targetid END) AS radioId, " +
"DATE_FORMAT(starttime, '%%Y-%%m-%%d') AS date, " +
"COUNT(*) AS count, " +
"SUM(duration) AS durationTime, " +
"type AS bizType, " +
"0 AS calling, " +
"(CASE WHEN calltype IN (1, 2, 3, 5, 7, 12) THEN targetid ELSE -1 END) AS groupId, " +
"calltype AS calltype " +
"FROM %s " +
"WHERE status in (0,6,11) " +
"AND type not in (3,4) " +
"AND STARTTIME between '%s' and '%s' " +
"GROUP BY targetid, date, type, calltype;",
tableName, YMDHMS.format(todayStart), YMDHMS.format(tomorrowStart)
);
sqlList.add(calledInsertSql);
// 批量执行 SQL
if (!sqlList.isEmpty()) {
for (String sql : sqlList) {
Query query = entityManager.createNativeQuery(sql);
query.executeUpdate();
}
}
return true;
} catch (Exception ex) {
// 错误日志记录
logger.error("StacUserTrafficDaoImpl类,doStacUserTraffic方法:SQL:{}",sqlList.toString(),ex);
return false;
}
}
}